使用C#泛型时理解协变和逆变的问题

116
我不明白为什么下面的C#代码无法编译。
如您所见,我有一个静态泛型方法Something,它有一个IEnumerable参数(其中T被限制为IA接口),并且这个参数无法隐式转换为IEnumerable。
这是为什么呢?(我不是在寻找解决方法,只是想理解为什么它不起作用)。
public interface IA { }
public interface IB : IA { }
public class CIA : IA { }
public class CIAD : CIA { }
public class CIB : IB { }
public class CIBD : CIB { }

public static class Test
{
    public static IList<T> Something<T>(IEnumerable<T> foo) where T : IA
    {
        var bar = foo.ToList();

        // All those calls are legal
        Something2(new List<IA>());
        Something2(new List<IB>());
        Something2(new List<CIA>());
        Something2(new List<CIAD>());
        Something2(new List<CIB>());
        Something2(new List<CIBD>());
        Something2(bar.Cast<IA>());

        // This call is illegal
        Something2(bar);

        return bar;
    }

    private static void Something2(IEnumerable<IA> foo)
    {
    }
}

Something2(bar) 行中出现的错误:

参数 1: 无法将类型为 'System.Collections.Generic.List' 的对象转换为类型 'System.Collections.Generic.IEnumerable'


8
可能是 为什么协变和逆变不支持值类型 的重复问题。 - Dirk
13
您没有将T限制为引用类型。如果您使用条件where T:class,IA,那么它应该可以工作。链接的答案有更多细节。 - Dirk
2
@Dirk 我认为这不应该被标记为重复。虽然确实存在一个协变/逆变问题,但这里的具体情况是“这个错误消息是什么意思”,以及作者没有意识到仅仅包含“class”就可以解决他的问题。我相信未来的用户会搜索这个错误消息,找到这篇文章,并感到满意(就像我经常做的那样)。 - Reginald Blue
您也可以通过直接说Something2(foo);来重现这种情况。不需要绕过.ToList()来获取List<T>T是由泛型方法声明的类型参数)以理解这一点(List<T>IEnumerable<T>)。 - Jeppe Stig Nielsen
@ReginaldBlue 百分之百正确,我也想发同样的回答。类似的答案并不意味着重复的问题。 - StayOnTarget
2个回答

219

错误信息缺乏足够的信息,这是我的错。对此感到抱歉。

你遇到的问题是由于协变仅适用于引用类型而导致的后果。

你可能会说,“但是IA是一个引用类型”。是的,它是。但你没有说T“等于”IA。你说T是实现IA的一种类型,并且值类型可以实现接口。因此我们不知道协变是否有效,所以我们禁止使用。

如果你想让协变起作用,你必须告诉编译器类型参数是具有class约束和IA接口约束的引用类型。

错误信息真的应该说明不可能进行转换,因为协变需要保证是引用类型,这是根本性的问题。


3
你为什么说这是你的错? - user4951
78
因为我实现了所有的转换检查逻辑,包括错误消息。 - Eric Lippert
@BurnsBA 这只是“错误”在因果上的意义上 -- 从技术上讲,实现和错误信息都是完全正确的。(只是无法转换的错误陈述可能需要详细说明实际原因。但生成具有泛型的良好错误信息很困难 -- 相比几年前的C++模板错误信息,这个错误信息也算是清晰简洁了。) - Peter - Reinstate Monica
3
@PeterA.Schneider: 我很感激。但我设计Roslyn中的错误报告逻辑时,我的一个主要目标是特别捕获不仅违反了哪个规则,而且在可能的情况下进一步识别“根本原因”。例如,对于customers.Select(c=>c.FristName),错误消息应该是什么?C#规范非常清楚,这是一个“重载解析”错误:可以接受该lambda的Select命名方法集为空。但是根本原因是FirstName拼写错误。 - Eric Lippert
3
我花了很多功夫确保涉及泛型类型推断和lambda表达式的场景能够使用适当的启发式方法来推断出最有助于开发人员的错误信息。但是在转换错误消息方面,尤其是涉及到方差的地方,我的工作做得不是很好。我一直为此感到遗憾。 - Eric Lippert

26

我想为Eric出色的内部答案补充一个代码示例,以便那些可能不太熟悉泛型约束的人使用。

Something的签名更改为: 约束必须首先出现

public static IList<T> Something<T>(IEnumerable<T> foo) where T : class, IA

2
我很好奇...排序背后的重要性到底是什么原因? - Tom Wright
6
@TomWright - 当然,规范并没有回答很多“为什么?”的问题,但在这种情况下,它确实明确指出有三种不同类型的约束条件,并且当使用全部三种条件时,它们必须特别地按照“primary_constraint ',' secondary_constraints ',' constructor_constraint”的顺序使用。 - Damien_The_Unbeliever
2
@TomWright:Damien是正确的;我不知道除了解析器作者的方便之外还有什么特别的原因。如果由我决定,类型约束的语法将会更加冗长。class很糟糕,因为它意味着“引用类型”,而不是“类”。我更喜欢像where T is not struct这样冗长的写法。 - Eric Lippert

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接