Code/C#

C# Generic: 서로 다른 타입 인수를 가진 제네릭 타입을 한 컬렉션에 담기

Segel 2023. 1. 24. 19:00

얼마 전 공부 중인 지인을 돕다가 예전의 내 경우와 똑같은 문제로 고민하는 것을 보았다.
서로 다른 타입 인수를 가진 제네릭 타입을 하나의 컬렉션에 담고 싶다는 것이다.

문제

어떤 타입이든 상관 없이 하나로 묶고 싶다고 생각한다면 object를 담는 컬렉션를 사용하는 방법을 떠올리기 쉽다.
object로 할 수 있는 것은 Equals()나 ToString() 호출 정도이니 원하는 타입으로 캐스팅하여 사용하게 될 것이다.

캐스팅을 이용한 방법에는 크게 두 가지가 있다:

  • 필요한 곳에서 필요한 대로 적당히 캐스팅해서 사용
    언제 어디서 예외가 발생할 지 모르며 너무 즉흥적이라 작성한 본인도 의도를 잊어버릴 수 있다.

  • Type 타입의 객체를 저장해 뒀다가 캐스팅하거나 비교해서 사용
    장황하고 짜증나는 방식이다. 제네릭이 있는 언어에서 이런 짓을 해야 할까?

또한 object 타입 변수에 값 타입 인스턴스를 넣고 캐스팅해 사용한다면 박싱/언박싱이 필요하기 때문에 성능에도 지장이 있다.


해결

그 전에 하나의 컬렉션에 담으려고 하는 이유가 무엇인지에 대해 생각해 볼 필요가 있다.

  • 왜 하나로 묶고 싶은가?
    함수에 인자로 전달하거나 메서드를 호출하거나 하는 등, 내부의 원소들에 공통된 동작을 수행하기 위해서이다.

  • 공통된 동작을 표현하기 위해서 필요한 것이 무엇일까?
    인터페이스이다.

결론: 제네릭 클래스 컬렉션에서 필요한 동작을 담은 논제네릭 인터페이스를 만들고, 그것을 컬렉션에 담으면 되겠다.


예시

using System;
using System.Collections.Generic;

var records = new List<NamedRecord>();

var intRecord = new Record<int>(Guid.NewGuid(), "int", 42);
var boolRecord = new Record<bool>(Guid.NewGuid(), "bool", true);

records.Add(intRecord);
records.Add(boolRecord);

foreach (var record in records)
{
    Console.WriteLine($"id: {record.GetID()}, name: {record.Name}");
}

public interface NamedRecord
{
    public string Name { get; }

    public Guid GetID();
}

public class Record<T> : NamedRecord
{
    public string Name { get; }

    public Guid GetID() => _id;

    public T Value { get; }

    private readonly Guid _id;

    public Record(Guid id, string name, T value) => (_id, Name, Value) = (id, name, value);
}
반응형