티스토리 뷰

 

공변성(covariance)과 반공변성(contravariance). 참 생소한 말이다.

우선 공변과 반공변에 대한 책에서 나온 정의부터 알아보자.

공변 : X를 Y로 바꾸어 사용할 수 있는 경우, C<T>가 C<X>를 C<Y>로 바꾸는 것이 가능하다면 공변이다.

반공변 : X를 Y로 바꾸어 사용할 수 있는 경우, C<T>가 C<Y>를 C<X>로 바꾸는 것이 가능하다면 반공변이다.

공변과 반공변을 처음 접해본 사람이라면 많이 혼란스러울 것 같다. 나도 그랬다.

그래서 조금 찾아봤을 때 그나마 쉽게 설명한 포스팅을 찾아봤다. 아래 링크를 참조하길 바란다.

(https://edykim.com/ko/post/what-is-coercion-and-anticommunism/)

아무튼 공변성과 반공변성은 가변성(variance)라고 부르고 이의 반대를 불변성(invariant)라고 한다.

공변과 반공변은 프로그래밍에서 특정 타입의 객체를 다른 타입의 객체로 변환할 수 있는 성격을 말하며 이러한 변환을 지원하려면 제네릭 인터페이스나 델리게이트의 정의부분에 제네릭 공변/반공변을 지원한다는 의미의 데코레이터(decorator)를 추가해야 한다.


배열의 공변은 안전하지 않다

우선 배열의 공변이 안전하지 않음을 알아보자.

abstract public class CelestialBody : IComparable<CelestialBody> { public double Mass { get; set; } public string Name { get; set; } // 생략 } public class Planet : CelestialBody { // 생략 } public class Moon : CelestialBody { // 생략 } public class Asteroid : CelestialBody { // 생략 }

다음 메서드는 CelestialBody 배열을 공변적으로 안전하게 다루는 경우이다.

public static void CovariantArray(CelestialBody[] baseItems) { foreach (var thing in baseItems) Console.WriteLine("{0} has a mass of {1} Kg", thing.Name, thing.Mass); }

다음 메서드도 동일 배열을 공변적으로 다루지만 안전하지 않다.

public static void UnsafeCovariantArray(CelestialBody[] baseItems) { baseItems[0] = new Asteroid { Name = "Hygiea", Mass = 8.85e19 }; }

앞의 예제에서 사용된 CelestialBody와 Asteroid는 상속 관계에 있는데 Celestialbody 타입의 매개변수를 취하는 메서드에는 Planet이나 Asteroid 타입의 객체를 넘겨줄 수 있다. 하지만 앞선 예제와 같이 파생 클래스의 객체를 베이스 클래스의 배열에 할당하면 문제가 발생할 수 있다.

제네릭 타입에 대한 공변

C# 4.0 이전에는 제네릭 타입은 가변성을 지원하지 않았다. 제네릭 타입은 모두 불변이었으며 타입 매개변수가 다른 경우 대체가 일절 불가능했다.

하지만 C# 4.0이 나오면서 비로소 공변과 반공변을 지원하도록 in과 out 키워드를 추가했는데 이를 이용하면 제네릭을 좀더 유용하게 사용할 수 있다.

이 데코레이터는 주로 제네릭 인터페이스나 델리게이트 선언 시에 사용할 수 있다.

우선 제네릭 타입에 대한 공변부터 알아보자. 다음 메서드에는 List<Planet> 타입의 객체를 인자로 전달할 수 있다.

public static void CovariantGeneric(IEnumerable<CelestialBody> baseItems) { foreach (var thing in baseItems) Console.WriteLine("{0} has a mass of {1} Kg", thing.Name, thing.Mass); }

이게 가능한 이유는 IEnumerable<T>를 정의할 때 T를 out으로 선언했기 때문이다.

public interface IEnumerable<out T> : IEnumerable { new IEnumerator<T> GetEnumerator(); } public interface IEnumerator<out T> : IDisposable, IEnumerator { new T Current { get; } // MoveNext(), Reset()은 IEnumerator에서 상속받는다. }

out 데코레이터에 주목하자. out 키워드는 타입 매개변수 T를 출력(output)위치에서만 사용하겠다고 컴파일러에게 알려주는 것이다. (출력 위치 : 함수의 반환값, 속성의 get 접근자, 델리게이트의 일부 위치)

out 키워드를 선언해주면 컴파일러는 시퀀스 내에서 T의 내용을 조회는 하겠지만 내용을 수정하지는 않을 것이라고 생각한다. 이 경우에는 Planet을 CelestialBody로 다뤄도 올바르게 동작한다.

제네릭 타입에 대한 반공변

반공변 제네릭 인터페이스와 델리게이트를 만드는 방법도 비슷하다. out을 in으로만 바꿔보자. in을 사용하면 컴파일러에게 타입 매개변수를 입력 위치에서만 사용할 것이라고 알려주게 된다.

(입력 위치 : 메서드의 매개변수, 일부 델리게이트의 매개변수를 지정하는 용도)

public interface IComparable<in T> { int CompareTo(T other); }

IComparable<T>는 in 데코레이터를 사용하는데 이럴 경우, CelestialBody에서 mass 속성등을 이용하여 IComparable<T>를 구현할 수 있음을 의미한다. 즉 2개의 Planet 객체를 비교하거나, Planet과 Moon객체 등을 비교하면서 CelestialBody에 서브 클래스 객체를 대입할 수 있다는 말이다.

델리게이트의 매개변수에 대한 공변/반공변

델리게이트의 매개변수에 대한 공변/반공변은 좀 더 쉬운 편이다. 메서드의 매개변수 타입은 반공변(in)이고 반환 타입은 공변(out)이다.

.NET Base Class Library(BCL)에 포함된 델리게이트의 정의도 가변성을 지원하도록 다음과 같이 수정됐다.

public delegate TResult Func<out TResult>(); public delegate TResult Func<in T, out TResult>(T arg); public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2); public delegate void Action<in T>(T arg); public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2); public delegate void Action<in T1, in T2, in T3>(T1 arg1, T2 arg2, T3 arg3);

정리

공변과 반공변이 어떻게 동작하는지를 정확히 설명하는 것은 어렵지만 다행스럽게도 언어 차원에서 제네릭 인터페이스나 델리게이트 정의 시에 사용할 수 있는 in(반공변), out(공변) 데코레이터를 정의해뒀다.

가능하면 제네릭 인터페이스나 델리게이트 정의 시에 in, out 데코레이터를 사용하자.

이렇게 하면 가변성과 관련된 에러를 컴파일러가 사전에 확인할 수 있다.


이전 항목에서 느낀 바와 동일하다.

와우..너무 생소하다..C#의 기반을 닦아야 한다...22222

공변과 반공변..곧 C# 기본책 살건데 거기서 다시 제대로 공부하자!!

참조 - Effective C# <강력한 C# 코드를 구현하는 50가지 전략과 기법, 이펙티브>, 빌 와그너, 김명신, 한빛미디어

공변성, 반공변성 참조 - https://edykim.com/ko/post/what-is-coercion-and-anticommunism/

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
글 보관함