공분산 및 반 공예 실제 사례


162

현실 세계에서 공분산과 공분산을 사용하는 방법을 이해하는 데 약간의 어려움이 있습니다.

지금까지 내가 본 유일한 예는 동일한 이전 배열 예입니다.

object[] objectArray = new string[] { "string 1", "string 2" };

다른 곳에서 사용되는 것을 볼 수 있다면 개발 중에 사용할 수있는 예제를 보는 것이 좋을 것입니다.


1
나는 (내 자신의) 질문에 대한 이 답변 에서 공분산 을 탐구합니다 : 공분산 유형 : 예를 들어 . 나는 당신이 그것을 흥미롭고 유익하게 찾을 것이라고 생각합니다.
Cristian Diaconescu

답변:


109

Person 클래스와 그 클래스에서 파생 된 클래스가 있다고 가정 해 봅시다. IEnumerable<Person>인수로 사용되는 몇 가지 작업이 있습니다 . School 클래스에는을 반환하는 메서드가 있습니다 IEnumerable<Teacher>. 공분산을 사용하면을 취하는 메소드에 대해 그 결과를 직접 사용할 수 있으며 IEnumerable<Person>, 파생이 덜 된 (보다 일반적인) 유형으로 더 파생 된 유형을 대체 할 수 있습니다. 반 직관적으로 반대 분산을 사용하면보다 파생 된 유형이 지정된 더 일반적인 유형을 사용할 수 있습니다.

MSDN에서 Generics의 공분산 및 공분산을 참조하십시오 .

수업 :

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);
    }
}

사용법 :

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<in 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());

14
@FilipBartuzi-이 답변을 쓸 때 나와 같이 당신은 매우 실제적인 예인 대학에 고용되어 있다면.
tvanfosson

5
질문에 대답하지 않고 C #에서 co / contra variance를 사용하는 예를 제공하지 않을 때 어떻게 대답을 표시 할 수 있습니까?
barakcaf

@barakcaf는 반차의 예를 추가했습니다. 왜 공분산의 예를 보지 못했는지 확실하지 않습니다. 아마도 코드를 아래로 스크롤해야 할 수도 있지만 그 주위에 의견을 추가했습니다.
tvanfosson 2016 년

@tvanfosson 코드는 co / contra를 사용하며, 어떻게 선언하는지를 보여주지 않습니다. 이 예는 일반 선언에서 입출력의 사용법을 보여주지 않지만 다른 답변은 사용합니다.
barakcaf

따라서 내가 올바르게 이해하면 공분산은 Liskov의 C #에서 대체 원칙을 허용하는 것입니다. 맞습니까?
Miguel Veloso

136
// 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();

완전성을 위해…

// 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??

138
나는이 현실적인 예를 좋아한다. 나는 지난주에 당나귀 고 블링 코드를 작성하고 있었고 지금 우리가 공분산을 할 수있어서 기뻤습니다. :-)
Eric Lippert

4
위의 @javadba가 위의 의견을 통해 EricLippert에게 공분산과 반공 분을 알려주는 것은 할머니에게 알을 빨아들이는 방법을 알려주는 현실적인 공분산 예입니다! : p
iAteABug_And_iLiked_it

1
이 질문은 반공 분산과 공분산이 무엇을 할 수 있는지 묻지 않았으며 왜 그것을 사용해야하는지 물었 습니다 . 귀하의 예는 필요하지 않기 때문에 실용적이지 않습니다. QuadrupedGobbler를 만들어 자체로 처리 할 수 ​​있으며 (IGobbler <Quadruped>에 할당) 여전히 Donkeys를 어글 링 할 수 있습니다 (Quadruped가 필요한 Gobble 메서드에 Donkey를 전달할 수 있음). 모순이 필요하지 않습니다. 우리 QuadrupedGobbler를 DonkeyGobbler로 취급 할 있다는 것이 멋지지만, QuadrupedGobbler가 이미 Donkey를 고칠 수 있다면 왜 우리가 필요한가?
wired_in

1
@wired_in 당나귀 만 신경 쓰면 좀 더 일반적인 사람이 될 수 있기 때문입니다. 예를 들어, 당나귀를 먹일 수있는 농장이 있다면이를로 표현할 수 있습니다 void feed(IGobbler<Donkey> dg). 대신 IGobbler <Quadruped>를 매개 변수로 사용하면 당나귀 만 먹는 용을 통과 할 수 없었습니다.
Marcelo Cantos

1
파티에 늦었지만, 이것은 내가 주변에서 본 최고의 서면 예에 관한 것입니다. 말도 안되는 동안 완전한 의미를 갖습니다. 나는 대답으로 내 게임을해야 할거야 ...
Jesse Williams

121

차이점을 이해하도록 돕기 위해 함께 정리 한 내용은 다음과 같습니다.

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) { }
}

tldr

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

