泛型接口、泛型委托的协变(out)与逆变(in)

部分借鉴自:https://blog.csdn.net/sudazf/article/details/17148971

首先回顾一下已经学过的内容,我们可以将派生类对象的实例赋值给基类的变量,这叫做赋值兼容,例如以下代码。

    class Animal
    {
        public int NumberOfLegs = 4;
    }
    class Dog:Animal
    {
    }
    public class Program
    {
        static void Main(string[] args)
        {
            Animal a = new Dog();
        }
    }

创建泛型类型的实例时,编译器会接收泛型类型声明以及类型参数来创建构造类型,通常会犯的一个错误就是将派生类型分配给基类型的变量,在非泛型的情况下,这是可以的,但是在泛型的情况下,类似的赋值会发生错误

    delegate T Factory<T>();   //作为返回值 如果想让编译不报错则需要对<T>加out修饰符
    class Animal { public int Legs = 4; } 
    class Dog : Animal
    {
        static public Dog MakeDog() { return new Dog(); }
    }  

    public class Program
    {
        static void Main(string[] args)
        {
            Factory<Dog> dogMaker = Dog.MakeDog;  //创建委托对象
            Factory<Animal> animalMaker = dogMaker;  //尝试赋值委托对象 此时编译报错
        }
    }

如代码所示,尽管Dog是Animal的派生类,但是委托Factory<Dog>没有从委托Factory<Animal>派生俩个委托对象是同级的,它们都从delegate类型派生,俩者没有相互之间的派生关系,因此赋值兼容性不适用。

协变与抗变只支持引用类型,只能用在泛型委托泛型接口

out、in关键字只能用于修饰泛型接口和泛型委托,不能用于类、方法,且泛型中的类型参数T必须是引用类型,而不能是值类型

协变(out泛型修饰符)

概念:协变(covariant),协变使你使用的类型可以比泛型参数指定的类型派生程度更大,例如:string->object,如果类型参数只是用于函数的返回值的时候,类型参数使用out修饰可以产生协变,out代表着返回,只能用作返回值,协变关系允许程度更大的派生类型处于返回位置。

逆变(in泛型修饰符)

概念:逆变(contravariant),逆变使你使用的类型可以比泛型参数指定的类型派生程度更小,例如:object->string,如果类型参数只能用于方法参数,那么可以使用in修饰类型参数产生逆变。in代表输入,代表着只能被使用,不能作为返回值。

泛型接口中的协变与抗变

    public interface ICovariant<out T>{}    //协变
    //public interface ICovariant<in T> {}  //逆变

    public class Sharp : ICovariant<Sharp>{}

    public class Rectange : Sharp, ICovariant<Rectange>{}

    public class Program
    {
        static void Main(string[] args)
        {
            Rectange rect = new Rectange();
            Sharp sharp = new Sharp();
            sharp = rect;

            ICovariant<Sharp> isharp = new Sharp();
            ICovariant<Rectange> irect = new Rectange();
            isharp = irect;    //如果不用out修饰T的话则会报错
            //irect = isharp;   //如果不用in修饰T的话则会报错
        }
    }

如果一个泛型接口IFoo<T>,IFoo<TSub>可以转换为IFoo<TParent>的话,我们称这个过程为协变,而且说“这个泛型接口支持对T的协变”。
如果一个泛型接口IFoo<T>,IFoo<TParent>可以转换为IFoo<TSub>的话,我们称这个过程为逆变,而且说“这个泛型接口支持对T的逆变”。
泛型接口并不单单只有一个参数,所以我们不能简单地说一个接口支持协变还是逆变,只能说一个接口对某个具体的类型参数支持协变或逆变,如ICovariant<out T1,in T2>说明该接口对类型参数T1支持协变对T2支持逆变,举个例子就是:ICovariant<Rectange,Sharp>能够转化成ICovariant<Sharp,Rectange>,这里既有协变也有抗变。

以上都是接口并没有属性或方法的情形,接下来给接口添加一些方法:

    //这时候,无论如何修饰T,都不能编译通过
    public interface ICovariant<out T>
    {
        T Method1();
        void Method2(T param);
    }

这是无论用out还是in修饰T参数,都编译不通过,原因是我们把仅有的一个类型参数T既用作函数的返回值类型,又用作参数类型。
(1)所以当用out参数修饰时,即允许接口对类型参数T协变,也就是满足ICovariant<Rectange>到ICovariant<Sharp>转换,Method1返回值Rectange,到Sharp转换没有任何问题。当isharp被irect赋值过后,此时isharp为Rectange类型,所以调用isharp.Method1()时,Method1函数返回的类型为Rectange,因为是协变所以它可以正常转换为Sharp类型。

            ICovariant<Sharp> isharp = new Sharp();
            ICovariant<Rectange> irect = new Rectange();
 
            isharp = irect;
            Sharp sharp = isharp.Method1();

