C#

C# 가변성,공변성, 반공변성, 불공변성

kark 2024. 7. 21. 14:05
728x90

공변성과 반공변성은 제네릭 타입 시스템에 타입의 상속 관계에 따라 제네릭 타입변환을 허용하는 특성을 의미한다.

 

string[] strArr = new string[5];
object[] objArr = strArr;

위의 코드처럼 형변환이 가능한 성질을 의미한다.

 

.Net 4.0이전 버전에서 IEnumerable 인터페이스 와 그 이후의 인터페이스를 살펴보자

public interface IEnumerable<T> : IEnumerable // .Net 4.0 이전

public interface IEnumerable<out T> : IEnumerable // 그 이후버전

 

 

위의 코드를 보면 4.0 이후의 버전에서 갑자기 out 키워드가 추가되었다.

 

IEnumerable<string> Ilist = new List<string>();
IEnumerable<object> objs = Ilist;

위의 코드는 하위클래스 타입에서 상위클래스 타입 IEnumerable 객체로 형변환이 가능하지만

.Net 4.0 버전에선 이 기능이 동작하지 않는다. ( 제네릭 타입을 지정하게 되면 다른 형변환이 불가 )

 

즉 out 키워드는 이러한 형변환을 가능하게 해준다는 기능이 있음을 알 수 있다.

 

위의 제네릭 타입을 그저 <T> 라고 지정한경우 형변환을 할 수 없다는 의미로 불변성, 불공변성이라 한다.

 

하위클래스를 상위클래스로, 상위클래스를 하위클래스로 변하는 성질을 가변성을 갖는다 라고 할수 있겠다.

주로 Array, Delegate, Generics 에 적용되는 개념이다.

 

공변성

 

out 키워드를 사용하여

제네릭 타입의 형식 매개변수가 정의된 것보다 더 구체적인 형식으로 변환될 수 있음을 의미한다.

 

다음처럼 상속관계를 갖는 코드를 작성해봤다.

    public class Vehicle
    {
        public void Ride() { } 
    }

    public class Car : Vehicle
    {
        public void Accel() { } 
    }

    public class Bus : Car
    {
        public void Multi() { } 
    }

 

 

공변성을 확인하기 위해 인터페이스의 제네릭 타입앞에 out 키워드를 붙여줬다.

public interface ICreator<out T>
{
    T Create();
}

public class VFactory : ICreator<Vehicle>
{
    public Vehicle Create() { return new Vehicle(); }
}

public class CarFactory : ICreator<Car>
{
    public Car Create() { return new Car(); }
}

public class BusFactory : ICreator<Bus>
{
    public Bus Create() { return new Bus(); }
}

 

ICreator<Car> carFactory = new CarFactory();
ICreator<Vehicle> vehicleFactory = carFactory; // 허용
vehicleFactory.Create().Ride(); // 허용

ICreator<Bus> busFatory = carFactory; // 불가
busFatory.Create();  // 불가

 

매개변수에서 사용되는 out 키워드 처럼 출력, 리턴전용의 특성이 똑같이 반영된다.

out T 는 T를 출력, 반환용도로 사용하겠다 라고 약속을 미리 정해둔다.

 

CarFactory 는 Car를 반환하기로 되어있지만, 제네릭타입을 Vehicle로 변경이 허용된다.

즉 Car를 반환하는 형태에서 Vehicle을 반환하는 형태로..

 

Car 클래스는 이미 Vehicle을 상속받고 있으며 Vehicle의 모든 멤버를 물려받고 있어

반환시 Vehicle로 접근해도 문제가 없으므로 허용되는 문법이 된다.

 

반대로 Car를 반환하는 형태에서 Bus를 반환하려한다 라면

Car 클래스는 Bus 클래스가 어떤 멤버를 갖고있는지 알 수 없기에 이는 허용되지 않게된다.

 

이렇게 공변성은 제네릭 타입을 out 키워드를 사용해 보다 일반적인 클래스로 형변환이 가능하게 된다.

 

반 공변성

 

public interface ICollector<in T>
{
    void Add(T element);
}

public class VCollector : ICollector<Vehicle>
{
    public void Add(Vehicle v) { } 
}

public class CarCollector : ICollector<Car>
{
    public void Add(Car v) { } 
}

public class BusCollector : ICollector<Bus>
{
    public void Add(Bus v) { } 
}

 

공변성과 반대로 in 키워드를 사용하며

out 키워드 출력 과는 달리 in 키워드는 입력, 매개변수 전용으로 활용된다.

 

ICollector<Car> carCollector = new CarCollector();
ICollector<Vehicle> vehicleCollector = carCollector; // 비허용
carCollector.Add(new Vehicle()); // 비허용

ICollector<Bus> busCollector = carCollector; // 허용
carCollector.Add(new Bus()); // 허용

 

CarCollector 객체는 Car 객체를 받아들이게 정의되어있다.

VehicleCollector를 CarCollector로 형변환하여 Vehicle 객체를 매개변수로 전달하려 시도하지만

Vehicle 객체는 Car 객체보다 더 일반적인 타입이므로 사용할 수 없게된다.

 

타입의 안정성을 보장하기 위해 컴파일러가 에러를 발생시킨다.

 

반대로 하위클래스를 받아들이게 될 경우 타입 안정성을 유지할 수 있으므로 이는 허용되는 문법이 된다.

 

 

 

 

뭔가 반공변성은 설명이 애매하다....