协方差和逆变现实世界的例子

Vin*_*cio 149 c# covariance c#-4.0

我在理解如何在现实世界中使用协方差和逆变时遇到了一些麻烦.

到目前为止,我见过的唯一例子是同样的旧数组示例.

object[] objectArray = new string[] { "string 1", "string 2" };
Run Code Online (Sandbox Code Playgroud)

很高兴看到一个允许我在开发过程中使用它的例子,如果我能看到它在其他地方使用的话.

Mar*_*tos 128

// Contravariance
interface IGobbler<in T> {
    void gobble(T t);
}

// Since a QuadrupedGobbler can gobble any four-footed
// creature, it is OK to treat it as a donkey gobbler.
IGobbler<Donkey> dg = new QuadrupedGobbler();
dg.gobble(MyDonkey());

// Covariance
interface ISpewer<out T> {
    T spew();
}

// A MouseSpewer obviously spews rodents (all mice are
// rodents), so we can treat it as a rodent spewer.
ISpewer<Rodent> rs = new MouseSpewer();
Rodent r = rs.spew();
Run Code Online (Sandbox Code Playgroud)

为了完整......

// Invariance
interface IHat<T> {
    void hide(T t);
    T pull();
}

// A RabbitHat…
IHat<Rabbit> rHat = RabbitHat();

// …cannot be treated covariantly as a mammal hat…
IHat<Mammal> mHat = rHat;      // Compiler error
// …because…
mHat.hide(new Dolphin());      // Hide a dolphin in a rabbit hat??

// It also cannot be treated contravariantly as a cottontail hat…
IHat<CottonTail> cHat = rHat;  // Compiler error
// …because…
rHat.hide(new MarshRabbit());
cHat.pull();                   // Pull a marsh rabbit out of a cottontail hat??
Run Code Online (Sandbox Code Playgroud)

  • 我喜欢这个现实的例子.我上周刚刚写了一些驴子吞噬代码,我很高兴我们现在有了协方差.:-) (132认同)
  • 上面的评论@javadba告诉THE EricLippert什么是协方差和逆变是一个现实的协变例子,我告诉我的奶奶如何吸蛋!:p (4认同)
  • 这个问题没有问逆变和协变*可以做什么*,而是问*为什么需要使用它*。您的示例远非实用,因为它不需要任何一个。我可以创建一个 QuadrupedGobbler 并将其视为自身(将其分配给 IGobbler&lt;Quadruped&gt;),并且它仍然可以吞噬驴(我可以将驴传递给需要 Quadruped 的 Gobble 方法)。不需要逆变。我们*可以*将 QuadrupedGobbler 视为 DonkeyGobbler,这很酷,但是在这种情况下,如果 QuadrupedGobbler 已经可以吞掉驴子了,为什么我们需要这样做呢? (3认同)
  • Waaay 迟到了,但这是我在 SO 周围看到的最好的书面例子。在荒谬的同时完全有道理。我将不得不用答案来提升我的游戏... (2认同)

CSh*_*per 110

这是我放在一起帮助我理解差异的原因

public interface ICovariant<out T> { }
public interface IContravariant<in T> { }

public class Covariant<T> : ICovariant<T> { }
public class Contravariant<T> : IContravariant<T> { }

public class Fruit { }
public class Apple : Fruit { }

public class TheInsAndOuts
{
    public void Covariance()
    {
        ICovariant<Fruit> fruit = new Covariant<Fruit>();
        ICovariant<Apple> apple = new Covariant<Apple>();

        Covariant(fruit);
        Covariant(apple); //apple is being upcasted to fruit, without the out keyword this will not compile
    }

    public void Contravariance()
    {
        IContravariant<Fruit> fruit = new Contravariant<Fruit>();
        IContravariant<Apple> apple = new Contravariant<Apple>();

        Contravariant(fruit); //fruit is being downcasted to apple, without the in keyword this will not compile
        Contravariant(apple);
    }

    public void Covariant(ICovariant<Fruit> fruit) { }

    public void Contravariant(IContravariant<Apple> apple) { }
}
Run Code Online (Sandbox Code Playgroud)

tldr

ICovariant<Fruit> apple = new Covariant<Apple>(); //because it's covariant
IContravariant<Apple> fruit = new Contravariant<Fruit>(); //because it's contravariant
Run Code Online (Sandbox Code Playgroud)

  • 到目前为止,这是我见过的最清楚简洁的事情.很好的例子! (9认同)
  • 当"水果"是"苹果"的父母时,如何将水果降低到苹果(在"逆变法"例子中)? (6认同)
  • 我觉得困惑更多的是“为什么”。水果被降格为苹果,并传递给某种功能。但该函数仍将使用潜在的假苹果作为原始水果,而不是苹果。我觉得矛盾只是为了方便,允许将派生程度较低的类型作为派生程度较高的类型传入,但它仍将用作原始派生程度较低的类型。 (4认同)