(2)但是对于调用Method2函数时,此时该函数因为是被Rectange类型的接口引用isharp来调用,所以此时Method2函数的形参为Rectange,但是调用时的实参为Sharp类型而Sharp类型并不能安全转化为Rectange类型,所以会报错。

            ICovariant<Sharp> isharp = new Sharp();
            ICovariant<Rectange> irect = new Rectange();
 
            isharp = irect;
            isharp.Method2(new Sharp());

(3)同样,当用in修饰符时,即允许接口对类型参数T逆变,也就是满足从ICovariant<Sharp>到ICovariant<Rectange>转换,此时irect被赋值为Sharp类型,当调用irect.Method2时,该函数的形参为Sharp类型,而此时实参为Rectange类型,为Sharp类型的子类,所以理所当然的可以正常的转换。

            ICovariant<Sharp> isharp = new Sharp();
            ICovariant<Rectange> irect = new Rectange();
 
            //isharp = irect;
            irect = isharp;
            irect.Method2(new Rectange());

(4)当调用Method1时,因为是irect来调用,所以它会返回Sharp类型,而这里将要用这个返回值赋值给Rectange类型的变量,这是不安全的,所以会出错。

            ICovariant<Sharp> isharp = new Sharp();
            ICovariant<Rectange> irect = new Rectange();
 
            //isharp = irect;
            irect = isharp;
            Rectange rect = irect.Method1();

所以在没有额外机制的限制下,接口进行协变或逆变都是类型不安全的,NET4.0有了改进,它允许在类型参数的声明时增加一个额外的描述,以确定这个类型参数的使用范围,这个额外的描述就是in、out修饰符,它们的用法为,如果一个了类型参数仅仅能用于函数的返回值,那么这个类型参数就对协变相容,用out修饰。而相仿,一个类型参数如果仅能用于方法参数,那么这个类型参数就对抗变相容,用in修饰

所以需要将上面的接口拆成俩个接口即可:

    public interface ICovariant<out T>
    {
        T Method1();
 
    }
 
    public interface IContravariant<in T>
    {
        void Method2(T param);
    }

总结要点:

(1)仅有泛型接口和泛型委托支持对类型参数的可变性,泛型类或泛型方法时不支持的
(2)值类型不参与协变或逆变,IFoo<int>永远无法协变成IFoo<object>,不管有无声明out。因为.NET泛型,每个值类型会生成专属的封闭构造类型,与引用类型版本不兼容。
(3)声明属性时要注意,可读写的属性会将类型同时用于参数和返回值,因此只有只读属性才允许使用out类型参数,只写属性能够使用in参数。

协变-逆变互换原则(out修饰类型作类型参数)

接下来将接口代码改成这样,同样是可以编译通过的。

    public interface ICovariant<out T>
    {
        T Method1();
        void Method3(IContravariant<T> param);
    }
 
    public interface IContravariant<in T>
    {
        void Method2(T param);
    }

如代码所示,根据之前的分析,第二个为逆变接口,它可以将Sharp类型转换为Rectange类型,而对于第一个协变接口,参数T可以直接作为返回值,但是作为方法参数则满足不了,原因是不能将Sharp类型转换为Rectange类型,所以如果要满足这个函数的定义,对Method3的参数要求是Sharp能够逆变成Rectange,正好使用第二个接口作为参数即可满足,也就是说,如果一个接口需要对类型参数T协变,那么这个接口所有方法参数类型必须支持对类型参数T的逆变(如果T有作为某些方法的参数类型)

所以我们并不能简单地说out参数只能用于方法返回类型参数,它确实只能直接用于声明返回值类型,但是只要一个支持逆变的类型协助,out类型参数就也可以用于参数类型。换句话说,in除了直接声明方法类型支持逆变外,也可以借助支持协变的类型用于方法参数,仅支持对T逆变的类型作为方法参数类型是不允许的。

协变-逆变一致原则(in修饰类型作返回值)

既然方法类型参数协变和逆变有互换影响,那么方法的返回值类型会不会有同样的问题呢?如下代码

    public interface IContravariant<in T>
    {
 
    }
    public interface ICovariant<out T>
    {
 
    }
 
    public interface ITest<out T1, in T2>
    {
        ICovariant<T1> test1();
        IContravariant<T2> test2();
    }

和之前的刚好相反,如果一个接口需要对类型参数T进行协变或逆变,那么这个接口的所有方法的返回值类型必须支持对T同样方向的协变或逆变(如果有某些方法的返回值时T类型)。这就是方法返回值的协变-抗变一致原则,也就是说,即使in参数也可以用于方法的返回值类型,只要借助一个额可以抗变的类型作为桥梁即可。

泛型委托中的协变与逆变

