본문 바로가기

Develop/.NET 가이드

[C#] Covariance 공변성 및 Contravariance 반공변성

반응형

Cvariance 공변성 및 Contravariance 반공변성
Covariance & Contravariance

공변성 및 반공변성

마이크로소프트의 공식문서에서 공변성과 반공변성에 대한 개념을 소개하고는 있지만, 저는 이해하기가 너무 어려웠습니다. 저에게는 글 구성이 너무 난해하고 배경 설명이 너무 부족했습니다. 그래서 저와 같은 어려움을 겪는 개발자를 위해 제 나름대로 이해한 내용을 정리하여 기록해봅니다. 모두에게 유익한 글이 되기를...

상속 관계에 있는 클래스는 서로 형변환이 가능합니다. 다만 형변환을 하게 되면 정의된 범위와 구현된 범위가 달라지게 됩니다. 그래서 객체 간에 형변환을 하게 되면 예상치 못한 예외가 발생하곤 합니다. 특히 공변성과 반공변성이라는 개념은 이러한 형변환으로 파생되는 예외와 관련된 개념입니다.

C# 언어가 발전하면서 .NET 내부적으로 제네릭 타입과 대리자 기능을 활발하게 사용되었습니다. 이는 곧 형변환이 자주 일어나게 되는 결과를 가져오게 됩니다. 문제는 형변환으로 발생 가능한 모든 예외를 컴파일할 때 검증할 수 없었습니다. 그리고 복잡하게 사용되는 제네릭 타입과 대리자를 따라가며 예외 사항을 모두 확인하기란 너무 어렵습니다. 그래서 형변환으로 발생 가능한 모든 예외를 컴파일할 때 검증하기 위해 형변환하는 형태를 공변성반공변성으로 분류하게 됩니다. 형변환 형태를 구분하여 처리함으로 컴파일로 더 많은 예외를 검증할 수 있게 하여 코드 생산성을 증진시키는 겁니다. C# 에서는 inout 이라는 키워드를 통해 허용되는 형변환 형태를 구분합니다.

결국 공변성과 반공변성은 형변환의 형태를 분류하기 위한 개념이고, 이 개념을 언어로 구현하기 위해 C# 에서는 inout 키워드를 활용합니다.

공변성과 반공변성은 쉽게 설명하면,

  • 공변성 자신과 자식으로만 형변환. out 키워드로 지정.
  • 반공변성 자신과 부모로만 형변환. in 키워드로 지정.
  • 불변성 자신으로만 형변환

일반적인 개발자라면 공변성과 반공변성이 필요한 이유를 납득하기 매우 어려우실 수 있습니다. 왜냐하면 지금 설명하는 내용이 미래에 어떤 오류를 발생시킬지 머리 속으로 상상하기가 매우 어렵기 때문이죠. 또한 이미 언어 수준에서 예외가 발생하지 않도록 세심하게 배려해주고 있기에 지금에 와서 형변환과 관련된 복잡한 예외를 실제로 마주칠 일도 거의 없어졌습니다. 그러므로 '공변성과 반공변성이라는 어려운 단어가 개발에서는 이러한 뜻이구나'라고 이해하고 코드에 등장하면 당황하지 않으실 정도로만 이해해도 충분하다 생각합니다.

하지만 지식에 목마른 개발자를 위해 더 자세히 설명하자면,

공변성 형변환

IEnumerable<string> strings = new List<string>();  
// object를 상속하는 자식 객체 string 이 object 부모 변수에 할당됩니다.
IEnumerable<object> objects = strings;  
static string GetString() { return ""; }  
static void Test()  
{  
    // 반환타입이 object 로 string 의 부모인 대리자에 반환타입이 string 으로 obejct의 자식인 함수를 할당합니다.  
    Func<object> del = GetString;  
}  

자식 객체가 부모 변수로 할당되는 형변환
C# 키워드는 out

공변 제네릭 인터페이스

// Covariant interface.
interface ICovariant<out R> { }

// Extending covariant interface.
interface IExtCovariant<out R> : ICovariant<R> { }

// Implementing covariant interface.
class Sample<R> : ICovariant<R> { }

class Program
{
    static void Test()
    {
        ICovariant<Object> iobj = new Sample<Object>();
        ICovariant<String> istr = new Sample<String>();

        // You can assign istr to iobj because
        // the ICovariant interface is covariant.
        iobj = istr;
    }
}

공변 제네릭 대리자

// Covariant delegate.
public delegate R DCovariant<out R>();

// Methods that match the delegate signature.
public static Control SampleControl()
{ return new Control(); }

public static Button SampleButton()
{ return new Button(); }

public void Test()
{            
    // Instantiate the delegates with the methods.
    DCovariant<Control> dControl = SampleControl;
    DCovariant<Button> dButton = SampleButton;

    // You can assign dButton to dControl
    // because the DCovariant delegate is covariant.
    dControl = dButton;

    // Invoke the delegate.
    dControl(); 
}

반공변성 형변환

// 클래스 내에 아래와 같은 함수가 정의되어 있다고 가정해봅시다.
// static void SetObject(object o) { }
Action<object> actObject = SetObject;  
// 자식 변수 string 을 받는 대리자에게 부모 변수 object 를 받는 대리자를 할당합니다.
Action<string> actString = actObject; 

부모 객체가 자식 변수로 할당되는 형변환
C# 키워드는in

반공변 제네릭 인터페이스

// Contravariant interface.
interface IContravariant<in A> { }

// Extending contravariant interface.
interface IExtContravariant<in A> : IContravariant<A> { }

// Implementing contravariant interface.
class Sample<A> : IContravariant<A> { }

class Program
{
    static void Test()
    {
        IContravariant<Object> iobj = new Sample<Object>();
        IContravariant<String> istr = new Sample<String>();

        // You can assign iobj to istr because
        // the IContravariant interface is contravariant.
        istr = iobj;
    }
}

반공변 제네릭 대리자

// Contravariant delegate.
public delegate void DContravariant<in A>(A argument);

// Methods that match the delegate signature.
public static void SampleControl(Control control)
{ }
public static void SampleButton(Button button)
{ }

public void Test()
{

    // Instantiating the delegates with the methods.
    DContravariant<Control> dControl = SampleControl;
    DContravariant<Button> dButton = SampleButton;

    // You can assign dControl to dButton
    // because the DContravariant delegate is contravariant.
    dButton = dControl;

    // Invoke the delegate.
    dButton(new Button()); 
}
반응형

태그

  • 비밀댓글입니다

    • 제네릭은 엔터프라이즈 아키텍처에서 특정 형타입을 지정하기 위해 사용하기 보다는 인터페이스를 제공하는 기능으로 활용됩니다!

      1) interface C가 있을 때 class A 와 class B 모두 interface C를 구현합니다.

      2) 그리고 class D<T>를 아래와 같이 정의합니다.

      "class D<T> where T : C"

      이렇게 함으로서 D<T>에 A와 B 모두 활용할 수 있게 됩니다. 왜냐하면 A와 B 모두 C를 구현했기 때문입니다.

      이렇게 복잡하게 구현하는 이유는 새로운 클래스를 만들어서 D<T>에 쓰고 싶을 때는 C만 구현하면 되는 편안함이 있기 때문입니다. 특히 여러 개발자가 함께 개발할 때 D<T>의 내부 구현을 알 필요없이 C만 알고 있으면 됩니다.

      여기서 형변환이 자주 일어나는 이유는 D<T>를 사용할 때마다 내부적으로 A와 B가 C로 형변환되기 때문입니다 ㅎㅎ

      더 궁금하신 점이 있으시다면 말씀해주세요 ㅎㅎ

  • jun 2020.09.15 12:48 댓글주소 수정/삭제 댓글쓰기

    공변/반공변 제네릭 인터페이스 예제는 in/out 키워드만 바뀌는데
    공변/반공변 제네릭 델리게이트 예제는 리턴타입과 아규먼트까지 바꿔줘야 하네요
    public delegate R DelegateCovariant<in R>(); 은 왜 안되는걸까욤,,
    좋은글 감사드립니다

    • in 키워드는 자신과 부모로만 형변환한다는 뜻입니다. 이 때 public delegate R DelegateCovariant<in R>(); 에서 반환하는 R 이 in 키워드로 지정되게 되면 R 이나 R의 부모로 반환하겠다는 의미가 됩니다. 그렇게 되면 대리자를 사용하는 입장에서 R 이 아닌 object 가 반환될 수 있으니 대비하라는 말이 되버립니다. 따라서 in 키워드를 적용하고 반환하는 건 R 보다 자식이 아닌 모든 것이 반환될 수 있으니 유의하라는 어처구니 없는 상황이 발생하기 때문에, in 키워드를 반환 형식에 사용할 수 없는 겁니다.

  • 선생님 꿀같은 글 감사합니다