-
Notifications
You must be signed in to change notification settings - Fork 729
Treat record structs as records #2009
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔧 You also need to update the releases.md
file to mention this change
🤔 Anything to add to the objectgraph.md
file about support for record structs?
.GetMethod? | ||
.GetCustomAttribute(typeof(CompilerGeneratedAttribute)) is not null); | ||
{ | ||
bool isRecord = t.GetMethod("<Clone>$") is not null && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔧 For readability I would prefer to clean-up the layout a little bit and keep an empty line between the two boolean statements
🤔 Any links to background material that we can add here for future reference?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No real background material actually, besides the link already posted in the linked issue (https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-10.0/record-structs#open-questions) where it is stated that recognizing record structs is an open point.
This pull request is based on common sense and heuristic testing, apparently giving good results but not supported by official documentation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would mention exactly using in-line comments
Pull Request Test Coverage Report for Build 3857283151Warning: This coverage report may be inaccurate.This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.
Details
💛 - Coveralls |
I guess you mean objectgraphs.md. I'm unsure about it, because it depends on how broad the meaning of "record" is in that context. If "record" is an umbrella term to mean both reference-type-based records and value-type-based record structs (either readonly or not), nothing has to be changed, otherwise we could be more explicit. For example, I was quite surprised discovering that record structs were not treated as records. |
Yes, sorry, that's what I meant. I think it wouldn't hurt to mention all of them. |
var isRecordStruct = t.BaseType == typeof(ValueType) && | ||
t.GetMethods() | ||
.Where(m => m.Name == "op_Inequality") | ||
.SelectMany(m => m.GetCustomAttributes(typeof(CompilerGeneratedAttribute))) | ||
.Any() && | ||
t.GetMethods() | ||
.Where(m => m.Name == "op_Equality") | ||
.SelectMany(m => m.GetCustomAttributes(typeof(CompilerGeneratedAttribute))) | ||
.Any(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While this is probably the best we can currently do, I wonder a bit if this is still too broad a check?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm afraid of this too. This is backed by MSDN too (see comment). I've tried my best to "crack" this test and was not able to, but please feel free to add counter-examples.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Checking for one operator should be enough.
var isRecordStruct = t.BaseType == typeof(ValueType) &&
t.GetMethod("op_Equality", BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly, null, new[] { t, t }, null)?
.GetCustomAttribute(typeof(CompilerGeneratedAttribute)) is not null;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had another look at creating a heuristic to identify record structs with few false positives.
specifies that a record struct
contains a synthesized (read has [CompilerGenerated]
)
private bool PrintMembers(System.Text.StringBuilder builder)
The name PrintMembers
is also a public part of Roslyn.
var isRecordStruct = t.BaseType == typeof(ValueType) &&
t.GetMethod("PrintMembers", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly, null, new[] { typeof(StringBuilder) }, null)?
.GetCustomAttribute(typeof(CompilerGeneratedAttribute)) is { };
What do you think about this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That may be a good catch. I'll look into it!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure I understand you here 🤔
As a record struct currently doesn't have any unspeakable parts, it's always possible to write a struct in plain C# that looks like a record struct.
E.g. this is valid C# code, SharpLab
public struct A
{
[CompilerGenerated]
public static bool operator ==(A x, A y) => true;
[CompilerGenerated]
public static bool operator !=(A x, A y) => false
}
I tried another thing.
I made a record struct Auto {}
and copied its output back into regular C# code.
The only thing I wasn't allowed to was using [IsReadOnly]
as that gave a compiler error.
error CS8335: Do not use 'System.Runtime.CompilerServices.IsReadOnlyAttribute'. This is reserved for compiler usage.
PrintMembers
is annotated with [IsReadOnly]
So what do you think about checking for this?
var isRecordStruct = t.BaseType == typeof(ValueType) &&
t.GetMethod("PrintMembers", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly, null, new[] { typeof(StringBuilder) }, null)?
.GetCustomAttribute(typeof(IsReadOnlyAttribute)) is { };
I don't know if an arbitrary source generator is allowed to use [IsReadOnly]
.
IL weaving would most likely be able to circumvent CS8335, but we aren't looking for a bullet proof solution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting! I didn't know one could annotate with [CompilerGenerated] in source code; it looks a little silly to me that you can, but it is what it is I suppose :)
In this case, I definitely like your proposal with PrintMembers and IsReadOnly more. Let me try to give a couple of final touches and I'll be back with a new commit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jnyrup I have just tried the last proposal checking for a PrintMembers annotated with IsReadOnly. IsReadOnly is not available on NET47 and NETSTANDARD2_0, so it does not compile on those target, but here the problem is just my ignorance on properly handling multi-target.
The major problem for me is that the check fails for the MyReadOnlyRecordStruct and MyRecordStructWithCustomPrintMembers tests, for which PrintMembers has not the IsReadOnly attribute.
For this reason, at the end of the day (literally, for me 😄 ) I believe the lesser evil for now is checking for compiler generated equality, knowing that we can have a false positive if someone uses the [CompilerGenerated] attribute in source code (I have added a test to document this).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aha!
I hadn't thought about the case in MyRecordStructWithCustomPrintMembers
where one implements PrintMembers
themselves such that the compiler doesn't create one.
Well found 👍
So it seems there will always be a PrintMembers
, but we don't know who wrote it.
This code handles all your test cases and the false positive.
public static bool IsRecord(this Type type)
{
return TypeIsRecordCache.GetOrAdd(type, static t => t.IsRecordClass() || t.IsRecordStruct());
}
private static bool IsRecordClass(this Type type)
{
return type.GetMethod("<Clone>$", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) is { } &&
type.GetProperty("EqualityContract", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)?
.GetMethod?.IsDecoratedWith<CompilerGeneratedAttribute>() == true;
}
private static bool IsRecordStruct(this Type type)
{
// As noted here: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-10.0/record-structs#open-questions
// recognizing record structs from metadata is an open point. The following check is based on common sense
// and heuristic testing, apparently giving good results but not supported by official documentation.
return type.BaseType == typeof(ValueType) &&
type.GetMethod("PrintMembers", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly, null, new[] { typeof(StringBuilder) }, null) is { } &&
type.GetMethod("op_Equality", BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly, null, new[] { type, type }, null)?
.IsDecoratedWith<CompilerGeneratedAttribute>() == true;
}
I would be confident enough with this one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea: this way we rely on the two documented behaviors of record structs!
There is still a small possibility of a false positive, where one both implements a PrintMember and annotates equality with [CompilerGenerated] (I have added a test for reference), but I'd consider such code as deliberate sabotage 😄
@salvois any follow-up? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was probably waiting for me.
My concerns boils down to how many false positives the heuristic will have and if we're willing to take that risk, or we want to wait until C# eventually resolves the open point about recognizing record structs.
docs/_pages/objectgraphs.md
Outdated
``` | ||
|
||
For records, this works like this: | ||
For records, record structs and readonly record structs this works like this: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For records, record structs and readonly record structs this works like this: | |
For records and record structs this works like this: |
var isRecordStruct = t.BaseType == typeof(ValueType) && | ||
t.GetMethods() | ||
.Where(m => m.Name == "op_Inequality") | ||
.SelectMany(m => m.GetCustomAttributes(typeof(CompilerGeneratedAttribute))) | ||
.Any() && | ||
t.GetMethods() | ||
.Where(m => m.Name == "op_Equality") | ||
.SelectMany(m => m.GetCustomAttributes(typeof(CompilerGeneratedAttribute))) | ||
.Any(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Checking for one operator should be enough.
var isRecordStruct = t.BaseType == typeof(ValueType) &&
t.GetMethod("op_Equality", BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly, null, new[] { t, t }, null)?
.GetCustomAttribute(typeof(CompilerGeneratedAttribute)) is not null;
I think it's fair for people to expect FA to properly handle record structs the way we treat other records. So although we're not fully confident we can cover all edge cases, I think it's worth the risk. Whatever comes after that, we'll fix. |
Hi @dennisdoomen , @jnyrup , I hope the new year finds you well. |
@salvois any progress? |
Hi @dennisdoomen , I should be able to find some spare time today. Thanks |
Hah, the spell check target fails on |
:D @salvois wrapping structs into `` But cool to prove it works 🎉 |
Co-authored-by: IT-VBFK <[email protected]>
Try to finish #2093 today.. stay tuned 🙃 |
Co-authored-by: IT-VBFK <[email protected]>
Thanks @dennisdoomen , @IT-VBFK and @jnyrup , I really enjoyed the constructive exchange and I also got the opportunity to learn something new! |
You should never stop to improve yourself :) |
Fixed #1808
There appears to be no objective way to detect whether a type is indeed a record struct. This pull requests tries to make a best guess by looking for a value type having at least a pair of compiler-generated equality and inequality operators.