泛型委托的协变逆变与泛型接口协变逆变类型,沿用Sharp、Rectange类作为示例,新建一个简单的泛型接口

        public delegate void MyDelegate1<T>();

测试代码:

            MyDelegate1<Sharp> sharp1 = new MyDelegate1<Sharp>(MethodForParent1);
            MyDelegate1<Rectange> rect1 = new MyDelegate1<Rectange>(MethodForChild1);
            sharp1 = rect1;

其中俩个方法为:

        public static void MethodForParent1() 
        {
            Console.WriteLine("Test1");
        }
        public static void MethodForChild1()
        {
            Console.WriteLine("Test2");
        }

编译并不能通过,因为无法将MyDelegate1<Rectange>隐式转化为MyDelegate1<Sharp>,接下来我将接口修改为支持对类型参数T协变,即加out修饰符:

        public delegate void MyDelegate1<out T>();

编译顺利通过,同样如果反过来,对类型参数T进行逆变:

            MyDelegate1<Sharp> sharp1 = new MyDelegate1<Sharp>(MethodForParent1);
            MyDelegate1<Rectange> rect1 = new MyDelegate1<Rectange>(MethodForChild1);
            //sharp1 = rect1;
            rect1 = sharp1;

只需要将修饰符改为in即可:

        public delegate void MyDelegate1<in T>();

考虑第二个委托:

        public delegate T MyDelegate2<out T>();

测试代码:

            MyDelegate2<Sharp> sharp2 = new MyDelegate2<Sharp>(MethodForParent2);
            MyDelegate2<Rectange> rect2 = new MyDelegate2<Rectange>(MethodForChild2);
            sharp2 = rect2;

其中两个方法为:

        public static Sharp MethodForParent2()
        {
            return new Sharp();
        }
        public static Rectange MethodForChild2()
        {
            return new Rectange();
        }

该委托对类型参数T进行协变没有任何问题,编译通过;如果我要对T进行抗变呢?是否只要将修饰符改成in就OK了?测试如下:

        public delegate T MyDelegate2<in T>();
            MyDelegate2<Sharp> sharp2 = new MyDelegate2<Sharp>(MethodForParent2);
            MyDelegate2<Rectange> rect2 = new MyDelegate2<Rectange>(MethodForChild2);
            //sharp2 = rect2;
            rect2 = sharp2;

错误如下:
变体无效: 类型参数“T”必须为对于“MyDelegate2<T>.Invoke()”有效的 协变式。“T”为 逆变。意思就是:这里的类型参数T已经被声明成抗变,如果上面的最后一句有效,那么以后rect2()执行结果返回的将是一个Sharp类型的实例,如果再出现这种代码:

            Rectange rectange = rect2();

那么这将是一个从Sharp类到Rectange类的不安全的类型转换!所以如果类型参数T抗变,并且要用于方法返回类型,那么方法的返回类型也必须支持抗变。即上面所说的方法返回类型协变-抗变一致原则那么如何对上面的返回类型进行抗变呢?很简单,只要借助一个支持抗变的泛型委托作为方法返回类型即可:

        public delegate Contra<T> MyDelegate2<in T>();
        public delegate void Contra<in T>();

具体的方法也需要对应着修改一下:

        public static Contra<Sharp> MethodForParent3()
        {
            return new Contra<Sharp>(MethodForParent1);
        }
        public static Contra<Rectange> MethodForChild3()
        {
            return new Contra<Rectange>(MethodForChild1);
        }

测试代码编译通过:

            MyDelegate2<Sharp> sharp2 = new MyDelegate2<Sharp>(MethodForParent3);
            MyDelegate2<Rectange> rect2 = new MyDelegate2<Rectange>(MethodForChild3);
            rect2 = sharp2;

接下来考虑第三个委托:

        public delegate T MyDelegate3<T>(T param);

首先,对类型参数T进行协变:

        public delegate T MyDelegate3<out T>(T param);

对应的方法及测试代码:

        public static Sharp MethodForParent4(Sharp param)
        {
            return new Sharp();
        }
        public static Rectange MethodForChild4(Rectange param)
        {
            return new Rectange();
        }
            MyDelegate3<Sharp> sharp3 = new MyDelegate3<Sharp>(MethodForParent4);
            MyDelegate3<Rectange> rect3 = new MyDelegate3<Rectange>(MethodForChild4);
            sharp3 = rect3;

和泛型接口类似,这里的委托类型参数T被同时用作方法返回类型和方法参数类型,不管修饰符改成in或out,编译都无法通过。所以如果用out修饰T,那么方法参数param的参数类型T就需借助一样东西来转换一下:一个对类型参数T能抗变的泛型委托。

        public delegate T MyDelegate3<out T>(Contra<T> param);

两个方法也需对应着修改:

        public static Sharp MethodForParent4(Contra<Sharp> param)
        {
            return new Sharp();
        }
        public static Rectange MethodForChild4(Contra<Rectange> param)
        {
            return new Rectange();
        }

