谈谈我对C#协变和逆变的理解
协变Covariance和逆变Contravariance
在 C# 中,协变和逆变能够实现数组类型、委托类型和泛型类型参数的隐式引用转换。简单点说,协变和逆变有一个基本的公式:
协变:IFoo<父类> = IFoo<子类>; 简单点说:从儿子变成老子,年龄自然长大,顺理成章,叫做协变。
逆变:IBar<子类> = IBar<父类>; 简单点说:从老子变成儿子,年龄从大到小,有点不可逆,叫做逆变。
当然你应该看不懂这个公式,没关系,这边文章将会循序渐进的给大家介绍协变和逆变的概念,以及协变和逆变是为了解决什么问题。
下面我们先列出来C#中支持协变逆变的一些常用的接口和委托
C#中的协变接口和逆变接口
1、IEnumerable<T>(T 是协变)
2、IEnumerator<T>(T 是协变)
3、IQueryable<T>(T 是协变)
4、IGrouping<TKey,TElement>(TKey 和 TElement 都是协变)
5、IReadOnlyList<T>(T 是协变)
6、IReadOnlyCollection<T>(T 是协变)
7、IComparer<T>(T 是逆变)
8、IEqualityComparer<T>(T 是逆变)
9、IComparable<T>(T 是逆变)
C#中的协变接口和逆变接口
C#中的Func委托是支持协变的委托,Action是支持逆变的委托。
父类Person和子类Student
首先我们声明一个父类Person,包含一个Name字段,表示人的姓名。然后声明一个Student子类继承自Person,Student有一个字段School,表示学生所在的学习。声明如下:
class Person
{
public string Name { get; set; }
}
class Student : Person
{
public string School { get; set; }
}
然后我们在程序中可以这么调用,我们用Person去接受一个Student,这是非常好理解的,从面向对象继承的角度来讲完全合理,因为我需要一个人Person,你给了我一个学生Student。
Person person = new Student();
现在问题来了,我想要五个人,用一个List<Person>表示,能够用List<Student>对象去赋值给List<Person>吗,答案是不可以!
List<Person> persons = new List<Student>();
当我们在C#中这么写的时候,就会编译失败,并且提示类型转换错误,如下图:
上面的编译错误告诉我们,无法将List<Student>类型转换为List<Person>。但是从现实世界的角度来讲,我们是希望这个可以编译通过的。我想要五个人List<Person>,你赋值给了我五个学生List<Student>,这完全合情合理,学生本身就是人。
但遗憾的是,C#无法将泛型子类(List<Student>)隐式转换到泛型父类(List<Person>)。
为了解决这个转换问题,微软提出了协变这个概念,对于泛型接口的参数T来说,给这个参数T加一个修饰符out,表示这个参数支持协变,也就是可以安全的从子类转换为父类。具体写法如下:、
IEnumerable<Person> persons= new List<Student>();
我们用IEnumerable<Person>去接受一个Student集合 的时候,发现编译成功了,这是因为IEnumerable的参数T有out关键字修饰,out关键字 表示参数类型T支持协变,同时限定参数T只能作为方法的返回值出现。IEnumerable的定义如下:
List<Person>不支持协变,是因为List的泛型参数T没有out修饰符。
同时大家要注意的是:协变和逆变只支持泛型接口、委托和数组。无法对一个普通的泛型类添加out关键字(协变)或者in关键字(逆变)。
接下来我们来声明一个泛型接口类ICovariant,这个接口的泛型T用out修饰。
interface ICovariant<out T>
{
T GetName();
}
紧接着我们就用下面的代码来看下:
static void Main(string[] args)
{
ICovariant<Student> covariant = default;
ICovariant<Person> covariant1 = covariant;
}
我们将一个ICovariant<Student>的变量成功赋值给ICovariant<Person>的变量,并且编译成功。但是如果ICovariant接口的参数T没有用out修饰,就会直接编译失败 ,因为只有用out修饰的参数T才 支持协变。
这就是泛型接口的协变,通过这个方案,我们可以轻松的泛型子类转换为泛型父类。
上面我给大家演示的泛型接口,接下来我们给大家演示以下支持协变的委托。我们先声明一个委托用于创建Person对象或者Student对象。
public delegate T GetObjectDelegate<out T>();
注意这个委托的泛型T使用了out关键字,表示这个委托的参数T是支持协变的。然后我们声明两个方法创建Person对象和Student对象。这两个方法的声明和委托的声明保持一致。
public static Student GetStudent()
{
return new Student();
}
public static Person GetPerson()
{
return new Person();
}
最后,我们声明一个子类委托GetObjectDelegate<Student>,然后可以成功赋值给父类委托GetObjectDelegate<Person>。这个用于委托类型的协变。
然而只要委托类型GetObjectDelegate 参数去掉out关键字,就会编译失败!
static void Main(string[] args)
{
GetObjectDelegate<Student> student = GetStudent;
GetObjectDelegate<Person> person = student;
}
协变的经典用法
static void Main(string[] args)
{
//协变委托 Func<out T> 只能作为返回值
Func<string> func = () => default;
Func<object> func1 = func;
//协变接口 IEnumerable<out T> 只能作为返回值
IEnumerable<string> strings = default;
IEnumerable<object> objects = strings;
//协变数组
string[] arr = new string[10];
object[] arr1 = arr;
}
逆变
说完协变,我们来说逆变。还是上面Person和Student的案例,我现在需要修改一个Person的姓名(Name),该怎么办呢。我先声明一个修改姓名的委托,对于泛型参数T,我们加了一个in关键字,它表示这个参数T支持逆变,限制泛型参数T只能用于方法的参数。可以将一个父类委托赋值给一个子类委托。
public delegate void SetNameDelegate<in T>(T obj);
我们现在可以将一个父类委托赋值给一个子类委托。
static void Main(string[] args)
{
SetNameDelegate<Person> setNameDelegate = SetName;
SetNameDelegate<Student> setNameDelegate1 = setNameDelegate;
}
public static void SetName(Person obj)
{
obj.Name = "person";
}
上面的代码我们将一个SetNameDelegate<Person>类型的父类委托赋值给一个SetNameDelegate<Student>的子类委托,可以正确编译并且执行。
但是,如果你去掉in关键字,就会编译失败,因为默认情况下,父类Person是不能直接转换为子类Student的。
这就是逆变在委托中的应用。那么接下来来看下逆变在接口中的应用,也是很简单的。我们声明一个可以修改名称的接口IContravariant<in T>。
interface IContravariant<in T>
{
void SetName(T obj);
}
public class Contravariant : IContravariant<Person>
{
void IContravariant<Person>.SetName(Person obj)
{
obj.Name = "person";
}
}
然后就可以缉将一个IContravariant<Person>的接口赋值给子类接口IContravariant<Student>.。代码如下:
static void Main(string[] args)
{
IContravariant<Person> contravariant = new Contravariant();
//逆变
IContravariant<Student> contravariant1 = contravariant;
Student student = new Student();
contravariant1?.SetName(student);
Console.WriteLine(student.Name);
}
上述代码是可以编译通过的,但是如果你把IContravariant接口中泛型参数T的in关键字去掉,代码就会编译失败。
以上就是逆变的介绍,简单点说,就是将一个父类型的委托或者接口转换成子类型的委托或者接口。
逆变的经典用法
static void Main(string[] args)
{
//协变委托 T只能作为参数
Action<object> action = (s) => { };
Action<string> action1 = action;
action1("");
//逆变接口 T只能作为参数
IContravariant<object> contravariant = default;
IContravariant<string> contravariant1 = default;
}
原文链接:http://cshelloworld.com/home/detail/1911355401014743040
//协变:对于一个方法或者委托的返回值来说,我默认用子类Student接收返回值,现在可不可以用父类Person接收返回值,当然可以,因为Student可以隐式转换为Person。
//我有一个Student对象默认使用Student类接收,现在我用Person类接收,这完全合情合理,因为Student可以隐式转换为Person
//Student GetValue();
//Person GetValue();
//协变为什么要限制参数T只能作为返回值?
//假设现在T可以作为输入参数来使用,我默认使用子类Student作为输入参数,现在可不可以用Person类作为输入参数代替。当然不可以,如果用Person代替,你给我传了一个Person的子类Teacher怎么办,这就是类型不安全。
//逆变:对于一个方法或者委托的参数来说,默认我使用父类Person作为输入参数,现在可不可以给我传一个子类对象Student,当然可以,因为Student可以隐式转换为Person
//void SetValue(Person p);
//void SetValue(Student p);
//逆变为什么要限制参数T只能作为输入参数?
//假设现在T可以作为返回值来使用,我默认使用父类Person作为返回值,现在可不可以用子类Student接收返回值,当然不可以,如果用Student代替,你给我返回一个Person的子类Teacher怎么办,这就是类型不安全。
2025-04-18 15:31:43【出处】:https://www.cnblogs.com/caoruipeng/p/18825965
=======================================================================================
如果,您希望更容易地发现我的新博客,不妨点击一下绿色通道的【关注我】。(●'◡'●)
因为,我的写作热情也离不开您的肯定与支持,感谢您的阅读,我是【Jack_孟】!
本文来自博客园,作者:jack_Meng,转载请注明原文链接:https://www.cnblogs.com/mq0036/p/18833982
【免责声明】本文来自源于网络,如涉及版权或侵权问题,请及时联系我们,我们将第一时间删除或更改!
浙公网安备 33010602011771号