C #의 공변 및 반 변성 인터페이스 이해


87

C #에서 읽고있는 교과서에서 이러한 문제를 발견했지만 컨텍스트 부족으로 인해 이해하는 데 어려움이 있습니다.

그것들이 무엇인지, 그리고 그들이 밖에서 무엇을 위해 유용한 지에 대한 간결한 설명이 있습니까?

명확히하기 위해 편집 :

공변 인터페이스 :

interface IBibble<out T>
.
.

반 변성 인터페이스 :

interface IBibble<in T>
.
.

3
이것은 짧고 좋은 확장 IMHO입니다 : blogs.msdn.com/csharpfaq/archive/2010/02/16/…
digEmAll

유용 할 수 있습니다 : 블로그 게시물
Krunal

흠 그것은 좋지만 그것이 나를 당황스럽게 하는지를 설명하지 못합니다 .
NibblyPig

답변:


143

를 사용 <out T>하면 인터페이스 참조를 계층 구조에서 상위로 취급 할 수 있습니다.

를 사용 <in T>하면 인터페이스 참조를 계층 구조에서 아래쪽으로 처리 할 수 ​​있습니다.

좀 더 영어로 설명해 보겠습니다.

동물원에서 동물 목록을 검색하고 있으며이를 처리하려고한다고 가정 해 보겠습니다. 동물원에있는 모든 동물에는 이름과 고유 ID가 있습니다. 어떤 동물은 포유류이고, 어떤 것은 파충류이고, 어떤 것은 양서류이고, 어떤 것은 물고기입니다. 그러나 그들은 모두 동물입니다.

따라서 동물 목록 (다른 유형의 동물 포함)을 사용하면 모든 동물에 이름이 있다고 말할 수 있으므로 모든 동물의 이름을 알아내는 것이 안전 할 것입니다.

그러나 물고기 목록 만 있지만 동물처럼 취급해야하는 경우에는 어떻게됩니까? 직관적으로 작동하지만 C # 3.0 이전에서는이 코드 조각이 컴파일되지 않습니다.

IEnumerable<Animal> animals = GetFishes(); // returns IEnumerable<Fish>

그 이유는 컴파일러가 동물 컬렉션을 검색 한 후 의도 한 바를 "알지"못하거나 할 수 있기 때문입니다. 아는 한, IEnumerable<T>개체를 목록에 다시 넣는 방법이있을 수 있으며, 그러면 잠재적으로 물고기가 아닌 동물을 물고기 만 포함해야하는 컬렉션에 넣을 수 있습니다.

즉, 컴파일러는 이것이 허용되지 않는다고 보장 할 수 없습니다.

animals.Add(new Mammal("Zebra"));

따라서 컴파일러는 코드 컴파일을 완전히 거부합니다. 이것은 공분산입니다.

반공 변성을 살펴 보겠습니다.

우리 동물원은 모든 동물을 다룰 수 있기 때문에 확실히 물고기를 다룰 수 있으므로 동물원에 물고기를 추가해 보겠습니다.

C # 3.0 이하에서는 컴파일되지 않습니다.

List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>
fishes.Add(new Fish("Guppy"));

여기서 컴파일러 List<Animal> 모든 물고기가 동물이기 때문에 메서드가 단순히 반환 하더라도이 코드 조각을 허용 수 있으므로 유형을 다음과 같이 변경하면됩니다.

List<Animal> fishes = GetAccessToFishes();
fishes.Add(new Fish("Guppy"));

그러면 작동하지만 컴파일러는이 작업을 수행하지 않는지 확인할 수 없습니다.

List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>
Fish firstFist = fishes[0];

목록은 실제로 동물 목록이므로 허용되지 않습니다.

따라서 반대 및 공분산은 객체 참조를 처리하는 방법과이를 사용하여 수행 할 수있는 작업입니다.

C # 4.0 의 inout키워드는 특히 인터페이스를 둘 중 하나로 표시합니다. 를 사용 in하면 제네릭 유형 (일반적으로 T)을 입력 위치에 배치 할 수 있습니다. 이는 메서드 인수 및 쓰기 전용 속성을 의미합니다.

를 사용하면 출력 위치에 out제네릭 유형을 배치 할 수 있습니다. 이는 메소드 반환 값, 읽기 전용 속성 및 out 메소드 매개 변수입니다.

이렇게하면 코드로 의도 한 작업을 수행 할 수 있습니다.