10
이것이 내가 지금까지 본 것 중 가장 명확하고 간결한 것입니다.
Rob L

6
부모는 Contravariance언제 과일을 사과로 다운 캐스트 할 수 있습니까 ( 예에서) ? FruitApple
Tobias Marschall

@TobiasMarschall은 "다형성"에 대해 더 공부해야한다는 것을 의미합니다.
snr

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

물고기가 동물의 하위 유형이라고 가정합니다. 그건 그렇고 좋은 대답입니다.
Rajan Prasad

48

상속 계층 구조를 사용하는 간단한 예는 다음과 같습니다.

간단한 클래스 계층이 주어지면 :

여기에 이미지 설명을 입력하십시오

그리고 코드에서 :

public abstract class LifeForm  { }
public abstract class Animal : LifeForm { }
public class Giraffe : Animal { }
public class Zebra : Animal { }

불일치 (예 : 또는 키워드로 장식 되지 않은 일반 유형 매개 변수 )inout

겉보기에는 이와 같은 방법

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

... 이기종 콜렉션을 받아 들여야합니다.

var myAnimals = new List<LifeForm>
{
    new Giraffe(),
    new Zebra()
};
PrintLifeForms(myAnimals); // Giraffe, Zebra

그러나 더 파생 된 형식 의 컬렉션을 전달 하면 실패합니다!

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

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(IList<LifeForm> lifeForms)
 {
     lifeForms.Add(new Zebra());
 }

이후 IList허가 추가 또는 요소의 제거 중 어느 서브 클래스 LifeForm따라서 파라미터에 추가 될 수 lifeForms및 유도 된 유형의 수집의 종류에 위반이 메소드로 전달. (여기서 악의적 인 방법은에를 추가하려고 시도 Zebra합니다 var myGiraffes). 다행히 컴파일러는 이러한 위험으로부터 우리를 보호합니다.

공분산 (로 장식 된 매개 변수화 된 유형의 일반 out)

공분산은 불변 컬렉션에 널리 사용됩니다 (즉, 컬렉션에서 새 요소를 추가하거나 제거 할 수없는 경우)

위의 예에 대한 해결책은 공변량 제네릭 수집 유형이 사용되도록하는 것입니다 IEnumerable( 예 :로 정의 됨 IEnumerable<out T>). IEnumerable컬렉션으로 변경할 방법이 없으며 out공분산 의 결과 로 하위 유형이있는 컬렉션 LifeForm이 이제 메서드에 전달 될 수 있습니다.

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

PrintLifeForms지금 호출 할 수 있습니다 Zebras, Giraffes그리고 어떤 IEnumerable<>의 서브 클래스의LifeForm

불균형 (로 장식 된 매개 변수화 된 유형의 일반 in)

공분산은 함수가 매개 변수로 전달 될 때 자주 사용됩니다.

다음 Action<Zebra>은 매개 변수로 매개 변수를 사용하여 알려진 Zebra 인스턴스에서 호출 하는 함수의 예입니다 .

public void PerformZebraAction(Action<Zebra> zebraAction)
{
    var zebra = new Zebra();
    zebraAction(zebra);
}

예상대로 이것은 잘 작동합니다.

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

직관적으로, 이것은 실패합니다 :

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

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

그러나 이것은 성공합니다

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

심지어 이것도 성공합니다.

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

왜? 때문에 Action같이 정의된다 Action<in T>, 즉, 그것이 contravariant대한 것을 의미 Action<Zebra> myAction하는, myAction"대부분의"에있을 수 Action<Zebra>의 수퍼 미만 유도는 Zebra도 가능하다.

처음에는 직관적이지 않을 수도 있지만 (예 : ?를 Action<object>요구하는 매개 변수로 전달 하는 방법 Action<Zebra>) 단계를 풀면 호출 된 함수 ( PerformZebraAction) 자체가 데이터 전달 (이 경우 Zebra인스턴스)을 담당합니다. )를 함수에-데이터가 호출 코드에서 나오지 않습니다.

이러한 방식으로 고차 함수를 사용하는 역 접근 방식 때문에 함수 Action가 호출 될 때 함수 자체가 덜 파생 된 유형을 사용하더라도 함수 Zebra에 대해 호출되는 것이 더 파생 된 인스턴스입니다 zebraAction(파라미터로 전달됨).


7
이것은 예제를 통해 이야기하고 컴파일러가 입력 / 출력 키워드없이 제한하거나 허용하는 이유를 설명하기 때문에 다양한 분산 옵션에 대한 훌륭한 설명입니다.
Vikhram

어디되는 in키워드는 사용 contravariance ?
javadba

위에서 @javadba, Action<in T>Func<in T, out TResult>입력 타입 contravariant이다. (내 예제는 기존 불변 (목록), 공변량 (IEnumerable) 및 반 변형 (Action, Func) 유형을 사용합니다)
StuartLC