tva*_*son 103

假设你有一个类Person和一个派生自它的类,教师.你有一些IEnumerable<Person>作为参数的操作.在您的School类中,您有一个返回的方法IEnumerable<Teacher>.协方差允许您直接将该结果用于采用a的方法,将IEnumerable<Person>更多派生类型替换为较少派生(更通用)的类型.反直觉,反直觉,允许您使用更通用的类型,其中指定更多派生类型.

另请参阅MSDN上的泛型中的协方差和逆变.

课程:

public class Person 
{
     public string Name { get; set; }
} 

public class Teacher : Person { } 

public class MailingList
{
    public void Add(IEnumerable<out Person> people) { ... }
}

public class School
{
    public IEnumerable<Teacher> GetTeachers() { ... }
}

public class PersonNameComparer : IComparer<Person>
{
    public int Compare(Person a, Person b) 
    { 
        if (a == null) return b == null ? 0 : -1;
        return b == null ? 1 : Compare(a,b);
    }

    private int Compare(string a, string b)
    {
        if (a == null) return b == null ? 0 : -1;
        return b == null ? 1 : a.CompareTo(b);
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

var teachers = school.GetTeachers();
var mailingList = new MailingList();

// Add() is covariant, we can use a more derived type
mailingList.Add(teachers);

// the Set<T> constructor uses a contravariant interface, IComparer<T>,
// we can use a more generic type than required.
// See https://msdn.microsoft.com/en-us/library/8ehhxeaf.aspx for declaration syntax
var teacherSet = new SortedSet<Teachers>(teachers, new PersonNameComparer());
Run Code Online (Sandbox Code Playgroud)

  • @FilipBartuzi - 如果像我一样,当我写这个答案时,你就受雇于一所非常现实世界的大学. (14认同)
  • 当它没有回答问题并且没有给出在c#中使用co/contra方差的任何例子时,如何将其标记为答案? (5认同)

Jac*_*ack 56

in和out关键字使用泛型参数控制编译器的接口和委托的转换规则:

interface IInvariant<T> {
    // This interface can not be implicitly cast AT ALL
    // Used for non-readonly collections
    IList<T> GetList { get; }
    // Used when T is used as both argument *and* return type
    T Method(T argument);
}//interface

interface ICovariant<out T> {
    // This interface can be implicitly cast to LESS DERIVED (upcasting)
    // Used for readonly collections
    IEnumerable<T> GetList { get; }
    // Used when T is used as return type
    T Method();
}//interface

interface IContravariant<in T> {
    // This interface can be implicitly cast to MORE DERIVED (downcasting)
    // Usually means T is used as argument
    void Method(T argument);
}//interface

class Casting {

    IInvariant<Animal> invariantAnimal;
    ICovariant<Animal> covariantAnimal;
    IContravariant<Animal> contravariantAnimal;

    IInvariant<Fish> invariantFish;
    ICovariant<Fish> covariantFish;
    IContravariant<Fish> contravariantFish;

    public void Go() {

        // NOT ALLOWED invariants do *not* allow implicit casting:
        invariantAnimal = invariantFish; 
        invariantFish = invariantAnimal; // NOT ALLOWED

        // ALLOWED covariants *allow* implicit upcasting:
        covariantAnimal = covariantFish; 
        // NOT ALLOWED covariants do *not* allow implicit downcasting:
        covariantFish = covariantAnimal; 

        // NOT ALLOWED contravariants do *not* allow implicit upcasting:
        contravariantAnimal = contravariantFish; 
        // ALLOWED contravariants *allow* implicit downcasting
        contravariantFish = contravariantAnimal; 

    }//method

}//class

// .NET Framework Examples:
public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable { }
public interface IEnumerable<out T> : IEnumerable { }


class Delegates {

    // When T is used as both "in" (argument) and "out" (return value)
    delegate T Invariant<T>(T argument);

    // When T is used as "out" (return value) only
    delegate T Covariant<out T>();

    // When T is used as "in" (argument) only
    delegate void Contravariant<in T>(T argument);

    // Confusing
    delegate T CovariantBoth<out T>(T argument);

    // Confusing
    delegate T ContravariantBoth<in T>(T argument);

    // From .NET Framework:
    public delegate void Action<in T>(T obj);
    public delegate TResult Func<in T, out TResult>(T arg);

}//class
Run Code Online (Sandbox Code Playgroud)

  • 到目前为止,这是最明确的答案,谢谢! (4认同)

Stu*_*tLC 39

这是一个使用继承层次结构的简单示例.

给定简单的类层次结构:

public abstract class LifeForm  { }
public abstract class Animal : LifeForm { }
public class Giraffe : Animal { }
public class Zebra : Animal { }
Run Code Online (Sandbox Code Playgroud)

在代码中:

public static void PrintLifeForms(IList<LifeForm> lifeForms)
{
    foreach (var lifeForm in lifeForms)
    {
        Console.WriteLine(lifeForm.GetType().ToString());
    }
}
Run Code Online (Sandbox Code Playgroud)

不变性(即通用类型参数*不*inout关键字装饰)

貌似,这样的方法

var myAnimals = new List<LifeForm>
{
    new Giraffe(),
    new Zebra()
};
PrintLifeForms(myAnimals); // Giraffe, Zebra
Run Code Online (Sandbox Code Playgroud)

...应该接受异构集合:(它确实如此)

var myGiraffes = new List<Giraffe>
{
    new Giraffe(), // "Jerry"
    new Giraffe() // "Melman"
};
PrintLifeForms(myGiraffes); // Compile Error!
Run Code Online (Sandbox Code Playgroud)

但是,传递更多派生类型的集合失败!

 public static void PrintLifeForms(IList<LifeForm> lifeForms)
 {
     lifeForms.Add(new Zebra());
 }
Run Code Online (Sandbox Code Playgroud)

cannot convert from 'System.Collections.Generic.List<Giraffe>' to 'System.Collections.Generic.IList<LifeForm>'

为什么?因为泛型参数IList<LifeForm>不是协变的 - IList<T>是不变的,所以IList<LifeForm>只接受参数化类型T必须的集合(实现IList)LifeForm.

如果方法实现PrintLifeForms是恶意的(但具有相同的方法签名),编译器阻止传递的原因List<Giraffe>变得明显:

public static void PrintLifeForms(IEnumerable<LifeForm> lifeForms)
{
    foreach (var lifeForm in lifeForms)
    {
        Console.WriteLine(lifeForm.GetType().ToString());
    }
}
Run Code Online (Sandbox Code Playgroud)

由于IList允许添加或删除元素,LifeForm因此可以将任何子类添加到参数中lifeForms,并且违反传递给该方法的任何派生类型集合的类型.(在这里,恶意方法将尝试添加Zebravar myGiraffes).幸运的是,编译器保护我们免受这种危险.

协方差(通用参数化类型装饰out)

协方差广泛用于不可变集合(即无法在集合中添加或删除新元素)

上述示例的解决方案是确保使用协变泛型集合类型,例如IEnumerable(定义为IEnumerable<out T>).IEnumerable没有方法可以更改为集合,并且作为out协方差的结果,任何具有子类型的集合LifeForm现在都可以传递给方法:

public void PerformZebraAction(Action<Zebra> zebraAction)
{
    var zebra = new Zebra();
    zebraAction(zebra);
}
Run Code Online (Sandbox Code Playgroud)

PrintLifeForms现在可以调用Zebras,Giraffes以及IEnumerable<>任何子类LifeForm

逆变(通用参数化类型装饰in)

当函数作为参数传递时,经常使用逆变量.

这是一个函数的示例,它将Action<Zebra>一个参数作为参数,并在已知的Zebra实例上调用它:

var myAction = new Action<Zebra>(z => Console.WriteLine("I'm a zebra"));
PerformZebraAction(myAction); // I'm a zebra
Run Code Online (Sandbox Code Playgroud)

正如所料,这很好用:

var myAction = new Action<Giraffe>(g => Console.WriteLine("I'm a giraffe"));
PerformZebraAction(myAction); 
Run Code Online (Sandbox Code Playgroud)

直观地说,这将失败:

var myAction = new Action<Animal>(a => Console.WriteLine("I'm an animal"));
PerformZebraAction(myAction); // I'm an animal
Run Code Online (Sandbox Code Playgroud)

cannot convert from 'System.Action<Giraffe>' to 'System.Action<Zebra>'

但是,这成功了

var myAction = new Action<object>(a => Console.WriteLine("I'm an amoeba"));
PerformZebraAction(myAction); // I'm an amoeba
Run Code Online (Sandbox Code Playgroud)

甚至这也成功了:

public abstract class LifeForm  { }
public abstract class Animal : LifeForm { }
public class Giraffe : Animal { }
public class Zebra : Animal { }
Run Code Online (Sandbox Code Playgroud)

为什么?因为Action被定义为Action<in T>,即它contravariant,意味着,对于Action<Zebra> myAction,myAction可以是"最多"的Action<Zebra>,但是较少派生的超类Zebra也是可接受的.

虽然这一开始可能不直观(例如,如何将Action<object>其作为参数传递Action<Zebra>?),如果解压缩步骤,您会注意到被调用的函数(PerformZebraAction)本身负责传递数据(在本例中是一个Zebra实例) )函数 - 数据不是来自调用代码.

由于以这种方式使用高阶函数的反向方法,在Action调用时,它是Zebra针对zebraAction函数调用的更多派生实例(作为参数传递),尽管函数本身使用较少派生的类型.

