C#의 인터페이스와 대리자에 적용되는 제너릭 타입의 형변환에 관한 기준이다.
Base 기반 클래스와 Derived 파생 클래스가 있다하자.
public class Base {}
public class Derived : Base {}
Covariance 공변성
- 일반적인 다형성 참조 형식의 캐스팅에도 사용되는 방식으로
- 파생 타입의 인스턴스를 기반 타입으로 참조할 수 있다.
IEnumerable<Derived> d = new List<Derived>();
IEnumerable<Base> b = d;
Contravariance 반공변성
- 반대로 기반 타입 인스턴스를 파생 타입으로 참조한다.
- 일반적인 다형성 참조 형식에는 적용되지 않는다.
- 대리자의 제너릭 타입에만 적용된다.
Action<Base> b = (target) => { Console.WriteLine(target.GetType().Name); };
Action<Derived> d = b;
d(new Derived());
- 반대로 타입을 지정할 수 있는 이유는 기존 대리자 타입이 Base이기 때문에 가능하다.
- 위 코드에서 Action<Derived> d에 Action<Base> 타입 인스턴스 b를 대입하였다.
- d는 항상 Derived 타입 인스턴스로 호출이 가능하고 d가 호출시 등록된 대리자 b가 호출된다.
- b는 Base 타입 인스턴스를 인자로 받고 Derived는 Base 타입으로 안전하게 캐스팅이 가능하므로 문제 없이 Action 타입b를 호출할 수 있다.
공변성과 반공변성을 지원하는 것을 variant 가변성이라고 지칭하고 둘다 지원하지 않는다면 invariant 불변성이라고 한다.
CLR이 제공하는 공변성과 반공변성의 특징은 다음과 같다.
- 가변성 타입(공변성/반공변성)은 제너릭 인터페이스와 대리자 형식에만 사용된다.
- 제너릭 인터페이스 혹은 대리자는 공변성과 반공변성을 모두 가질 수 있다.
- 가변성은 참조 타입에만 적용되므로 가변 타입에 값 타입을 사용한다면, 만들어진 인터페이스나 대리자는 불변성이 된다.
공변성 제너릭 인터페이스 예시) IEnumerable<T>
using System;
using System.Collections.Generic;
class Base
{
public static void PrintBases(IEnumerable<Base> bases)
{
foreach(Base b in bases)
{
Console.WriteLine(b);
}
}
}
class Derived : Base
{
public static void Main()
{
List<Derived> dlist = new List<Derived>();
Derived.PrintBases(dlist);
IEnumerable<Base> bIEnum = dlist;
}
}
List<Derived> 형식은 공변성에 의해 안전하게 IEnumerable<Derived>에서 IEnumerable<Base>로 참조될 수 있으므로 Base 클래스의 PrintBases(IEnumerable<Base> bases)를 호출할 수 있다.
반공변성 제너릭 인터페이스 예시) IComparable<T>
using System;
using System.Collections.Generic;
abstract class Shape
{
public virtual double Area { get { return 0; }}
}
class Circle : Shape
{
private double r;
public Circle(double radius) { r = radius; }
public double Radius { get { return r; }}
public override double Area { get { return Math.PI * r * r; }}
}
class ShapeAreaComparer : System.Collections.Generic.IComparer<Shape>
{
int IComparer<Shape>.Compare(Shape a, Shape b)
{
if (a == null) return b == null ? 0 : -1;
return b == null ? 1 : a.Area.CompareTo(b.Area);
}
}
class Program
{
static void Main()
{
// You can pass ShapeAreaComparer, which implements IComparer<Shape>,
// even though the constructor for SortedSet<Circle> expects
// IComparer<Circle>, because type parameter T of IComparer<T> is
// contravariant.
SortedSet<Circle> circlesByArea =
new SortedSet<Circle>(new ShapeAreaComparer())
{ new Circle(7.2), new Circle(100), null, new Circle(.01) };
foreach (Circle c in circlesByArea)
{
Console.WriteLine(c == null ? "null" : "Circle with area " + c.Area);
}
}
}
/* This code example produces the following output:
null
Circle with area 0.000314159265358979
Circle with area 162.860163162095
Circle with area 31415.9265358979
*/
Circle 파생 클래스는 Shape 기반 클래스를 상속한다. IComparer<Shape>를 구현한 ShapeAreaComparer 클래스는 Shape 타입 인자에 대해 어떤 것이 넓이가 더 큰 지 결과를 비교하는 메서드를 제공한다. 실제 넓이를 구하는 프로퍼티는 각 파생 클래스에서 재정의되었다.
Circle 인스턴스를 담는 SortedSet<Circle>를 만들어주기 위해 IComparer<Shape> 인스턴스를 넘겨주었다. 실제 컬렉션을 구성하는 타입은 Circle이 되는데, 각 인스턴스가 삽입될 때 IComparer<Shape>타입의 ShapeAreaComparer 인스턴스를 사용하여 컬렉션을 정렬한다. IComparer<Shape> 인터페이스는 매개변수 인자에 대해 Shape 타입을 받지만, Circle은 Shape 기반 타입으로 참조될 수 있으므로 반공변성이 적용되어 안전하게 ShapeAreaComparer 인스턴스를 정렬하는데 사용할 수 있게 된다.
공변성/반공변성을 사용하는 제너릭 대리자 예시) Func<T, TResult>, Action<T1, T2>
Func<T, TResult> 대리자 클래스는 각 타입에 대해 공변성과 반공변성을 제공한다. Action<T>는 반공변성을 제공하는 매개변수를 지칭한다.
Func 대리자 클래스는 마지막 인자는 항상 반환되는 변수의 타입을 지칭한다. 해당 타입은 out 키워드로 지칭되어 공변성을 제공하고 in 키워드로 지칭되는 입력 매개변수들은 반공변성을 제공한다.
다음 Func<Base, Derived>와 같은 의미를 가지는 정적 메서드가 있다.
public static Derived MyMethod(Base b)
{
return b as Derived ?? new Derived();
}
Func 대리자 클래스는 입력 매개변수에 대해 반공변성을 지원하고 반환 타입에 공변성을 지원하기 때문에 다음 예시의 대입은 모두 유효하다.
Func<Base, Derived> f1 = MyMethod;
// Contravariant paramter 타입
Func<Derived, Derived> f3 = f1;
Derived d3 = f3(new Derived());
// Covariant return type and contravariant parameter type
Func<Derived, Base> f4 = f1;
Base b4 = f4(new Derived());
제너릭이 아닌 일반 타입의 대리자에 대해서도 가변성이 적용된다.
public delegate Base NonGenericVariant(Derived d);
public static Derived MyMethod(Base b)
{
return b as Derived ?? new Derived();
}
static void Main(string[] args)
{
NonGenericVariant f6 = MyMethod;
}
일반 타입 대리자의 경우
시그니쳐가 가변성을 적용될 수 있는 것처럼 보이지만 일반 타입의 대리자는 가변성이 적용되지 않고 정확한 타입이 매칭되는 경우만 대입할 수 있다. 가변성을 적용하려면 인스턴스 생성으로 대리자를 등록할 수 있다.
public delegate Derived NonGenericMatch(Base b);
public delegate Base NonGenericVariant(Derived d);
public static Derived MyMethod(Base b)
{
return b as Derived ?? new Derived();
}
static void Main(string[] args)
{
NonGenericMatch f6 = MyMethod;
NonGenericVariant f7 = f6; // 에러: 일반 타입 대리자는 불변이다.
NonGenericVariant f8 = new NonGenericVariant(f6);
}
가변성을 지원하는 제너릭 인터페이스와 대리자 정의하기
공변성을 지원하는 타입은 주로 반환 타입에 오는 경우가 많다. out 키워드를 사용하여 해당 타입이 공변성을 가지는 타입으로 지정할 수 있으며 해당 타입은 반공변성을 지원하지 않는다.
반공변성을 지원하는 타입은 주로 입력 매개변수로 오는 경우가 많다. in 키워드를 사용하여 해당 타입이 반공변성을 가지는 타입으로 지정할 수 있으며 해당 타입은 공변성을 지원하지 않는다.
참고 :
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/covariance-contravariance/
'C# > C# 인터페이스' 카테고리의 다른 글
C# 인터페이스 vs 추상 클래스 (0) | 2022.04.09 |
---|---|
IComparable/IComparer/IEquatable : 비교와 관련된 인터페이스 (0) | 2021.11.13 |
ICollection : 컬렉션 인터페이스 (0) | 2021.11.13 |
IEnumerable / IEnumerator : 순회 가능한 컬렉션과 순회하는 방법 (0) | 2021.11.13 |
IDisposable : 비관리 리소스 해제하기 (0) | 2021.11.12 |