为何C#的ValueTuple支持协变?自定义值类型却无法实现?
你观察到的这个现象很有意思——这其实不是CLR层面的协变支持,而是C#编译器给ValueTuple类型开的“后门”,是专门为它实现的特殊语法糖和转换逻辑,和接口/委托的协变机制完全是两回事。
先澄清:CLR根本不支持值类型的泛型协变
你说得没错,C#的协变(和逆变)确实只限于泛型接口和委托,而且还有额外限制:比如协变的类型参数必须是引用类型(所以IEnumerable<int>也不能隐式转成IEnumerable<object>,因为int是值类型)。值类型本身(包括自定义的泛型值类型)在CLR层面是没有协变支持的,这也是你自定义的MyValueTuple无法直接转换的根本原因。
ValueTuple的“协变”是编译器的特殊处理
ValueTuple是C#团队特意优化的类型,编译器为它内置了隐式转换的自动生成逻辑:当你尝试把ValueTuple<T1, T2>赋值给ValueTuple<U1, U2>时,编译器会检查每个对应的类型参数是否存在隐式转换(比如T1可以隐式转成U1,T2可以隐式转成U2),如果满足条件,编译器会自动生成转换代码——相当于帮你完成了创建新ValueTuple实例、把每个元素转换后赋值的过程。
比如你写的代码:
ValueTuple<string, string> s = new ValueTuple<string, string>("a", "b"); ValueTuple<object, string> o = s;
编译器实际会帮你转换成类似这样的代码:
ValueTuple<string, string> s = new ValueTuple<string, string>("a", "b"); ValueTuple<object, string> o = new ValueTuple<object, string>((object)s.Item1, s.Item2);
自定义值类型需要手动实现转换
如果你想让自己的MyValueTuple也支持类似的转换,就需要手动定义用户自定义隐式转换运算符。比如给你的MyValueTuple添加这样的代码:
struct MyValueTuple<T1, T2> { public T1 Item1; public T2 Item2; public MyValueTuple(T1 item1, T2 item2) { Item1 = item1; Item2 = item2; } // 定义泛型隐式转换,支持类型参数的隐式转换 public static implicit operator MyValueTuple<U1, U2>(MyValueTuple<T1, T2> source) where T1 : implicit operator U1(T1) where T2 : implicit operator U2(T2) { return new MyValueTuple<U1, U2>(source.Item1, source.Item2); } }
添加这个转换后,你的赋值代码MyValueTuple<object, string> o = s;就能正常编译了——不过要注意,这个转换的约束是C# 7.3及以后才支持的,刚好符合你使用的Framework 4.8环境。
总结一下
- 接口/委托的协变是CLR层面的特性,值类型不参与;
ValueTuple的“协变”赋值是C#编译器的专属优化,属于语法糖范畴;- 自定义值类型没有这种编译器级别的特殊照顾,需要手动实现对应的隐式转换才能实现类似效果。
内容的提问来源于stack exchange,提问作者Heinzi

