-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
Prefer by-val methods over in methods in overload resolution #23122
Conversation
@dotnet-bot test this please #Closed |
@dotnet/roslyn-compiler #Closed |
I'm wondering if we should move this to C# 7.3 and gate by language version. We don't want different people on a team seeing two different behaviors for this. #Resolved |
@jcouv every release of the compiler changes overload resolution: some more explicitly than others. So far we've avoided using the language version to toggle how overload resolution works. Mostly because it adds layers to the complexity matrix of an already incredibly hard problem. #Resolved |
{ | ||
return BetterResult.Neither; | ||
} | ||
} | ||
|
||
// If an ambiguity exists between 'by-val' and 'in' parameter, choose the 'by-val' one. | ||
// Except if it was params. Subsequent betterness analysis will always prefer the non-params one. | ||
if (!p1.IsParams && p1.RefKind == RefKind.None && p2.RefKind == RefKind.In) |
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 is confusing.
If we have
M1(params int[] arr) . . .
and
M1(in int[] arr) ...
And I call with M1( new int[] {1,2})
- i.e. first is applicable in normal (not expanded) form
I would expect the byval method to be selected, regardless if it was declared params or not. The comment seems to say the opposite.
We need some tests for this scenario - with params overload matching in normal or expanded form. (in expanded form I expect it to not be selected) #Resolved
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.
@VSadov We already have a test that tests params expansion with in: PassingInArgumentsOverloadedOnInParams
.
For the array variation, we are still reporting ambiguity (I prefer this over selecting the by-val one, as it can break the caller). Will add another test for that variation and explain in the comment. #Closed
var type1 = GetParameterType(i, m1.Result, m1.LeastOverriddenMember.GetParameters(), out refKind1); | ||
var type2 = GetParameterType(i, m2.Result, m2.LeastOverriddenMember.GetParameters(), out refKind2); | ||
var type1 = GetParameterType(i, m1.Result, m1.LeastOverriddenMember.GetParameters(), out ParameterSymbol parameter1); | ||
var type2 = GetParameterType(i, m2.Result, m2.LeastOverriddenMember.GetParameters(), out ParameterSymbol parameter2); |
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.
ParameterSymbol parameter2 [](start = 109, length = 26)
Consider using discard. #Closed
{ | ||
return BetterResult.Neither; | ||
} | ||
} | ||
|
||
// If an ambiguity exists between 'by-val' and 'in' parameter, choose the 'by-val' one, except if it was params, | ||
// for later betterness analysis will decide on whether the expanded/non-expanded form can be used, or if it is ambiguous. | ||
if (!p1.IsParams && p1.RefKind == RefKind.None && p2.RefKind == RefKind.In) |
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.
p1.IsParams [](start = 17, length = 11)
See IsValidParams helper how to check for valid params parameter. Also, why should this matter if candidate is not using expanded form? #Closed
{ | ||
return BetterResult.Neither; | ||
} | ||
} | ||
|
||
// If an ambiguity exists between 'by-val' and 'in' parameter, choose the 'by-val' one, except if it was params, |
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.
// If an ambiguity exists between 'by-val' and 'in' parameter, choose the 'by-val' one, except if it was params, [](start = 12, length = 112)
It feels suspicious that we do this check before we checked which candidate uses BetterConversionFromExpression. Even if we do that immediately after, we might still introduce a breaking change as the tie breaking rules that are executed today after this function can resolve the ambiguity differently. #Closed
Done with review pass (iteration 2). It feels like we need to figure out exact rules and where do they fit in the current overload resolution specification. Then we can review the change. #Closed |
@AlekseyTs I addressed the comments. Will follow up on the spec change in the mean time. #Closed |
// Otherwise, prefer methods with 'val' parameters over 'in' parameters. | ||
Debug.Assert(m1Original.Length == m2Original.Length, "Unequal parameters counts are handled in an earlier stage"); | ||
for (i = 0; i < m1Original.Length; i++) | ||
{ |
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.
It seems the order of paremeters matters.
I.E. we can resolve M(1, 1) when
M(in int x, int y)
M(int x, in int y)
Second wins. Is that intentional? #Resolved
int m1ModifierCount = m1.LeastOverriddenMember.CustomModifierCount(); | ||
int m2ModifierCount = m2.LeastOverriddenMember.CustomModifierCount(); | ||
if (m1ModifierCount != m2ModifierCount) | ||
{ | ||
return (m1ModifierCount < m2ModifierCount) ? BetterResult.Left : BetterResult.Right; | ||
} | ||
|
||
// Otherwise, prefer methods with 'val' parameters over 'in' parameters. | ||
Debug.Assert(m1Original.Length == m2Original.Length, "Unequal parameters counts are handled in an earlier stage"); |
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.
"Unequal parameters counts are handled in an earlier stage" [](start = 65, length = 59)
Could you please point to the place that does that? #Closed
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.
Check lines 1441-1519. They should handle these cases where we end up with mismatching parameter count because of unused optional or expanded ones. I'll add a test as well.
In reply to: 151523306 [](ancestors = 151523306)
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 see that code comparing the number of parameters used, but not the number of parameters declared.
In reply to: 151544214 [](ancestors = 151544214,151523306)
int m1ModifierCount = m1.LeastOverriddenMember.CustomModifierCount(); | ||
int m2ModifierCount = m2.LeastOverriddenMember.CustomModifierCount(); | ||
if (m1ModifierCount != m2ModifierCount) | ||
{ | ||
return (m1ModifierCount < m2ModifierCount) ? BetterResult.Left : BetterResult.Right; | ||
} | ||
|
||
// Otherwise, prefer methods with 'val' parameters over 'in' parameters. | ||
Debug.Assert(m1Original.Length == m2Original.Length, "Unequal parameters counts are handled in an earlier stage"); | ||
for (i = 0; i < m1Original.Length; i++) |
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 (i = 0; i < m1Original.Length; i++) [](start = 12, length = 39)
I think we should only look at parameters that have arguments. #Closed
for (i = 0; i < m1Original.Length; i++) | ||
{ | ||
var p1 = m1Original[i]; | ||
var p2 = m2Original[i]; |
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.
var p2 = m2Original[i]; [](start = 16, length = 23)
It looks like these parameters may correspond to different arguments. Why would we look at them in this case? #Closed
|
||
if (p1.RefKind == RefKind.None && p2.RefKind == RefKind.In) | ||
{ | ||
return BetterResult.Left; |
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.
return BetterResult.Left; [](start = 20, length = 25)
Returning too early? Please add tests for multiple arguments including cases when different arguments point to a different winners. #Closed
Done with review pass (iteration 3) #Closed |
@VSadov @AlekseyTs comments addressed. #Closed |
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.
LGTM
// Always prefer operators with val parameters over operators with in parameters: | ||
BetterResult valOverInPreference; | ||
|
||
if (op1.LeftRefKind == RefKind.None && op2.LeftRefKind == RefKind.In) |
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.
if (op1.LeftRefKind == RefKind.None && op2.LeftRefKind == RefKind.In) [](start = 12, length = 69)
It feels like the logic here could be simplified if we add a helper that calculates the best among two ref kinds. Then we call it for both sides and then reason about final answer.
Something like this:
BetterResult fromLeft = better(op1.LeftRefKind, op2.LeftRefKind);
BetterResult fromRight = better(op1.RightRefKind, op2.RightRefKind);
if (fromLeft == BetterResult.Neither || fromLeft == fromRight)
return fromRight;
if (fromRight == BetterResult.Neither)
return fromLeft;
return BetterResult.Neither;
``` #Resolved
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.
To me, the existing implementation is easier to read/trace. However, I don't feel strongly about this. I can implement your suggestions if you think it is the better alternative.
In reply to: 155026276 [](ancestors = 155026276)
Done with review pass (iteration 5). #Closed |
@AlekseyTs comments addressed. #Closed |
I meant "Equals" implementation, but, given how LeftRefKind/RightRefKind are implemented, the current implementation should be fine. In reply to: 349467602 [](ancestors = 349467602,349380594) Refers to: src/Compilers/CSharp/Portable/Binder/Semantics/Operators/BinaryOperatorSignature.cs:42 in c06fca6. [](commit_id = c06fca6, deletion_comment = False) |
{ | ||
get | ||
{ | ||
if (Method != 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.
Method [](start = 20, length = 6)
Please add cast to object. #Resolved
{ | ||
get | ||
{ | ||
if (Method != 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.
Method [](start = 20, length = 6)
Please add cast to object. #Resolved
{ | ||
get | ||
{ | ||
if (Method != 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.
Method [](start = 20, length = 6)
Please add cast to object. #Resolved
{ | ||
Debug.Assert(Method.ParameterRefKinds.Length == 1); | ||
|
||
return Method.ParameterRefKinds.Single(); |
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.
.Single() [](start = 55, length = 9)
Is Single()
better than just [0]
? #Resolved
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.
It will throw a more descriptive exception if something went wrong.
In reply to: 155394341 [](ancestors = 155394341)
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.
LGTM (iteration 6)
@MeiChin-Tsai for ask mode approval. |
Ping for @MeiChin-Tsai for ask-mode approval (15.6). Thanks |
Approved. |
@dotnet-bot test windows_release_unit32_prtest |
@dotnet-bot test this please |
* dotnet/master: (39 commits) Prefer by-val methods over in methods in overload resolution (dotnet#23122) Update build to account for additional Mono.Cecil assemblies Fix build break Enable multicore JIT in the compilers (dotnet#23173) Separate debugging workspace service and EnC service (dotnet#23630) UseInferredMemberName: use one code style option and share more code (dotnet#23506) Add negative test cases Remove unnecessary Mono.Cecil reference Updated formatting of decompiler license notice Use sentence case for the decompiler legal notice Use ConcurrentDictionary to avoid locks in IsGeneratedCode and HasHiddenRegions Recognize condition with logical negation Refactor so that GetSymbolInfo can be called last Change the code fix to find the expression so that we don't need to unwrap the argument Add check for null argument syntax Fix argument names Formatting adjustments Add VB tests for omitted arguments Add WorkItem attributes Pool the Stopwatch instance used in the analyzer driver ...
Customer scenario
Given two methods with parameters that only differ by ref kind:
The call
M(int)
would result in (C# 7.2, shipped with VS 15.5):error CS0121: The call is ambiguous between the following methods or properties: 'C.M(int)' and 'C.M(in int)'
This PR proposes to change that behavior, and always prefer value methods over in methods. This is not a breaking change, because it should only affect cases where the compiler produced an error, to turn it into valid invocations.
[Note from jcouv:] This change would ship in 15.6 preview 2 as a bug fix for C# 7.2 (without introducing a new language version).
Bugs this fixes
Contributes to dotnet/csharplang#945 (comment)
Workarounds, if any
None needed
Risk
Low. This should be a new tie-breaker rule that is introduced to C# overload resolution.
Performance impact
None.
Is this a regression from a previous update?
No. This is a new feature.
Test documentation updated?
No.