C#中的变体



变体的大概意思是:有T和U两个类型,并且T = U (此处的等号为赋值)成立,如果T和U经过某种操作之后分别得到T’和U’,并且T’ = U’也成立,则称此操作为协变;如果U’ = T’,则称此操作为逆变。



协变和逆变统称为变体,这是用于数组类型,委托类型,泛型参数类型间进行隐式引用转换用的语法规则,有点类似多态。

变体包括抗变,协变,它是为了处理泛型,委托中基类与派生类赋值问题而出现的,因此类似于多态。
关于协变我们很容易理解,它是实现派生级别高赋给派生级别低的。在泛型接口中,协变的标示是out,并且它表示的也是函数的返


回值,就根据这一点说明它就很类似。
关于抗变,接口中标示的是in,并且它表示的是传入的函数参数,所以抗变中表象是派生级别低的赋给派生级别高的,实质上还是把


用派生级别高的来做输入,输入到派生级别低的函数参数中。

泛型接口中的变体
<1>协变

接口中声明的方法的泛型返回类型,它可以接受派程度更大的返回类型

?
1
2
3
4
5
6
interface ICovariant<out r= "" >
{
     R GetSomething();
     // The following statement generates a compiler error.
     // void SetSometing(R sampleArg);
}</out>


< 2>逆变
接口中声明的方法的泛型参数类型,它可以接受派生程度更小的参数类型

?
1
2
3
4
5
6
7
8
interface IContravariant<in a= "" >
{
     void SetSomething(A sampleArg);
     void DoSomething<t>() where T : A;
     // The following statement generates a compiler error.
     // A GetSomething();           
}
</t></in>


<3> 协变和抗变的同时实现


?
1
2
3
4
5
6
7
interface IVariant<out a= "" in= "" r,= "" >
{
     R GetSomething();
     void SetSomething(A sampleArg);
     R GetSetSometings(A sampleArg);
}
</out>



有如下四个类。

复制代码
    public class Animal
    {
    }

    public class Mammal : Animal
    {
    }

    public class Dog : Mammal
    {
        public void EatBone()
        {
        }
    }

    public class Panda : Mammal
    {
        public void EatBamboo()
        {
        }
    }
复制代码

 

    Animal animal = new Dog();

这样的赋值肯定是没问题的,但这只是多态。

变体的大概意思是:有T和U两个类型,并且T = U (此处的等号为赋值)成立,如果T和U经过某种操作之后分别得到T’和U’,并且T’ = U’也成立,则称此操作为协变;如果U’ = T’,则称此操作为逆变。

复制代码
//以下代码能通过,则说明Operation是协变。
T = U; //=表示赋值
  ↓
Operation(T) = Operation(U);

//类似的,以下操作为逆变。
T = U;
  ↓
Operation2(U) = Operation2(T);
复制代码

 

一、特殊的协变——数组

我们常说协变和逆变是.net 4.0中引入的概念,但实际上并不是。其实只要符合上面定义的,都是变体。我们先来看一个.net 1.0中就包含的一个协变:

    Animal[] animalArray = new Dog[10];

这个不是多态,因为Dog[]的父类不是Animal[],而是object。
我们对照变体的定义来看一下,首先Animal = Dog,这个是成立的,因为Dog是Animal的子类;然后经过Array这个操作后,等式左右两边分别变成了Animal[]和dog[],并且这个等式仍然成立。这已经是满足协变的定义了。

可能有人会困惑,这为什么等号就成立了呢?
我们有一点要明确的是,因为C#语言规定了Array操作是协变,并且Compiler支持了,所以等式就成立了。变体都是人为定的,你甚至可以规定任何操作都是协变或者逆变,无非就是使编译和在运行期变体处的赋值通过。

我们再看一下Array的应用:

    Animal[] animalArray = new Dog[10]; //Line1
    animalArray[0] = new Bird(); //Line2

上面的代码能编译通过,Line1处也能运行通过,但是到了Line2处就会抛异常,所以说虽然Array这个操作是一个协变,但并不是安全的,在某些时候还是会出错。

至于说为什么要支持Array这样的协变,据Eric Lippert在Covariance and Contravariance in C#, Part Two: Array Covariance说,是为了兼容Java的语法,虽然他本人也不是很满意这样的设计。

 

二、委托中的变体

在.net 2.0中委托也支持了协变,不过暂时还只是支持方法的赋值。

考虑下面的代码

复制代码
    //一个入参为Dog的委托。抓住了一只Dog,应该怎么处理?
    delegate void DogCatched(Dog d);   
 
    //定义两个方法
    void OnAnimalCatched(Animal animal) {}  //处理抓到的Animal
    void OnDogCatched(Dog dog) {}  //处理抓到的Dog

    Catch catchDog = OnDogCatched; //把抓到的Dog交给处理Dog的方法
    catchDog = OnAnimalCatched;  //把抓到的Dog交给处理Animal的方法
复制代码

以上两个赋值都可以成功,其中第一个为符合委托原型的赋值。第二个则可以看做是Operate(Dog) = Operate(Animal),那这是一个逆变。

同样的,下面就是一个协变。

