같은 일을 하는 함수를 타입마다 따로 만들 필요는 없습니다. 제네릭은 "어떤 타입인지는 쓰는 쪽이 정한다" 고 미뤄 두는 문법입니다. 이번 편에서는 목록을 잘라 내는 함수, 날짜를 찍는 함수, 타입마다 따로 세는 정적 클래스를 만들며 제네릭과 typeof 를 익힙니다.
2024년 부산 콘텐츠 아카데미 퍼스널 트레이닝 수업에서 MyLesson Unity 실습 프로젝트의 Strategy_02_Generic.cs · Strategy_03_Typeof.cs 로 가르친 내용입니다. 수업은 다섯 단계로 나뉘고, 단계마다 코드가 조금씩 바뀝니다.
타입만 다른 함수가 늘어난다
오크 세 마리 중 앞의 두 마리만 골라 내는 함수를 만든다고 해 봅시다. List<GameObject> 를 받아 List<GameObject> 를 돌려주면 됩니다. 그런데 다음에는 이름 목록에서 두 개, 그다음에는 점수 목록에서 세 개를 골라야 합니다. 하는 일은 같고 타입만 다릅니다.
타입마다 만들면
- CopyFirstOrcs(List<GameObject>)
- CopyFirstNames(List<string>)
- CopyFirstScores(List<int>)
- 고칠 일이 생기면 세 곳을 고쳐야 한다
제네릭으로 만들면
- CopyAtMost<T>(List<T>)
- T 는 부르는 쪽이 넘긴 목록의 타입으로 정해진다
- 함수는 하나, 고칠 곳도 하나
타입 매개변수 T
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) 처럼요.
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 가 정해지면 그 타입 그대로 담고 꺼냅니다. 형 변환도 필요 없고, 다른 타입이 섞이면 컴파일러가 바로 막아 줍니다.
where 로 T 에게 조건 걸기
이번에는 날짜 목록을 받아, 나라마다 다르지 않은 형식(InvariantCulture)으로 찍어 봅니다.
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 : IEnumerable | foreach 로 돌 수 있는 타입 | 배열, List (1편의 WriteList) |
where T : class · struct | 참조 타입만 · 값 타입만 | string · int |
default, 타입마다 다른 빈 값
제네릭 함수 안에서 "T 의 빈 값" 이 필요할 때가 있습니다. T 가 int 면 0, string 이면 null 입니다. 이럴 때 쓰는 것이 default 입니다.
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 값 | 이유 |
|---|---|---|
int | 0 | 값 타입은 0 으로 채운 값 |
bool | false | 값 타입 |
DateTime | 0001년 1월 1일 0시 | 값 타입(struct)이라 가장 이른 날짜 |
string, CultureInfo | null | 참조 타입은 가리키는 곳이 없음 |
값 타입은 기본값이 있고 참조 타입은 null 이라는 차이가 다음 편 박싱과 Nullable 의 출발점입니다.
typeof, 타입을 값처럼 꺼내기
typeof(T) 는 타입 자체를 System.Type 객체로 꺼냅니다. 제네릭 함수 안에서 "지금 T 가 무엇인지" 를 확인할 수 있습니다.
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편의 Type.GetType("Strategy_01_Array") 도 Type 객체를 돌려줍니다. typeof 는 컴파일할 때 아는 타입에서, Type.GetType 은 실행 중에 문자열에서 같은 Type 객체를 얻는 방법입니다.
제네릭 정적 클래스는 T 마다 따로 있다
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: 1GenericStaticClass<string> 과 GenericStaticClass<int> 는 이름이 비슷할 뿐 서로 다른 클래스입니다. 그래서 정적 필드 value 도 따로 있고, 정적 생성자도 T 마다 처음 쓰일 때 한 번씩 불립니다. string 쪽을 두 번 더해도 int 쪽은 0 에서 시작합니다.
이 성질은 "타입마다 하나씩 있는 저장소" 를 만들 때 쓸모가 있습니다. 타입별 캐시나 타입별 개수 세기가 그런 예입니다.
돌아보며
이번 편에서 얻은 것
- T 는 부르는 쪽이 정하고, 컴파일러가 대신 추론한다
- where 로 T 에게 조건을 걸면 함수 안에서 그 기능을 쓸 수 있다
- typeof 로 타입을 값처럼 꺼내고, 제네릭 정적 클래스는 T 마다 따로 있다
다음에 풀 질문
- 값 타입과 참조 타입은 함수에 넘길 때 무엇이 다른가
- int 에 null 을 넣고 싶을 때는 어떻게 하나
정리
- 제네릭은 타입을 나중에 정하는 문법입니다. CopyAtMost<T> 하나로 오크 목록도, 이름 목록도 처리합니다.
- where 는 T 에게 거는 조건입니다. 조건을 걸어야 함수 안에서 그 타입의 기능을 쓸 수 있고, 맞지 않는 타입은 컴파일러가 막습니다.
- typeof 는 타입을 값으로 꺼냅니다. 제네릭 정적 클래스는 T 마다 따로 만들어지고, 정적 필드도 따로 가집니다.
다음 편에서는 값 타입과 참조 타입, 박싱과 언박싱, 그리고 null 을 담을 수 있는 int? 를 봅니다.