当时第一次学到协变和逆变的时候印象不是很深,不是很能理解。
结果很快想起在刷leetcode题的时候遇到关于这个问题的情况。
问题的出现
就比如力扣第15题三数之和
他的返回值是IList<IList< int >> 代表意思类似二维数组。
我第一反应就是先声明一个List<List< int >> res 的变量来作为答案,
并理所应当的认为泛型参数是支持隐式转换的。
但并不是这样滴
你得写成 List<IList< int >> 才行
这时我不禁新生疑问,难度他真的不支持隐式转换吗?
于是开始上网查找资料,自己研究总结。
假设可以
我们首先思考如果接口泛型参数真的支持隐式转换会发生什么
首先Dog是Animal类的子类
假设35行不会报错,能正常执行
因为抽象接口泛型传入的是Animal类,
所以我们会发现36行的In方法是可以传Animal类的,但是我们的实现类的泛型是Dog类,
这就意味着具体实现类的In方法是把这个泛型T看做Dog来处理的
分析一下,这是因为抽象接口的In方法仅要求传入一个Animal类即可,
但是我们的具体实现类的In方法却是对Dog类进行处理。
假如具体实现类的In方法会说话,他会说,我要一个Dog类,你却传一个Animal类给我?
这时的In方法相当于要你子类装父类,明显是不合理,是具有类型安全风险的。
但是仔细观察的话,Out方法却可以
因为接口的Out方法输出的是父类Animal
尽管实现类Out方法输出的是Dog
但是相当于父类装子类:Animal res = new Dog(); //是合理的
豁然开朗
所以这时候协变的作用就出来了
在泛型参数T前加入out 关键字
35行的报错就解决了
使用协变(out)修饰T之后,T只能作为接口方法的返回值
所以把先前写的In方法注释了
如果理解了上面的协变的例子,那么逆变就好理解了。
我们将上面例子的泛型参数调过来,
抽象接口泛型用子类Dog,具体实现类泛型用父类Animal
跟上面协变的例子同理
但这次是In方法没问题,Out方法出现了问题。
分析一下,我们发现抽象接口的In方法要求传入一个Dog类,
但是我们的具体实现类的In方法仅仅要求Animal类。
我要的是父类,你给我子类,这很合理,父类装子类。
但是Out方法就有疑问了
分析一下,我们发现抽象接口的Out方法会输出一个Dog类,
但是我们的具体实现类的Out方法仅会输出Animal类。
我要输出的是子类,你却给我父类,这很合理吗?
此时根据多态的原理,entity可能会说:
怎么编译之前我Out方法输出的是一个Dog具体类型
但是编译完之后我Out方法输出的就是更抽象的一个Animal抽象类型了?
这并意味着这两个不能一起用,仅仅只是不能两个同时修饰同一个泛型参数T而已
可以一个用out(协变)来修饰T1,另一个用in(逆变)来修饰T2
回到开头
所以为什么IList<IList< int >> 不能装入List<List< int >>呢?
这时我们去翻看IList源码能发现,原来他并没有使用协变
因为他的泛型T既要作为方法的参数输入和输出
所以才用不了协变,IList<IList< int >> 才不装不了List<List< int >>
但值得注意的是IEnumerable是支持协变
所以IEnumerable<IEnumerable< int >> 就能装List<List< int >>了
总结
什么是协变和逆变?
协变 允许你在泛型中使用比指定类型更具体的类型。
例如IEntity < Animal > entity = new Entity< Dog >();
它适用于返回值类型,使用 out 关键字标记泛型类型参数。
逆变 允许你在泛型中使用比指定类型更抽象的类型。
例如IEntity < Dog > entity = new Entity< Animal >();
它适用于参数类型,使用 in 关键字标记泛型类型参数。
为什么需要协变和逆变?
在处理复杂的类型层次结构和泛型集合时,协变和逆变确保类型安全并提高代码的灵活性。
它能够允许我们在不牺牲类型安全的情况下实现"属于泛型的多态性"。