본문 바로가기
Programming/C#

C# 제네릭 (Generics) 타입의 유연성과 안전성을 동시에

by Dev_카페인 2024. 6. 9.
반응형

[C#] 제네릭 (Generics) : 타입의 유연성과 안전성을 동시에

안녕하세요! 이번 포스트에서는 C#의 제네릭(Generics)에 대해 알아보겠습니다. 제네릭은 코드의 유연성과 타입 안전성을 동시에 제공하는 강력한 기능입니다. 이를 통해 재사용 가능하고, 안전하며, 효율적인 코드를 작성할 수 있습니다.

제네릭(Generics)이란 무엇인가요?

제네릭은 클래스, 구조체, 인터페이스, 메서드 등을 정의할 때, 데이터 타입을 일반화하는 기법입니다. 제네릭을 사용하면 데이터 타입에 의존하지 않는 코드를 작성할 수 있어, 다양한 데이터 타입을 처리할 수 있는 유연한 구조를 만들 수 있습니다.

제네릭의 장점

  1. 타입 안전성: 컴파일 시점에 타입을 체크하여 타입 관련 오류를 줄입니다.
  2. 코드 재사용성: 데이터 타입에 상관없이 동작하는 코드를 작성할 수 있어, 중복 코드를 줄이고 재사용성을 높입니다.
  3. 성능 향상: 박싱과 언박싱을 피하여 성능을 향상시킵니다.

제네릭 클래스

제네릭 클래스를 정의하려면 클래스 이름 뒤에 타입 매개변수를 추가합니다. 일반적으로 타입 매개변수는 T로 표시합니다.

제네릭 클래스 예시

public class GenericList<T>
{
    private T[] items;
    private int count;

    public GenericList(int capacity)
    {
        items = new T[capacity];
        count = 0;
    }

    public void Add(T item)
    {
        if (count < items.Length)
        {
            items[count++] = item;
        }
        else
        {
            Console.WriteLine("List is full");
        }
    }

    public T Get(int index)
    {
        if (index >= 0 && index < count)
        {
            return items[index];
        }
        else
        {
            throw new IndexOutOfRangeException();
        }
    }
}

위 예시에서 GenericList<T> 클래스는 제네릭 타입 T를 사용하여 다양한 타입의 리스트를 만들 수 있습니다.

제네릭 메서드

제네릭 메서드는 메서드 이름 뒤에 타입 매개변수를 추가하여 정의합니다. 이를 통해 메서드 내부에서 다양한 타입을 처리할 수 있습니다.

제네릭 메서드 예시

public class GenericMethods
{
    public void Swap<T>(ref T a, ref T b)
    {
        T temp = a;
        a = b;
        b = temp;
    }
}

class Program
{
    static void Main(string[] args)
    {
        GenericMethods methods = new GenericMethods();

        int x = 1, y = 2;
        methods.Swap(ref x, ref y);
        Console.WriteLine($"x: {x}, y: {y}"); // x: 2, y: 1

        string str1 = "Hello", str2 = "World";
        methods.Swap(ref str1, ref str2);
        Console.WriteLine($"str1: {str1}, str2: {str2}"); // str1: World, str2: Hello
    }
}

위 예시에서 Swap<T> 메서드는 제네릭 타입 T를 사용하여 두 변수의 값을 교환합니다. 메서드를 호출할 때 타입을 지정할 필요가 없으며, 컴파일러가 인수의 타입을 자동으로 추론합니다.

제네릭 인터페이스

제네릭 인터페이스는 인터페이스 이름 뒤에 타입 매개변수를 추가하여 정의합니다. 이를 통해 다양한 타입을 처리할 수 있는 인터페이스를 만들 수 있습니다.

제네릭 인터페이스 예시

public interface IRepository<T>
{
    void Add(T item);
    T Get(int id);
}

public class GenericRepository<T> : IRepository<T>
{
    private Dictionary<int, T> storage = new Dictionary<int, T>();

    public void Add(T item)
    {
        int id = storage.Count + 1;
        storage.Add(id, item);
    }

    public T Get(int id)
    {
        if (storage.ContainsKey(id))
        {
            return storage[id];
        }
        else
        {
            throw new KeyNotFoundException();
        }
    }
}

위 예시에서 IRepository<T> 인터페이스는 제네릭 타입 T를 사용하여 다양한 타입의 저장소를 정의할 수 있습니다. GenericRepository<T> 클래스는 이 인터페이스를 구현합니다.

제네릭 제약 조건

제네릭 타입 매개변수에 제약 조건을 추가하여 특정 타입만 허용할 수 있습니다. 제약 조건은 where 키워드를 사용하여 정의합니다.

제네릭 제약 조건 예시

public class GenericConstraints
{
    public void Display<T>(T item) where T : IComparable<T>
    {
        Console.WriteLine(item);
    }
}

class Program
{
    static void Main(string[] args)
    {
        GenericConstraints constraints = new GenericConstraints();

        constraints.Display(123); // 123 출력
        constraints.Display("Hello"); // Hello 출력
    }
}

위 예시에서 Display<T> 메서드는 제네릭 타입 T가 IComparable<T> 인터페이스를 구현해야 한다는 제약 조건을 가집니다. 이를 통해 T 타입이 비교 가능한 타입임을 보장합니다.

공변성과 반공변성

제네릭은 공변성(Covariance)과 반공변성(Contravariance)을 지원합니다. 이는 주로 제네릭 인터페이스와 델리게이트에서 사용됩니다.

  • 공변성: 제네릭 타입이 특정 타입의 하위 타입을 허용합니다. out 키워드를 사용하여 정의됩니다.
  • 반공변성: 제네릭 타입이 특정 타입의 상위 타입을 허용합니다. in 키워드를 사용하여 정의됩니다.

공변성과 반공변성 예시

public interface ICovariant<out T>
{
    T GetItem();
}

public interface IContravariant<in T>
{
    void SetItem(T item);
}

위 예시에서 ICovariant<out T> 인터페이스는 공변성을, IContravariant<in T> 인터페이스는 반공변성을 지원합니다.

 

공변성(Covariance)이란 무엇인가요?

공변성은 하위 타입 관계를 유지하면서 객체 간의 변환을 허용하는 것을 말합니다. C#에서는 주로 배열과 제네릭 인터페이스에서 사용됩니다. 공변성은 out 키워드를 사용하여 정의됩니다.

공변성 예시: 배열

class Animal { }
class Dog : Animal { }

class Program
{
    static void Main(string[] args)
    {
        Dog[] dogs = new Dog[] { new Dog(), new Dog() };
        Animal[] animals = dogs; // 배열 공변성

        foreach (var animal in animals)
        {
            Console.WriteLine(animal.GetType().Name);
        }
    }
}

위 예시에서 Dog 배열을 Animal 배열에 할당할 수 있습니다. 이는 공변성을 통해 하위 타입 관계를 유지하면서 배열 간의 변환을 허용하는 것입니다.

반공변성(Contravariance)이란 무엇인가요?

반공변성은 상위 타입 관계를 유지하면서 객체 간의 변환을 허용하는 것을 말합니다. C#에서는 주로 제네릭 델리게이트에서 사용됩니다. 반공변성은 in 키워드를 사용하여 정의됩니다.

반공변성 예시: 델리게이트

delegate void Action<in T>(T obj);

class Program
{
    static void Main(string[] args)
    {
        Action<Animal> animalAction = (Animal animal) =>
        {
            Console.WriteLine(animal.GetType().Name);
        };

        Action<Dog> dogAction = animalAction; // 델리게이트 반공변성

        dogAction(new Dog());
    }
}

위 예시에서 Action<Animal> 델리게이트를 Action<Dog> 델리게이트에 할당할 수 있습니다. 이는 반공변성을 통해 상위 타입 관계를 유지하면서 델리게이트 간의 변환을 허용하는 것입니다.

공변성과 반공변성의 사용 예시

공변성 예시: 배열

공변성은 배열과 같은 컬렉션에서 주로 사용됩니다. 하위 타입의 배열이 상위 타입의 배열로 취급될 수 있습니다.

Dog[] dogs = new Dog[] { new Dog(), new Dog() };
Animal[] animals = dogs; // 배열 공변성

foreach (var animal in animals)
{
    Console.WriteLine(animal.GetType().Name);
}

반공변성 예시: 델리게이트

반공변성은 델리게이트와 같은 대리자 타입에서 주로 사용됩니다. 상위 타입의 델리게이트가 하위 타입의 델리게이트로 취급될 수 있습니다.

Action<Animal> animalAction = (Animal animal) =>
{
    Console.WriteLine(animal.GetType().Name);
};

Action<Dog> dogAction = animalAction; // 델리게이트 반공변성

dogAction(new Dog());

공변성과 반공변성의 주의사항

  • 공변성과 반공변성은 제네릭 타입 매개변수를 가진 인터페이스나 델리게이트에서만 사용할 수 있습니다.
  • 공변성과 반공변성은 배열이나 대리자와 같은 특정 타입에 대해서만 가능합니다. 모든 경우에 적용되는 것은 아닙니다.
  • 공변성은 출력 매개변수(리턴 타입)에 사용되고, 반공변성은 입력 매개변수에 사용됩니다.

마치며

이번 포스트에서는 C#의 제네릭에 대해 알아보았습니다. 제네릭은 코드의 유연성과 타입 안전성을 동시에 제공하여, 재사용 가능하고 효율적인 코드를 작성할 수 있게 합니다. 제네릭 클래스, 메서드, 인터페이스를 통해 다양한 데이터 타입을 처리할 수 있으며, 제네릭 제약 조건을 사용하여 타입 안전성을 강화할 수 있습니다. 추가적인 질문이나 궁금한 점이 있다면 언제든지 댓글로 남겨주세요!

반응형