C#에서 공변성과 반공변성은 어려운 용어일 수 있지만, 이해하면 코드를 작성하고 이해하는 데 도움이 됩니다. 이 두 용어는 객체 간의 관계를 이해하고 코드를 더욱 유연하게 만들어줍니다.
목차
[개념]
[공변성]
공변성은 서브 타입 관계가 유지되는 경우를 의미합니다. 즉, 하위 타입의 컬렉션을 상위 타입의 컬렉션으로 대체할 수 있는 경우를 말합니다. 이를 통해 코드를 더 유연하게 만들 수 있습니다. 이를 예로 들어보겠습니다.
가령, 동물이라는 큰 개념과 강아지라는 작은 개념이 있다고 가정해봅시다. 여기서 동물은 강아지를 포함하는 상위 개념입니다. 이때, 동물의 목록에 강아지를 추가할 수 있습니다. 왜냐하면 강아지는 동물이기 때문에 동물의 목록에 강아지를 넣어도 문제가 없습니다. 이것이 공변성입니다.
List<Animal> animals = new List<Dog>();
위와 같이 강아지의 리스트를 동물로 취급하는 것과 같습니다.
[반공변성]
공변성과 반대의 개념으로, 제네릭 타입 매개변수의 서브 타입 관계가 유지되는 경우를 의미합니다. 즉, 상위 타입의 컬렉션을 하위 타입의 컬렉션으로 대체할 수 있는 경우를 말합니다.
using System;
using System.Collections.Generic;
class Animal {}
class Dog : Animal {}
class Program
{
static void Main(string[] args)
{
// 반공변성을 이용한 예시
// Animal을 받는 메서드를 Action<in T> 델리게이트에 할당할 수 있습니다.
Action<Animal> animalAction = PrintAnimal;
Action<Dog> dogAction = animalAction; // 반공변성 적용
// Dog 객체 생성
Dog dog = new Dog();
// dogAction을 호출하면 animalAction에 할당된 PrintAnimal 메서드가 실행됩니다.
dogAction(dog);
// 동물 리스트에 대한 반공변성 예시
List<Animal> animalList = new List<Animal>();
animalList.Add(new Dog());
animalList.Add(new Animal());
// Dog 리스트는 Animal 리스트로 대체될 수 있습니다.
List<Dog> dogList = new List<Dog>();
dogList.Add(new Dog());
// 반공변성이 적용되므로 동물 리스트를 받는 메서드에도 Dog 리스트를 전달할 수 있습니다.
PrintAnimals(animalList);
PrintAnimals(dogList);
}
static void PrintAnimal(Animal animal)
{
Console.WriteLine("Animal");
}
static void PrintAnimals(IEnumerable<Animal> animals)
{
foreach (var animal in animals)
{
Console.WriteLine("Animal");
}
}
}
위 코드에서는 Action<in T>
델리게이트를 사용하여 Animal을 입력으로 받는 메서드를 할당한 후, Dog 객체를 해당 델리게이트를 통해 호출했습니다. 또한 리스트에 대한 반공변성도 적용하여, Animal 리스트를 받는 메서드에 Dog 리스트를 전달할 수 있습니다.
[차이]
개념을 이해하더라도 차이점을 명확하게 이해 해야 헷갈리지 않을 수 있습니다. 이제 차이점을 쉽게 풀어서 설명해 드리겠습니다.
공변성은 구체적인 것을 더 일반적인 것으로 대체할 수 있는 것을 말합니다. 예시로 과일이라는 개념이 있고 사과라는 더 구체적인 개념이 있다면 공변성을 가진 경우 과일 리스트를 사과 리스트로 대체할 수 있습니다. 즉, 사과 리스트를 과일 리스트로 사용할 수 있는 것 입니다.
반공변성은 그와 반대로 더 일반적인 것을 구체적인 것으로 대체 할 수 있는 것을 말합니다.
즉, 공변성은 더 특수한 것을 더 일반적인 것으로 대체할 수 있께 해주는 것이고, 반공변성은 일반적인 것을 특수한 것으로 대체할 수 있게 해주는 것 입니다.
[사용 예시들]
공변성과 반공변성을 코드로 설명하는 데에는 대표적으로 C#의 제네릭 인터페이스와 델리게이트를 활용할 수 있습니다. 아래에 각각의 사용 예시를 코드로 작성해 보겠습니다.
[공변성의 사용 예시]
using System;
using System.Collections.Generic;
// 일반적인 인터페이스
interface IFruit { }
// 구체적인 인터페이스
class Apple : IFruit { }
class Program
{
static void Main(string[] args)
{
// 공변성을 활용한 예시: IEnumerable<out T>
IEnumerable<Apple> apples = new List<Apple> { new Apple(), new Apple() };
IEnumerable<IFruit> fruits = apples; // Apple 리스트를 IFruit 리스트로 대체
// 모든 과일 출력
foreach (var fruit in fruits)
{
Console.WriteLine(fruit.GetType().Name);
}
}
}
위 코드에서는 Apple 클래스가 IFruit 인터페이스를 상속하므로, IEnumerable<Apple>을 IEnumerable<IFruit>으로 대체할 수 있습니다. 따라서 Apple 리스트를 IFruit 리스트로 대체하여 출력할 수 있습니다.
[반공변성 사용 예시]
using System;
// 반공변성을 가진 델리게이트
delegate void AnimalHandler(Animal animal);
// 동물 클래스
class Animal { }
// 강아지 클래스 (동물의 서브 클래스)
class Dog : Animal { }
class Program
{
static void Main(string[] args)
{
// 반공변성을 활용한 예시: Action<in T>
Action<Animal> animalAction = HandleAnimal;
Action<Dog> dogAction = animalAction; // Dog를 처리하는 Action으로 대체
// 강아지 처리
Dog dog = new Dog();
dogAction(dog);
}
static void HandleAnimal(Animal animal)
{
Console.WriteLine("Handling animal");
}
}
위 코드에서는 AnimalHandler 델리게이트가 Animal을 매개변수로 받으므로, Action<Animal>을 Action<Dog>으로 대체할 수 있습니다. 이를 통해 강아지를 처리하는 Action으로 대체하여 강아지를 처리할 수 있습니다.
[장점]
저는 게임 개발자이기에 더 이해가 쉽도록 게임 개발로 예시를 들어보겠습니다.
[공변성의 장점]
게임에서 여러 유닛이 있는데, 모든 유닛은 “유닛”이라는 상위 클래스를 상속받고 있습니다. 각 유닛은 다양한 기능과 특성을 가지고 있습니다.
- 코드 간소화: 모든 유닛을 단일한 리스트로 관리하고 싶을 때, List<Unit>으로 모든 유닛을 다룰 수 있습니다. 이로써 코드를 간단하게 유지할 수 있습니다.
- 유연한 설계: 새로운 유닛이 추가되더라도 코드 수정 없이 간단히 List<Unit>에 추가할 수 있습니다. 새로운 유닛이 추가되어도 이전 코드에 영향을 미치지 않습니다.
즉, 유닛 밑에 펫, 메인 캐릭터 등이 자식으로 존재한다면 유닛으로 모두 취급할 수 있다는 것 입니다.
[반공변성의 장점]
게임에서 적들을 관리하는 기능이 있는데, 모든 적들은 “적”이라는 인터페이스를 구현하고 있습니다. 그런데 특정 유닛만 관리해야 할 때가 있습니다.
- 코드 재사용: 모든 적을 처리하는 기능은 한 번에 구현되어 있지만, 특정 유닛만 처리해야 할 때도 있습니다. 이때 적들을 처리하는 기능을 사용하면서도 특정 유닛만 선택적으로 처리할 수 있습니다.
- 유연한 확장성: 새로운 유닛이 추가되더라도 적들을 처리하는 기능을 수정할 필요 없이 기존 코드를 재활용할 수 있습니다. 이로써 새로운 유닛을 추가하더라도 이전 코드에 영향을 미치지 않습니다.
[마무리 말]
반공변성과 공변성은 제네릭 타입과 델리게이트를 보다 유연하게 다룰 수 있게 해주는 중요한 개념입니다. 이를 통해 코드의 재사용성과 가독성을 높일 수 있습니다. 공변성은 더 구체적인 것을 더 일반적인 것으로 대체할 수 있게 하고, 반공변성은 더 일반적인 것을 더 구체적인 것으로 대체할 수 있게 합니다. 따라서 이러한 개념을 잘 활용하여 더욱 유연하고 효율적인 코드를 작성할 수 있습니다. 단어 자체가 생소하고 어렵게 느껴지지만 생각보다 간단한 개념이기에 잘 이해하셨다고 사용하시길 바랍니다.