-
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
Incorrect inferred tuple names with null coalescing operator #16825
Comments
It's a little concerning that C# would allow you to so easily convert between named tuples in either case. The equivalent Swift code would fail to compile. |
@gafter @jaredpar Confirmed this behavior. The logic for inferring type in FYI @VSadov @AlekseyTs |
From discussion with @VSadov this is by design. I'll confirm, document and add tests. |
This is ByDesign. Operands of |
Example: static void Main(string[] args)
{
(long One, object Two)? left = null;
// There is an implicit conversion (user defined + standard) like:
// C1 ===> (int Alice, int Bob) ===> (long One, object Two)
//
// the following is valid
// resutl has type (long One, object Two)
var result = left ?? new C1();
}
class C1
{
public static implicit operator (int Alice, int Bob) (C1 x)
{
return (1, 1);
}
} |
@HaloFour Not sure about Swift. Interesting fact. C# tuple types convert as long as corresponding element types convert. Here: string has identity conversion to string, therefore |
I understand that the names are optional and erased, but it worries me that they are as treated so inconsequentially by the C# compiler. I foresee lots of missed opportunities in preventing logic errors. In Swift, Furthermore, Swift allows implicit convertion from using System;
public class C {
public (String name, String email) GetPerson() {
return ("Bill", "bill@foo.com");
}
public void M() {
(string address, string name) t2 = GetPerson();
string s = t2.address; // so much fail
Console.WriteLine(s); // prints "Bill"
}
} |
A poor consolation prize. Compiler warning waves seem better suited to helping identify legal code that can be used incorrectly in unexpected ways after a feature ships, not to cover poor initial design. Note that tuples don't even align behaviorally with named arguments, from which their syntax is derived. At least there compiler is smart enough there to know when to transpose values. That will only add to the confusion. void M() {
void L(string name, string email) {
Console.WriteLine(email);
}
L(email: "foo", name: "bar"); // prints bar
(string name, string email) tuple = (email: "foo", name: "bar");
Console.WriteLine(tuple.email); // prints foo, so much fail
} Sorry to be a bit harsh about this, but I've been harping on it since CodePlex. |
There is nothing semantically wrong with assigning Compiler can give warnings on "Suspicious" use of names, but it is not required by the language specification. |
@HaloFour I'm sympathetic to the desire for safety but I also really do think a warning wave is actually ideal. |
You're technically correct, but it's still very likely a logic error and a perfect opportunity for the C# compiler to step in and nudge the developer to be more explicit regarding their intent. In that sense I don't see how it's any different than how the C# compiler forbids implicit switch case fall-through.
Considering that the language specification is being amended in tandem with the language I don't see how that's relevant. The specification could just as easily be written to make the names more strictly enforced.
I've just demonstrated that this is not the case. Named arguments in an argument list and named tuples behave very differently despite having identical syntax. The former will silently transpose the values, the latter will not. What a mess that will be if splatting of tuples into arguments is eventually considered. Beyond that, I largely agree. I think tuples should be treated as a loose association of values, and the compiler should treat conversion rules and whatnot for each of the elements individually. But if the developer is going to go to the trouble of naming the elements then the compiler should go to the trouble of trying to ensure that the developer isn't making a simple mistake. And by adding it after the fact via a warning wave means that an entire generation of projects will be created that will never have that protection as doing so means upgrading the language version and opting into stricter warnings for that project. |
Hmm, that hadn't quite sunk in but you've won me over. To the extent that they differ, tuples should be more like an argument list than a list of variables. |
@HaloFour tuple literals have syntactical similarities with named arguments and there are some warnings when names are lost to conversions. These cases are more obvious and fixes are easy. When expressions have types that involve tuples, the similarity ends since values can be passed around, can be cast to base types and back and so on..., without using element names at all. List<(int Alice, int Bob)>[] x = ...;
List<(int Bob, int Alice)>[] y = ...;
List<(int, int)>[] t;
// the following is allowed
y = t = x;
// the following works too
y = (List<(int Bob, int Alice)>[])(object)x;
// but the following does not? or does it transpose something?
y = x; We may have better analysis and more warnings in #14217 for obvious mistakes related to the tuple names. The structural equivalence is, however, a major design choice and thus |
Tuple Literal Syntax is analogous to Parameter Arguments. |
The initial design included warnings for permuted names in tuple conversions. See #14217 (comment) and the LDM meeting notes it links to. We simply did not get started on the implementation of those warnings in time to include them in C# 7. Nevertheless, we believe our customers are better served getting the tuple feature in C# 7 with this aspect deferred rather than deferring the entire tuple feature until C# 8. |
I've opened #16862 regarding that specific difference.
Yes, the developer is very explicitly working to circumvent the protections.
Personally I'd think that should be an error since there is no simple transposition. It's interesting in this case that Swift silently allows the conversion: var d1 = [String : (name : String, email : String)]()
d1["foo"] = (name: "x", email: "y")
let d2 : [String : (email : String, name : String)] = s1;
d2["foo"]?.email // prints "x"
Except when they're not. Named parameter arguments transpose. Understood. But it scares me how many people might be adversely affected by accidental transmutations and will never see those warnings. And if C# 7.0 ships without transposition then that will be permanently off of the table. |
@MadsTorgersen Can you please confirm if the current behavior is as you would expect (i.e. by design)? |
The design philosophy behind this aspect of tuples was/is as follows:
Unfortunately we missed 3.ii in the 7.0 timeframe. We would like to add it in as part of a warning wave. You may disagree with this design, but we found the alternative, where element names are semantically part of the tuple type, to be unnecessarily restrictive, and the cause of lots of needless "translation" code just to rename things. Agreed that the difference in semantics with named arguments is confusing. 3.ii was intended to cleanly address any mistaken code arising from that, and it is therefore a bit unfortunate that we didn't get it in. |
@MadsTorgersen Can you please confirm if the current behavior described in this issue - that is, the difference in behavior between |
This is intentional. The spec for the null-coalescing operator is here. There is no connection between the null-coalescing and the conditional operators: one is not described in terms of the other, and there is no expectation that they are interchangeable. Specifically, the type of a conditional expression (spec here) is determined "symmetrically" between the two branches, whereas the null-coalescing operator has a strong bias for the type of the first operand. While the spec for C# 7.0 isn't finished yet, the natural extension of these rules leads to what you are seeing in the original example above. C# tries to find the "best common type" in the conditional expression, so where they don't agree on tuple names, it just elides those names. A null-coalescing expression, on the other hand, will try to keep the type of the left-hand operand (or the underlying type, if, as here, it is a nullable type), seeing that the right hand operand implicitly converts to it. That's what you are seeing in the behavior above. Note that the example conceals a likely bug, since you probably didn't intend to be able to access the var person2 = personWithAddress ?? (name: "Jane", email: "Jane@doe.com"); you would get a squiggle saying that you are renaming some elements. This illustrates the fine balance we have to make of the warnings. When it's a literal, we can be pretty certain that you meant for the element names to match, but when you get the right operand from elsewhere, we deem it too likely that you meant to rename the element, and it is too risky to give the warning. Also, it would not be as easy for the user to address the warning in that case. |
In the following code sample variables 'person' and 'person2' should have same set of elements' names. I.e. they should both have '(string name, string)?' type. However, 'person2' has '(string name, string address)?' type.
This issue makes it impossible to replace the null coalescing operator with a conditional operator as following code will become invalid:
The text was updated successfully, but these errors were encountered: