Methods in C# have special checks if tuples appear in their signatures. Sometimes you can see confusing compile errors due to these checks, for example, in interface implementation. Unfortunately there is no such a “formal document” that defines these behaviors. So I note them down here, which may serve as a side note until the specification follows up (or not :D).
I found this problem when writing a piece of code. An example is demonstrated below:
|
The code compiles. That’s expected. But then I began to think, that the tuple ((int First, int Second)
) is such a burden, because it spreads everywhere. Several classes have the similar definitions and usages, and those tuples all have the same semantics. According to the C# syntax, tuples equivalence are compared structurally. Element names are just a syntax sugar backed by attributes. So, it should be okay to erase the names in the generics of interface, but keep them in the returned value to provide their semantics to the invoker. In other words, the code should be like this:
|
Guess what? I got a compile error. But what if I use the returned tuple without explicitly mentioning their element names?
|
When SomeEnumerable
is unchanged, the code above can be compiled without any error.
Strange, isn’t it? It seems sometimes the tuples are structually equivalent, and sometimes they are not. Does it mean I have to keep all the frustrating (int First, int Second)
pieces? Is it a negligence in language design, or some other problem?
Therefore I searched the specification and other documents. Unfortunately, by now the specification is only updated to language version 5.0. Microsoft Docs only has the full reference until the draft of version 6.0 (and it is not even a specification). Tuples were introduced in C# 7.0 and there is only an incremental language reference (only mentioned the lowest language version but not in an explicit branch). Well, C# 9.0 will be out soon. Unfortunately, there is no documentation of the compiler behaviors I presented above.
Then, according to the error code (CS8141
) I was able to find dotnet/orleans#3421. One of its discussion mentioned dotnet/roslyn#20838, a pull request (PR) that relaxed the matching rules for tuple element names.
In C# 6.0, the types System.ValueTuple<...>
are supported but the compilation will fail if the referened assembly is authored in C# 7.0 and custom tuple element names are used (dotnet/roslyn#20528). dotnet/roslyn#20838 fixed that problem by allowing names mismatch under certain situations. That is to say, the code below (taken from dotnet/orleans#3421) could not compile before, but can compile after that PR was merged:
|
However, this doesn’t solve my problem. My case is different from above and it can’t compile. According to dotnet/orleans#3421, this problem was fixed in .NET Core 2.1, but I still can’t compile on .NET Framework 4.7.2 and .NET Core 3.1 SDK. Is it a software regression?
I couldn’t find other PRs related to fixing tuple behaviors, so I read dotnet/roslyn#20838 carefully. The thing that interests me is the testing code added in this PR. Its snapshot (of the full file) in July 2020 is here.
After reading the tests I think I understand why it is like that: it is an error-tolerant design. All tuples (the ones that the syntax sugar use) are System.ValueTuple<...>
types, and they can be assigned whenever implicit casts succeed. Moreover, you can use whatever name you like for the tuple elements (after C# 7.0). And those, may break code contracts and introduce subtle but deadly errors. Consider the code illustrated below:
|
Calling GetContactInfo()
via SomeClass
or SomeInterface
will give you confusing results. So the best solution is prohibiting the change in element names.
Let’s head back to the problem at starting point. Unfortunately, if I want to preserve the semantics in code, I have to keep (int First, int Second)
everywhere. I can’t omit some of them and write (int, int)
instead. So be it.
In the unit tests of CodeGenTupleTest.cs
there are several error codes. After reading the invesigation process above, you should have understood why they are designed like that. However, the error messages are still unclear and confusing. They would be better with hints, like what GCC or Clang does. So here I will explain them with some examples. For each error there may be more than one way to be triggerred, and the examples below are the most simple ones.
CS8139
: Checked when overriding a method whose signature contains tuples. Thrown when element names in base and child classes differ.
|
CS8140
: Checked when one of a interface I
‘s generic parameter T
is instantiated with a tuple. Thrown when the I
‘s implementation implements multiple I
s but T
s are the same tuple with different element names.
|
CS8141
: Checked when one of a interface I
‘s generic parameters T
is instantiated with a tuple. Thrown when T
s have different element names.
|
CS8142
: Checked when defining a partial method whose signature contains tuples. Thrown when element names differ in declaration and definition.
|