제네릭은 코드의 재사용성과 유연성을 높여주는 도구로, 타입 안정성까지 제공합니다. 제네릭의 개념과 장점, 제네릭 클래스와 메서드 구현, 제네릭 컬렉션과 제한(Constraints)에 대한 글 입니다.
1. 제네릭(Generic)이란?
제네릭의 개념
**제네릭(Generic)**은 데이터의 타입을 일반화하여 코드의 재사용성을 높이는 기능입니다. 제네릭을 사용하면 특정 데이터 타입에 의존하지 않는 유연한 클래스, 메서드, 인터페이스를 작성할 수 있습니다.
제네릭을 사용하는 이유
- 코드 재사용성: 다양한 데이터 타입에서 동일한 로직을 사용할 수 있습니다.
- 타입 안전성: 컴파일 시점에 데이터 타입을 확인하여 오류를 방지합니다.
- 성능 최적화: 박싱(Boxing)과 언박싱(Unboxing)을 줄여 성능이 향상됩니다.
제네릭 없이 작성한 코드의 한계
class NonGenericList
{
private object[] items = new object[10];
private int index = 0;
public void Add(object item)
{
items[index++] = item;
}
public object Get(int i)
{
return items[i];
}
}
class Program
{
static void Main()
{
NonGenericList list = new NonGenericList();
list.Add(10); // 정수 추가
list.Add("Hello"); // 문자열 추가
// 데이터 타입을 명시적으로 캐스팅해야 함
int number = (int)list.Get(0); // 성공
string text = (string)list.Get(1); // 성공
}
}
문제점: 타입이 object이므로 데이터가 섞일 가능성이 높고, 매번 캐스팅해야 하며, 잘못된 캐스팅 시 런타임 오류가 발생할 수 있습니다.
2. 제네릭 클래스와 메서드 구현
제네릭 클래스
제네릭 클래스를 사용하면 클래스 내에서 특정 타입에 의존하지 않고 유연하게 데이터를 처리할 수 있습니다.
class GenericList<T>
{
private T[] items = new T[10];
private int index = 0;
public void Add(T item)
{
items[index++] = item;
}
public T Get(int i)
{
return items[i];
}
}
class Program
{
static void Main()
{
// 정수형 리스트
GenericList<int> intList = new GenericList<int>();
intList.Add(10);
int number = intList.Get(0);
Console.WriteLine($"정수: {number}");
// 문자열 리스트
GenericList<string> stringList = new GenericList<string>();
stringList.Add("Hello");
string text = stringList.Get(0);
Console.WriteLine($"문자열: {text}");
}
}
💡 <T>는 타입 매개변수로, 클래스가 생성될 때 타입을 결정합니다. 이를 통해 타입 안전성을 유지하면서 재사용 가능한 클래스를 작성할 수 있습니다.
제네릭 메서드
제네릭 메서드는 특정 클래스 내에서 뿐만 아니라, 일반 메서드에서도 유연한 타입을 처리할 수 있도록 합니다.
class Utility
{
public static void Print<T>(T data)
{
Console.WriteLine($"데이터: {data}");
}
}
class Program
{
static void Main()
{
Utility.Print(123); // 정수 출력
Utility.Print("Hello"); // 문자열 출력
Utility.Print(45.67); // 실수 출력
}
}
💡 제네릭 메서드는 다양한 데이터 타입을 처리하는 로직에서 중복 코드를 줄이는 데 유용합니다.
3. 제네릭 컬렉션 활용
C#의 System.Collections.Generic 네임스페이스에는 제네릭 기반의 강력한 컬렉션 클래스가 포함되어 있습니다.
List<T>
List<T>는 제네릭 기반으로 설계된 가장 많이 사용하는 컬렉션 중 하나입니다.
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<int> numbers = new List<int>();
numbers.Add(10);
numbers.Add(20);
numbers.Add(30);
foreach (int number in numbers)
{
Console.WriteLine(number);
}
}
}
Dictionary<TKey, TValue>
Dictionary<TKey, TValue>는 키-값 쌍으로 데이터를 저장하는 제네릭 컬렉션입니다.
class Program
{
static void Main()
{
Dictionary<int, string> students = new Dictionary<int, string>();
students.Add(1, "홍길동");
students.Add(2, "김철수");
foreach (var pair in students)
{
Console.WriteLine($"학번: {pair.Key}, 이름: {pair.Value}");
}
}
}
💡 제네릭 컬렉션은 타입 안전성을 유지하면서 성능도 높이기 때문에, 기존 ArrayList나 Hashtable 대신 권장됩니다.
4. 제네릭 제한 (Constraints)
제네릭에서 **제한(Constraints)**을 사용하면 타입 매개변수가 특정 조건을 만족해야만 사용할 수 있도록 제한할 수 있습니다.
기본 사용법
class Utility<T> where T : class // T는 참조형 타입이어야 함
{
public static void Print(T data)
{
Console.WriteLine(data.ToString());
}
}
다양한 제한 조건
제한설명
where T : class | T는 참조형 타입이어야 함 |
where T : struct | T는 값 형식이어야 함 |
where T : new() | T는 기본 생성자가 있어야 함 |
where T : BaseClass | T는 특정 기본 클래스 또는 인터페이스를 상속해야 함 |
예제: 기본 생성자 제한
class Factory<T> where T : new()
{
public static T Create()
{
return new T();
}
}
class Program
{
static void Main()
{
var obj = Factory<StringBuilder>.Create();
obj.Append("Hello, Generics!");
Console.WriteLine(obj.ToString());
}
}
5. 제네릭 인터페이스와 Delegate
제네릭 인터페이스
제네릭 인터페이스는 여러 클래스에서 공통적인 기능을 구현하도록 강제할 수 있습니다.
interface IRepository<T>
{
void Add(T item);
T Get(int id);
}
class Repository<T> : IRepository<T>
{
private Dictionary<int, T> data = new Dictionary<int, T>();
public void Add(T item)
{
data[data.Count] = item;
}
public T Get(int id)
{
return data[id];
}
}
class Program
{
static void Main()
{
IRepository<string> repo = new Repository<string>();
repo.Add("Hello");
Console.WriteLine(repo.Get(0));
}
}
제네릭 Delegate
델리게이트에도 제네릭을 사용할 수 있습니다.
delegate T Transformer<T>(T input);
class Program
{
static void Main()
{
Transformer<int> square = x => x * x;
Console.WriteLine(square(5)); // 출력: 25
}
}