复制代码
    //一个返回值为Animal的委托,一个需要抓到一只Animal的任务
    delegate Animal AnimalCatching();

    //两个方法
    Animal CatchAnAnimal() { return new Animal(); } //抓到一个Animal
    Dog CatchADog() { return new Dog(); } //抓到一个Dog
    
    AnimalCatching animalCatching = CatchAnAnimal; //把抓Animal的任务交给能抓到Animal的方法
    animalCatching = CatchADog; //把抓Animal的任务交给能抓到Dog的方法
复制代码

 

至于Action<T>和Func<TResult>(.net 3.5)等泛型委托,其实也是如此,同样只局限于方法给委托实例赋值,而不支持委托实例赋值给委托实例。下面的例子编译时会报错。

    Action<Animal> aa = animal => { };
    Action<Dog> ad = aa;  //编译错误

 

三、泛型中的变体

我们常说的协变和逆变,大多数指的是.net 4.0中引入的针对泛型委托和泛型接口的变体。

泛型委托

 我们发现,到了.net 4.0,之前不能编译的这段代码通过了

    Action<Animal> aa = animal => { };
    Action<Dog> ad = aa;  //编译通过

 

其实是Action的签名变了,多了in这个关键字。

    public delegate void Action<T>(T obj); //.net 4 之前
    public delegate void Action<in T>(T obj); //.net 4

 

类似的,Func的签名也变了,多了out关键字

    public delegate TResult Func<TResult>(); //.net 4 之前
    public delegate TResult Func<out TResult>(); //.net 4

in和out就是C# 4.0中用于在泛型中显式的表示协变和逆变的关键字。in表示逆变参数,out表示协变参数。

对于泛型委托的变体这一块上,.net 4.0相对于之前的版本主要增强的就是委托实例赋值委托实例(方法赋值给委托实例是.net 2.0就支持的)。

泛型接口

在.net 4.0以前,Array是协变的(尽管它不安全),但IList<T>却不是,IEnumerable<T>也不是。而到了.net 4.0,我们终于可以这样干了:

    IEnumerable<Animal> animals = new List<Dog>();  //.net 4正确

 

不过以下的操作还是会造成编译失败:

    IList<Animal> a2 = new List<Dog>(); //错误

 

究其原因,当然还是因为IEnumerable<T>在.net 4.0中是协变的,IList<T>不是:

    public interface IEnumerable<out T> : IEnumerable
    public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable

 

那泛型接口既然有协变的,同样也有逆变的,如IComparable<T>

 

四、一些疑问

1,问:我们自定义的泛型接口和泛型委托是否可以随便加上in/out关键字,来表明它是逆变或者协变的?

答:这个当然是不可能的,编译器会校验。

一般来说,如果一个泛型接口中,所有用到T的方法,都只将其用于输入参数,则T可以是逆变参数;如果用到T的方法,都只将其用于返回值,则T可以是协变参数。

委托的输入参数可以是逆变参数;返回值可以是协变参数

2,问:既然in/out不能乱加,为什么还要加呢?完全由编译器来决定协变或者逆变的赋值不可以么?

答:这个理论上应该是可以的,不过in/out关键字就像是一个泛型委托和泛型接口定义者同使用者之间的契约,必须显式的指定使用方式,否则,程序中出现一些既不是多态,又没有标明是协变或逆变,却可以赋值成功的代码,看起来比较混乱。

3,问:是不是所有的泛型委托和接口都遵从输入参数是协变的,输出参数是逆变的这一规律呢?

答:我们定义一个泛型委托Operate<T>,它的输入参数是一个Action<T>

复制代码
    delegate void Operate<T>(Action<T> action);
    //两个Action<T>的实例
    Action<Mammal> MammalEat = mammal => Console.WriteLine("mammal eat");
    Action<Panda> PandaEat = panda => panda.EatBamboo();

    //Operate<T>的实例
    Operate<Mammal> MammalOperation = action => action(new Dog()); //Action<T>是逆变,所以这里是允许的。
复制代码

然后我们可以执行下面的操作

    //操作1
    MammalOperation(MammalEat);

如果我们想让这个泛型委托是一个变体,按照我们通常的理解,T是用作输入参数的,那肯定就是逆变,应该加上in关键字。我们不考虑编译器的提示,假设定义成这样:

    delegate void Operate<in T>(Action<T> action);

因为是逆变,所以,我们可以将Operate<Mammal>赋给Operate<Panda>

    Operate<Panda> PandaOperate = MammalOperation;

由于上面这个Operate的T已经改成了Panda,所以其对应参数Action的T也应该改为Panda,所以上面的“操作1”可以改成这样:

    //操作2
    MammalOperation(PandaEat);

最终变成了PandaOperate = (new Dog()).EatBamboo()。这是个啥?完全不合常理。

实际上,当我们给Operate<T>加上in的时候,编译器就已经告诉我们,这是不对的了。写成out就可以了,说明这是一个协变,下面的操作也是可以的:

 

    Operate<Animal> AnimalOperate = MammalOperation;

 

上面这个例子似乎说明了,也并不是所有的输入参数都是逆变的?其实这已经不完全是一个输入参数了,由于有Action<T>的影响,似乎就变成了“逆逆得协”?如果把Action<T>换成Func<T>,则Operate<T>就应该用in关键字了。是不是比较费脑?还好平时工作中很少碰到这种情况,更何况还有编译器给我们把关。

 



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值