这就是上面所说的方法参数的协变-抗变互换原则

推广到一般的泛型委托:

        public delegate T1 MyDelegate4<T1,T2,T3>(T2 param1,T3 param2);

可能三个参数T1,T2,T3会有各自的抗变和协变,如:

        public delegate T1 MyDelegate4<out T1,in T2,in T3>(T2 param1,T3 param2);

那么对应的T1,T2类型参数就会出问题,原因上面都已经分析过了。于是就需要修改T1对应的方法返回类型,T2对应的方法参数类型,如何修改?只要根据上面提到的:
1)方法返回类型的协变-抗变一致原则;
2)方法参数类型的协变-抗变互换原则!

对应本篇的例子,就可以修改成:

        public delegate Contra<T1> MyDelegate4<in T1, out T2, in T3>(Contra<T2> param1, T3 param2);

 

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Kotlin中的泛型是一种类型变量的机制,它允许我们在不确定具体类型的情况下编写通用代码。Kotlin中的泛型支持协变逆变,以及in和out的关键字。 首先,我们来看一个简单的例子,实现一个泛型容器类: ```kotlin class Container<T>(var item: T) { fun getItem(): T { return item } } fun main() { val container = Container<String>("Hello") println(container.getItem()) } ``` 在这个例子中,我们定义了一个名为Container的泛型类,它有一个类型参数T。我们可以创建一个Container实例,并将其实例化为一个具体类型。我们使用getItem方法来获取这个容器中的item。 接下来,我们来介绍一下协变逆变。假设我们有两个类: ```kotlin open class Animal { fun makeSound() { println("Making animal sound") } } class Cat: Animal() { fun meow() { println("Meow") } } ``` 我们可以通过一个简单的示例来说明协变逆变: ```kotlin fun main() { val animals: List<Animal> = listOf(Cat(), Animal()) makeSounds(animals) } fun makeSounds(animals: List<Animal>) { for (animal in animals) { animal.makeSound() } } ``` 在这个例子中,我们定义了一个List<Animal>类型的变量animals,它包含了一个Cat和一个Animal实例。我们将这个变量传递给了makeSounds函数,该函数接受一个List<Animal>类型的参数。 在makeSounds函数中,我们使用for循环遍历animals列表,并对其中的每个Animal实例调用makeSound方法。由于Cat是Animal的子类,因此它也可以被视为Animal类型,因此我们可以将其添加到List<Animal>类型的变量中。 这里的List<Animal>类型就是协变的,因为我们可以将它的子类(如Cat)作为参数传递给makeSounds函数。 现在我们来看一下逆变。假设我们有一个接受Animal类型的参数的函数: ```kotlin fun takeAnimal(animal: Animal) { animal.makeSound() } ``` 我们可以将这个函数传递给另一个函数,该函数期望一个Cat类型的参数。在这种情况下,我们可以使用in关键字来表示逆变: ```kotlin fun main() { val cat: Cat = Cat() takeCat(cat) } fun takeCat(cat: Cat) { takeAnimal(cat) } fun takeAnimal(animal: Animal) { animal.makeSound() } ``` 在这个例子中,我们定义了一个takeCat函数,它接受一个Cat类型的参数。我们将这个函数传递给了takeAnimal函数,该函数期望一个Animal类型的参数。由于Cat是Animal的子类,因此我们可以将Cat类型的参数传递给takeAnimal函数。这里的takeAnimal函数是逆变的,因为它可以接受其超类型(如Animal)的参数。 最后,我们来看一下out和in关键字。我们可以在定义泛型类型参数时使用这些关键字来限制泛型类型参数的使用方式。out关键字用于声明泛型类型参数是协变的,in关键字用于声明泛型类型参数是逆变的。 例如,我们可以定义一个只允许读取的泛型接口: ```kotlin interface ReadOnlyContainer<out T> { fun getItem(): T } ``` 在这个例子中,我们使用out关键字来声明泛型类型参数T是协变的。这意味着我们只能从ReadOnlyContainer接口中获取T类型的值,而不能修改它。这样做的好处是可以使我们更加安全地使用泛型类型参数。 类似地,我们可以定义一个只允许写入的泛型接口: ```kotlin interface WriteOnlyContainer<in T> { fun setItem(item: T) } ``` 在这个例子中,我们使用in关键字来声明泛型类型参数T是逆变的。这意味着我们只能向WriteOnlyContainer接口中设置T类型的值,而不能获取它。这样做的好处是可以避免意外修改泛型类型参数的值。 总结一下,Kotlin中的泛型支持协变逆变,以及in和out关键字。使用协变逆变可以使我们更加灵活地使用泛型类型参数,而使用in和out关键字可以帮助我们更加安全地使用泛型类型参数。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值