로그인게임하기
Unity C# 강좌

[Unity C# 2] 제네릭과 typeof, 타입을 나중에 정하기

2026.10.14

Unity C# 강좌 · 2편

같은 일을 하는 함수를 타입마다 따로 만들 필요는 없습니다. 제네릭은 "어떤 타입인지는 쓰는 쪽이 정한다" 고 미뤄 두는 문법입니다. 이번 편에서는 목록을 잘라 내는 함수, 날짜를 찍는 함수, 타입마다 따로 세는 정적 클래스를 만들며 제네릭과 typeof 를 익힙니다.

수업 정보

2024년 부산 콘텐츠 아카데미 퍼스널 트레이닝 수업에서 MyLesson Unity 실습 프로젝트의 Strategy_02_Generic.cs · Strategy_03_Typeof.cs 로 가르친 내용입니다. 수업은 다섯 단계로 나뉘고, 단계마다 코드가 조금씩 바뀝니다.

PROBLEM

타입만 다른 함수가 늘어난다

오크 세 마리 중 앞의 두 마리만 골라 내는 함수를 만든다고 해 봅시다. List<GameObject> 를 받아 List<GameObject> 를 돌려주면 됩니다. 그런데 다음에는 이름 목록에서 두 개, 그다음에는 점수 목록에서 세 개를 골라야 합니다. 하는 일은 같고 타입만 다릅니다.

타입마다 만들면

  • CopyFirstOrcs(List<GameObject>)
  • CopyFirstNames(List<string>)
  • CopyFirstScores(List<int>)
  • 고칠 일이 생기면 세 곳을 고쳐야 한다

제네릭으로 만들면

  • CopyAtMost<T>(List<T>)
  • T 는 부르는 쪽이 넘긴 목록의 타입으로 정해진다
  • 함수는 하나, 고칠 곳도 하나
GENERIC STEP 1

타입 매개변수 T

Strategy_02_Generic.csAssets/Source/Strategy_02_Generic.cs8–19줄
private static List<T> CopyAtMost<T>(List<T> input, int maxElements)
{
    int actualCount = Math.Min(input.Count, maxElements);

    List<T> result = new List<T>(actualCount);
    for (int i = 0; i < actualCount; i++)
    {
        result.Add(input[i]);
    }

    return result;
}

CopyAtMost<T> 의 <T> 가 타입 매개변수입니다. 함수 이름 뒤에 "여기 들어올 타입을 T 라고 부르자" 고 선언한 것입니다. 함수 안에서는 T 를 진짜 타입처럼 씁니다. new List<T>(actualCount) 처럼요.

Strategy_02_Generic.csAssets/Source/Strategy_02_Generic.cs35–50줄
List<GameObject> orcs = new List<GameObject>();
orcs.Add(GameObject.Instantiate(Resources.Load<GameObject>("Character/Orc")));
orcs.Add(GameObject.Instantiate(Resources.Load<GameObject>("Character/Orc")));
orcs.Add(GameObject.Instantiate(Resources.Load<GameObject>("Character/Orc")));

foreach (GameObject orc in orcs)
{
    orc.transform.SetParent(Program.FindObject("/Field").transform);
}

orcs[0].transform.SetLocalPositionAndRotation(new Vector3(-3.55f, 0, 4.91f), Quaternion.identity);
orcs[1].transform.SetLocalPositionAndRotation(new Vector3(-3.55f, 0, 6.67f), Quaternion.identity);
orcs[2].transform.SetLocalPositionAndRotation(new Vector3(-2.51f, 0, 4.04f), Quaternion.identity);

List<GameObject> firstTwo = CopyAtMost(orcs, 2);
Program.WriteList(firstTwo);

부르는 쪽에서는 CopyAtMost(orcs, 2) 라고만 씁니다. orcs 가 List<GameObject> 이니 컴파일러가 T 를 GameObject 로 알아서 정합니다(타입 추론). CopyAtMost<GameObject>(orcs, 2) 라고 적어도 같은 뜻입니다.

박싱이 없다

1편의 ArrayList 는 무엇이든 object 로 담았습니다. List<T> 와 CopyAtMost<T> 는 T 가 정해지면 그 타입 그대로 담고 꺼냅니다. 형 변환도 필요 없고, 다른 타입이 섞이면 컴파일러가 바로 막아 줍니다.

GENERIC STEP 2

where 로 T 에게 조건 걸기

이번에는 날짜 목록을 받아, 나라마다 다르지 않은 형식(InvariantCulture)으로 찍어 봅니다.

Strategy_02_Generic.cs수업 자료 2단계21–37줄
private static void PrintFormatableItems<T>(List<T> items) where T : IFormattable
{
    CultureInfo culture = CultureInfo.InvariantCulture;
    foreach (T item in items)
    {
        Program.WriteLine(item.ToString(null, culture));
    }
}

public void Run()
{
    List<DateTime> items = new List<DateTime>();
    items.Add(new DateTime(2024, 08, 1));
    items.Add(new DateTime(2024, 08, 3));

    PrintFormatableItems(items);
}

T 가 아무 타입이나 될 수 있으면, 함수 안에서 item.ToString(null, culture) 를 부를 수 없습니다. 모든 타입에 그런 메서드가 있는 건 아니니까요. where T : IFormattable 이 "T 는 IFormattable 을 구현한 타입만 된다" 는 조건입니다. 조건을 걸면 함수 안에서 IFormattable 의 메서드를 쓸 수 있고, 조건에 맞지 않는 타입을 넘기면 컴파일 오류가 납니다.

실행 결과게임 화면의 출력 창
08/01/2024 00:00:00
08/03/2024 00:00:00
조건뜻예
where T : IFormattable그 인터페이스를 구현한 타입DateTime, int, float
where T : Component그 클래스이거나 자식 클래스Rigidbody, Button
where T : IEnumerableforeach 로 돌 수 있는 타입배열, List (1편의 WriteList)
where T : class · struct참조 타입만 · 값 타입만string · int
GENERIC STEP 3

default, 타입마다 다른 빈 값

제네릭 함수 안에서 "T 의 빈 값" 이 필요할 때가 있습니다. T 가 int 면 0, string 이면 null 입니다. 이럴 때 쓰는 것이 default 입니다.

Strategy_02_Generic.cs수업 자료 3단계30–52줄
public void Run()
{
    DateTime dt = default;
    Program.WriteLine("DateTime(defult)=" + dt);

    bool flag = default;
    Program.WriteLine("bool(defult)=" + flag);

    int i = default;
    Program.WriteLine("int(defult)=" + i);

    string str = default;
    if (str != null)
        Program.WriteLine("string(defult)=" + str);
    else
        Program.WriteLine("string(defult) is null");

    CultureInfo ci = default;
    if (ci != null)
        Program.WriteLine("CultureInfo(defult)=" + ci);
    else
        Program.WriteLine("CultureInfo(defult) is null");
}
타입default 값이유
int0값 타입은 0 으로 채운 값
boolfalse값 타입
DateTime0001년 1월 1일 0시값 타입(struct)이라 가장 이른 날짜
string, CultureInfonull참조 타입은 가리키는 곳이 없음

값 타입은 기본값이 있고 참조 타입은 null 이라는 차이가 다음 편 박싱과 Nullable 의 출발점입니다.

TYPEOF

typeof, 타입을 값처럼 꺼내기

typeof(T) 는 타입 자체를 System.Type 객체로 꺼냅니다. 제네릭 함수 안에서 "지금 T 가 무엇인지" 를 확인할 수 있습니다.

Strategy_03_Typeof.cs수업 자료 1단계6–18줄
private static void PrintType<T>(string title)
{
    Program.WriteLine(title);
    Program.WriteLine("typeof(T) = {0}", typeof(T));
    Program.WriteLine("typeof(List<T>) = {0}", typeof(List<T>));
}

public void Run()
{
    PrintType<string>("string type result");
    Program.WriteLine("");
    PrintType<int>("int type result");
}
실행 결과게임 화면의 출력 창
string type result
typeof(T) = System.String
typeof(List<T>) = System.Collections.Generic.List`1[System.String]

int type result
typeof(T) = System.Int32
typeof(List<T>) = System.Collections.Generic.List`1[System.Int32]

C# 의 string 과 int 는 .NET 의 System.String, System.Int32 의 별명입니다. List`1 의 `1 은 "타입 매개변수가 하나인 제네릭" 이라는 표시입니다.

1편과 이어지는 곳

1편의 Type.GetType("Strategy_01_Array") 도 Type 객체를 돌려줍니다. typeof 는 컴파일할 때 아는 타입에서, Type.GetType 은 실행 중에 문자열에서 같은 Type 객체를 얻는 방법입니다.

TYPEOF STEP 2

제네릭 정적 클래스는 T 마다 따로 있다

Strategy_03_Typeof.cs수업 자료 2단계13–42줄
class GenericStaticClass<T>
{
    private static int value;

    static GenericStaticClass()
    {
        Program.WriteLine("Static Class {0}가 생성되었습니다.", typeof(T));
    }

    public static void Plus()
    {
        value++;
    }

    public static void PrintCount()
    {
        Program.WriteLine("Count of {0}: {1}", typeof(T), value);
    }
}

public void Run()
{
    GenericStaticClass<string>.Plus(); // static class 생성
    GenericStaticClass<string>.Plus();
    GenericStaticClass<string>.PrintCount();
    Program.WriteLine();
    GenericStaticClass<int>.PrintCount(); // static class 생성
    GenericStaticClass<int>.Plus();
    GenericStaticClass<int>.PrintCount();
}
실행 결과게임 화면의 출력 창
Static Class System.String가 생성되었습니다.
Count of System.String: 2

Static Class System.Int32가 생성되었습니다.
Count of System.Int32: 0
Count of System.Int32: 1

GenericStaticClass<string> 과 GenericStaticClass<int> 는 이름이 비슷할 뿐 서로 다른 클래스입니다. 그래서 정적 필드 value 도 따로 있고, 정적 생성자도 T 마다 처음 쓰일 때 한 번씩 불립니다. string 쪽을 두 번 더해도 int 쪽은 0 에서 시작합니다.

GenericStaticClass<string>value = 2 · 정적 생성자 1회
GenericStaticClass<int>value = 1 · 정적 생성자 1회
GenericStaticClass<float>아직 쓰이지 않아 만들어지지 않음

이 성질은 "타입마다 하나씩 있는 저장소" 를 만들 때 쓸모가 있습니다. 타입별 캐시나 타입별 개수 세기가 그런 예입니다.

RETRO

돌아보며

이번 편에서 얻은 것

  • T 는 부르는 쪽이 정하고, 컴파일러가 대신 추론한다
  • where 로 T 에게 조건을 걸면 함수 안에서 그 기능을 쓸 수 있다
  • typeof 로 타입을 값처럼 꺼내고, 제네릭 정적 클래스는 T 마다 따로 있다

다음에 풀 질문

  • 값 타입과 참조 타입은 함수에 넘길 때 무엇이 다른가
  • int 에 null 을 넣고 싶을 때는 어떻게 하나

정리

  1. 제네릭은 타입을 나중에 정하는 문법입니다. CopyAtMost<T> 하나로 오크 목록도, 이름 목록도 처리합니다.
  2. where 는 T 에게 거는 조건입니다. 조건을 걸어야 함수 안에서 그 타입의 기능을 쓸 수 있고, 맞지 않는 타입은 컴파일러가 막습니다.
  3. typeof 는 타입을 값으로 꺼냅니다. 제네릭 정적 클래스는 T 마다 따로 만들어지고, 정적 필드도 따로 가집니다.

다음 편에서는 값 타입과 참조 타입, 박싱과 언박싱, 그리고 null 을 담을 수 있는 int? 를 봅니다.

← 개발 노트 목록