IEnumerable<Animal> animals = GetFishes(); // returns IEnumerable<Fish>
// since we can only get animals *out* of the collection, every fish is an animal
// so this is safe

List<T> T에 대한 내부 및 외부 방향이 모두 있으므로 공변도 반 변이도 아니지만 다음과 같이 객체를 추가 할 수있는 인터페이스입니다.

interface IWriteOnlyList<in T>
{
    void Add(T value);
}

다음과 같이 할 수 있습니다.

IWriteOnlyList<Fish> fishes = GetWriteAccessToAnimals(); // still returns
                                                            IWriteOnlyList<Animal>
fishes.Add(new Fish("Guppy")); <-- this is now safe

다음은 개념을 보여주는 몇 가지 비디오입니다.

예를 들면 다음과 같습니다.

namespace SO2719954
{
    class Base { }
    class Descendant : Base { }

    interface IBibbleOut<out T> { }
    interface IBibbleIn<in T> { }

    class Program
    {
        static void Main(string[] args)
        {
            // We can do this since every Descendant is also a Base
            // and there is no chance we can put Base objects into
            // the returned object, since T is "out"
            // We can not, however, put Base objects into b, since all
            // Base objects might not be Descendant.
            IBibbleOut<Base> b = GetOutDescendant();

            // We can do this since every Descendant is also a Base
            // and we can now put Descendant objects into Base
            // We can not, however, retrieve Descendant objects out
            // of d, since all Base objects might not be Descendant
            IBibbleIn<Descendant> d = GetInBase();
        }

        static IBibbleOut<Descendant> GetOutDescendant()
        {
            return null;
        }

        static IBibbleIn<Base> GetInBase()
        {
            return null;
        }
    }
}

이러한 표시가 없으면 다음이 컴파일 될 수 있습니다.

public List<Descendant> GetDescendants() ...
List<Base> bases = GetDescendants();
bases.Add(new Base()); <-- uh-oh, we try to add a Base to a Descendant

아니면 이거:

public List<Base> GetBases() ...
List<Descendant> descendants = GetBases(); <-- uh-oh, we try to treat all Bases
                                               as Descendants

흠, 공분산과 반공 분산의 목표를 설명해 주시겠습니까? 더 이해하는 데 도움이 될 것입니다.
NibblyPig

1
컴파일러가 이전에 방지 한 마지막 부분을 참조하십시오. in과 out의 목적은 안전한 인터페이스 (또는 유형)로 할 수있는 작업을 말하여 컴파일러가 안전한 작업을 수행하는 것을 방해하지 않도록하는 것입니다. .
Lasse V. Karlsen

훌륭한 답변, 나는 그들이 매우 도움이 된 비디오를 보았고 귀하의 예와 결합하여 지금 정렬했습니다. 한 가지 질문 만 남아 있습니다. 이것이 'out'과 'in'이 필요한 이유입니다. 왜 Visual Studio는 사용자가하려는 작업 (또는 그 이유)을 자동으로 인식하지 못합니까?
NibblyPig

Automagic "I see what you 're trying to do there"는 일반적으로 클래스와 같은 것을 선언 할 때 눈살을 찌푸립니다. 프로그래머가 유형을 명시 적으로 표시하도록하는 것이 좋습니다. T를 반환하는 메서드가있는 클래스에 "in"을 추가하면 컴파일러가 불평 할 것입니다. 이전에 자동으로 추가 된 "in"을 자동으로 제거하면 어떻게 될지 상상해보십시오.
Lasse V. Karlsen

1
한 키워드에이 긴 설명이 필요하면 분명히 잘못된 것입니다. 제 생각에 C #은이 특별한 경우에 너무 똑똑해 지려고합니다. 그래도 멋진 설명 감사합니다.
rr- 2014-06-05

8

이 게시물 은 내가 주제에 대해 읽은 것 중 최고입니다.

간단히 말해서, 공분산 / 반 변성 / 불변은 자동 유형 캐스팅 (기본에서 파생으로 또는 그 반대)을 다룹니다. 이러한 유형 캐스트는 캐스트 된 객체에 대해 수행되는 읽기 / 쓰기 작업과 관련하여 일부 보장이 존중되는 경우에만 가능합니다. 자세한 내용은 게시물을 읽어보십시오.


5
링크가 죽은 것 같습니다. 여기에 보관 된 버전입니다 : web.archive.org/web/20140626123445/http://adamnathan.co.uk/...
si618은

1
나는이 설명을 더 좋아한다 : codepureandsimple.com/…
volkit
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.