  • 这是对不同方差选项的很好解释,因为它遍历了示例,并且还阐明了为什么编译器在没有in / out关键字的情况下限制或允许使用 (5认同)
  • 这是迄今为止所提供的最容易理解的解释。 (2认同)

Mic*_*tum 32

class A {}
class B : A {}

public void SomeFunction()
{
    var someListOfB = new List<B>();
    someListOfB.Add(new B());
    someListOfB.Add(new B());
    someListOfB.Add(new B());
    SomeFunctionThatTakesA(someListOfB);
}

public void SomeFunctionThatTakesA(IEnumerable<A> input)
{
    // Before C# 4, you couldn't pass in List<B>:
    // cannot convert from
    // 'System.Collections.Generic.List<ConsoleApplication1.B>' to
    // 'System.Collections.Generic.IEnumerable<ConsoleApplication1.A>'
}
Run Code Online (Sandbox Code Playgroud)

基本上每当你有一个带有一个类型的Enumerable的函数时,如果没有显式地转换它,你就无法传入一个派生类型的Enumerable.

只是为了警告你一个陷阱:

var ListOfB = new List<B>();
if(ListOfB is IEnumerable<A>)
{
    // In C# 4, this branch will
    // execute...
    Console.Write("It is A");
}
else if (ListOfB is IEnumerable<B>)
{
    // ...but in C# 3 and earlier,
    // this one will execute instead.
    Console.Write("It is B");
}
Run Code Online (Sandbox Code Playgroud)

无论如何这都是可怕的代码,但它确实存在,如果使用这样的结构,C#4中不断变化的行为可能会引入微妙且难以发现的错误.

