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:
using System;
using System.Collections;
using System.Collections.Generic;
class Program
{
static void Main()
{
var e = new SomeEnumerable();
foreach (var x in e)
{
Console.WriteLine("({0}, {1})", x.First, x.Second);
}
}
}
class SomeEnumerable : IEnumerable<(int First, int Second)>
{
public IEnumerator<(int First, int Second)> GetEnumerator()
{
throw new NotImplementedException();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
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:
// ↓ notice the tuple here
class SomeEnumerable : IEnumerable<(int, int)>
{
// ↓ and here
public IEnumerator<(int First, int Second)> GetEnumerator()
{
throw new NotImplementedException();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
Guess what? I got a compile error. But what if I use the returned tuple without explicitly mentioning their element names?
// ↓ directly deconstruct the tuple
foreach (var (f0, f1) in e)
{
Console.WriteLine("({0}, {1})", f0, f1);
}
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:
using System.Threading.Tasks;
public interface ISomeGrain
{
Task<(bool someBool, string someString)> Func();
}
class Impl : ISomeGrain
{
public Task<(bool, string)> Func()
{
return Task.FromResult((true, ""));
}
}
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:
interface SomeInterface
{
(string Name, string Address) GetContactInfo();
}
class SomeClass : SomeInterface
{
// notice the element names of the tuple here
// and the one defined in the interface
public (string Address, string Name) GetContactInfo()
{
// ...
}
}
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.
class Base
{
public virtual (int, int) TupleAsReturn()
{
// ...
}
public virtual void TupleAsParam((int A, int B) value)
{
// ...
}
}
class Derived : Base
{
// ↓ CS8139: cannot change tuple element name when overriding method
public override (int A, int B) TupleAsReturn()
{
// ...
}
// ↓ CS8139: cannot change tuple element name when overriding method
public override void TupleAsParam((int, int B) value)
{
// ...
}
}
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.
interface IGeneric<T>
{
}
// ↓ CS0528:IGeneric<(int A, int B)> already exists in the implementation list
class Class1 : IGeneric<(int A, int B)>, IGeneric<(int A, int B)>
{
}
// ↓ CS8140:IGeneric<(int A, int B)> already exists in the implementation list, with different tuple element names
class Class2 : IGeneric<(int A, int B)>, IGeneric<(int C, int D)>
{
}
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.
interface IGeneric<T>
{
T Get();
void Set(T t);
}
class Class1 : IGeneric<(int A, int B)>
{
// ↓ CS8141: tuple element names must match with interface method
public virtual (int, int) Get()
{
// ...
}
// ↓ CS8141: tuple element names must match with interface method
public virtual void Set((int, int) tuple)
{
// ...
}
}
class Class2 : Class1, IGeneric<(int C, int D)>
{
// ↓ CS8141: tuple element names must match with interface method
public override (int C, int D) Get()
{
// ...
}
// ↓ CS8141: tuple element names must match with interface method
public override void Set((int C, int D) tuple)
{
// ...
}
}
CS8142
: Checked when defining a partial method whose signature contains tuples. Thrown when element names differ in declaration and definition.
partial class Class
{
partial void Method((int A, int B) tuple);
}
partial class Class
{
// ↓ CS8142: both parts of partial method must use the same tuple element names
partial void Method((int, int) tuple)
{
// ...
}
}