좋아, C#그렇게하지 않을 것입니다.
javadba

스칼라에서는 상당히 유사합니다. 단지 다른 구문입니다. [+ T]는 T에서 공변량이고, [-T]는 T에서 공변량입니다. Scala는 '사이'제약 조건과 'Nothing'무차별 서브 클래스를 적용 할 수 있습니다. 없습니다.
StuartLC

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>'
}

기본적으로 한 유형의 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");
}

어쨌든 그것은 끔찍한 코드이지만 존재하지만 C # 4의 변화하는 동작은 이와 같은 구문을 사용하면 미묘하고 버그를 찾기가 어려울 수 있습니다.


c # 3에서는 파생 된 형식이 아닌 메서드에 더 파생 된 형식을 전달할 수 있기 때문에 이는 컬렉션에 영향을줍니다.
면도기

3
예, 큰 변화는 IEnumerable이 이제 이것을 지원하지만 이전에는 지원하지 않았다는 것입니다.
Michael Stum

4

에서 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;
}

4

공분산

실제로는 동물 보호소가 토끼를 주최 할 때마다 동물이므로 동물 보호소 대신 토끼 보호소 대신 동물 보호소를 사용할 수 있습니다. 그러나 동물 보호소 대신 토끼 보호소를 사용하면 직원이 호랑이에게 먹을 수 있습니다.

당신이있는 경우 코드에서,이 방법은 IShelter<Animal> animals간단히 쓸 수 IShelter<Rabbit> rabbits = animals 있다면 당신이 약속과 사용 TIShelter<T>메소드 매개 변수가 너무 좋아 전용으로 :

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;
    }
}

항목을보다 일반적인 것으로 대체하십시오. 즉, 분산을 줄이거 나 대비 분산을 도입하십시오 .

공분산

실제 세계에서는 토끼 공급 업체가 토끼를 줄 때마다 동물이기 때문에 동물 공급 업체 대신 항상 토끼 공급 업체를 사용할 수 있습니다. 그러나 토끼 공급 업체 대신 동물 공급 업체를 사용하면 호랑이가 먹을 수 있습니다.

당신이있는 경우 코드에서,이 방법은 ISupply<Rabbit> rabbits간단히 쓸 수 ISupply<Animal> animals = rabbits 있다면 당신이 약속과 사용 TISupply<T>방법 반환 값이 너무 좋아 전용으로 :

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;
    }
}

그리고 즉, 둘 이상의 파생와 항목을 대체 분산을 증가 시키거나 소개 공동의 분산을.

결국, 이것은 당신이 형식 안전을 유지하고 다른 사람을 먹지 않도록 특정 형식으로 일반 형식을 처리 할 것이라는 컴파일 타임의 확인 가능한 약속입니다.

당신은 제공 할 수 있습니다 A가이 주위에 두 번 감싸 머리에 읽어 보시기 바랍니다.


당신은
공짜

에 대한 귀하의 의견 contravariance은 흥미 롭습니다. 운영 요구 사항 을 나타내는 것으로 읽으 십시오.보다 일반적인 유형은 파생 된 모든 유형의 사용 사례를 지원해야한다는 것입니다. 따라서이 경우 동물 보호소 는 모든 동물 보호소 를 보호 할 수 있어야합니다. 이 경우 새로운 서브 클래스를 추가하면 수퍼 클래스가 깨질 수 있습니다! 즉, 티라노사우루스 렉스 (Tyrannosaurus Rex) 아형을 추가하면 기존의 동물 보호소를 망칠 수 있습니다.
javadba

(계속되는). 이는 구조적 으로 명확하게 설명 된 공분산 과는 크게 다릅니다 . 더 구체적인 하위 유형은 모두 수퍼 유형에 정의 된 연산을 지원하지만 반드시 같은 방식은 아닙니다.
javadba

3

변환기 대리자는 두 가지 개념을 함께 시각화하는 데 도움이됩니다.

delegate TOutput Converter<in TInput, out TOutput>(TInput input);

TOutput메소드가 보다 구체적인 유형을 리턴하는 공분산을 나타냅니다 .

TInput메소드가 덜 구체적인 유형으로 전달되는 반공 분산을 나타냅니다 .

public class Dog { public string Name { get; set; } }
public class Poodle : Dog { public void DoBackflip(){ System.Console.WriteLine("2nd smartest breed - woof!"); } }

public static Poodle ConvertDogToPoodle(Dog dog)
{
    return new Poodle() { Name = dog.Name };
}

List<Dog> dogs = new List<Dog>() { new Dog { Name = "Truffles" }, new Dog { Name = "Fuzzball" } };
List<Poodle> poodles = dogs.ConvertAll(new Converter<Dog, Poodle>(ConvertDogToPoodle));
poodles[0].DoBackflip();
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.