제네릭(Generics)은 C# 2.0에서 도입된 기능으로, 이 언어에서 가장 중요한 개념 중 하나로 꼽힙니다. 제네릭을 사용하면 컴파일 시점에 타입 안전성을 보장하면서 재사용 가능하고 고성능인 코드를 작성할 수 있습니다. 즉, 코드를 작성하는 시점에 타입을 미리 알지 못해도 나중에 해당 타입을 유연하게 사용할 수 있습니다.
제네릭은 .NET 전반에서 폭넓게 활용됩니다. 컬렉션, 델리게이트, 비동기 코드 등이 대표적인 예입니다. 제네릭을 사용하면 컬렉션의 크기를 미리 알 필요가 없으며, 직접 정의한 사용자 지정 데이터 타입을 포함한 어떤 요소 타입과도 함께 사용할 수 있습니다. C#은 제네릭 형식(클래스, 인터페이스 등)과 제네릭 메서드를 모두 지원합니다.
제네릭에는 타입 매개변수(type parameter)와 타입 인수(type argument)라는 개념이 있습니다. 이는 메서드가 매개변수를 선언하고, 호출 시 인수를 전달하는 방식과 매우 유사합니다.
제네릭 형식(Generic Types)
제네릭 형식을 선언하는 문법은 형식 이름 뒤에 꺾쇠괄호(<>)로 타입 매개변수를 표기하는 것입니다. 아래 예제에서 Locator<T>는 제네릭 클래스입니다.
public class Locator<T>
{
}Locator<T>의 인스턴스를 생성하려면 new 키워드 뒤에 클래스 이름을 작성하되, T 대신 실제로 전달하고자 하는 타입을 지정합니다. 다음 예제는 string 타입을 인수로 전달합니다.
var stringLocator = new Locator<string>();
아래 예제처럼 클래스의 메서드에서도 타입 매개변수(T)를 사용할 수 있습니다.
public class Locator<T>{
public IList<T> Items { get; set; }
public T Locate(int index){
return Items[index];
}
}
var stringLocator = new Locator<string>();
string item = stringLocator.Locate(2);제네릭의 또 다른 장점은 편집기가 제공하는 IntelliSense입니다. Visual Studio나 VS Code에서 stringLocator.Locate(4)를 입력한 후 메서드 이름 위에 마우스를 올리면, 반환 타입이 추상적인 T가 아니라 string임을 바로 확인할 수 있습니다. 만약 결과를 string이 아닌 다른 타입에 할당하려고 하면 컴파일러가 오류를 발생시킵니다.
// 오류: 'string' 형식을 'int'로 암시적으로 변환할 수 없습니다 [c-sharp]csharp(CS0029) int item = stringLocator.Locate(2);
제네릭 형식은 제네릭 기본 형식이나 제네릭 인터페이스를 상속·구현할 때 자신의 타입 매개변수를 타입 인수로 그대로 사용할 수 있습니다. 예를 들어 LinkedList<T> 형식은 IEnumerable<T>를 비롯한 여러 제네릭 인터페이스를 구현합니다.
public class LinkedList<T> : IEnumerable<T>
제네릭 메서드(Generic Methods)
제네릭 메서드란 메서드 내부에서 사용할 수 있고, 매개변수 및 반환 타입으로도 활용할 수 있는 타입 매개변수를 선언한 메서드를 말합니다. 아래 예제에서 Swap<T>는 T 타입의 두 매개변수를 받아 T의 인스턴스를 반환하는 제네릭 메서드입니다.
public class Swapper{
public T Swap<T>(T first, T second){
T temp = first;
first = second;
second = temp;
return temp;
}
}제네릭 형식과 마찬가지로, 제네릭 메서드를 호출하면 강력한 형식(strongly typed)의 변수가 반환됩니다.
var swapper = new Swapper(); int result = swapper.Swap<int>(5, 3);
타입 매개변수는 여러 개 사용할 수도 있습니다. System.Collections.Generic 네임스페이스의 Dictionary 클래스는 키와 값에 각각 하나씩, 총 두 개의 타입 매개변수를 가집니다.
public class Dictionary<TKey, TValue>
마지막으로, 무엇이 제네릭이 될 수 있는지 아는 것이 중요합니다. 형식의 경우 열거형(enum)을 제외한 모든 것이 제네릭이 될 수 있으며, 여기에는 다음이 포함됩니다.
- 클래스(class)
- 구조체(struct)
- 인터페이스(interface)
- 델리게이트(delegate)
반면 형식 멤버 중에서는 메서드와 중첩 형식만 제네릭이 될 수 있습니다. 다음 멤버들은 제네릭으로 선언할 수 없습니다.
- 필드(Field)
- 속성(Property)
- 인덱서(Indexer)
- 생성자(Constructor)
- 이벤트(Event)
- 종료자(Finalizer)
종합 예제
using System;
using System.Collections.Generic;
class Program{
static void Main(){
var stringLocator = new Locator<string>(){
Items = new string[] { "JavaScript", "CSharp", "Golang" }
};
string item = stringLocator.Locate(1);
Console.WriteLine(item); // CSharp
var swapper = new Swapper();
int a = 5, b = 3;
int result = swapper.Swap<int>(ref a, ref b);
Console.WriteLine($"a = {a}, b = {b}");
}
}
public class Locator<T>{
public IList<T> Items { get; set; }
public T Locate(int index){
return Items[index];
}
}
public class Swapper{
public T Swap<T>(ref T first, ref T second){
T temp = first;
first = second;
second = temp;
return temp;
}
}실행 결과
CSharp a = 3, b = 5