  • 是的,最大的变化是IEnumerable现在支持这一点,而它之前没有. (3认同)

小智 10

逆变

在现实世界中,您始终可以使用动物收容所而不是兔子收容所,因为每次动物收容所收容一只兔子时,它都是动物。但是,如果您使用兔子收容所而不是动物收容所,它的工作人员可能会被老虎吃掉。

在代码中,这意味着如果你有一个,IShelter<Animal> animals你可以简单地编写IShelter<Rabbit> rabbits = animals if你 promise 和 use Tin IShelter<T>only 作为方法参数,如下所示:

public class Contravariance
{
    public class Animal { }
    public class Rabbit : Animal { }

    public interface IShelter<in T>
    {
        void Host(T thing);
    }

    public void NoCompileErrors()
    {
        IShelter<Animal> animals = null;
        IShelter<Rabbit> rabbits = null;

        rabbits = animals;
    }
}
Run Code Online (Sandbox Code Playgroud)

并用更通用的项目替换项目,即减少方差或引入反向方差。

协方差

在现实世界中,您始终可以使用兔子供应商而不是动物供应商,因为每次兔子供应商给您一只兔子时,它都是动物。但是,如果您使用动物供应商而不是兔子供应商,您可能会被老虎吃掉。

在代码中,这意味着如果你有一个,ISupply<Rabbit> rabbits你可以简单地写ISupply<Animal> animals = rabbits if you promise 和 use Tin ISupply<T>only as 方法返回值,如下所示:

public class Covariance
{
    public class Animal { }
    public class Rabbit : Animal { }

    public interface ISupply<out T>
    {
        T Get();
    }

    public void NoCompileErrors()
    {
        ISupply<Animal> animals = null;
        ISupply<Rabbit> rabbits = null;

        animals = rabbits;
    }
}
Run Code Online (Sandbox Code Playgroud)

并用更派生的项目替换项目,即增加方差或引入方差。

总而言之,这只是您的一个编译时可检查承诺,即您将以某种方式对待泛型类型以保持类型安全并且不会让任何人吃掉。

你可能想读一读这篇文章,让你的头脑围绕这一点。


Kam*_*ely 5

来自MSDN

以下代码示例显示了对方法组的协方差和逆变支持

static object GetObject() { return null; }
static void SetObject(object obj) { }

static string GetString() { return ""; }
static void SetString(string str) { }

static void Test()
{
    // Covariance. A delegate specifies a return type as object, 
    // but you can assign a method that returns a string.
    Func<object> del = GetString;

    // Contravariance. A delegate specifies a parameter type as string, 
    // but you can assign a method that takes an object.
    Action<string> del2 = SetObject;
}
Run Code Online (Sandbox Code Playgroud)