int타입의 배열을 출력하는 메소드 IntPrint
를 만들었다.
근데 float타입의 배열과 string타입의 배열도 출력해야하는 일이 발생했다.
그렇다면?
우리는 같은 형식의 출력하는 함수를 타입별로 중복해서 만들어줘야 한다.
아래 예시 코드가 바로 그러한 상황이다.
IntPrint
와 FloatPrint
, StringPrint
가 같은 형식으로 중복된다;;; (매우 비효율적)
namespace GenericTest
{
internal class Program
{
static void Main(string[] args)
{
int[] Num1 = { 1, 2, 3, 4 };
float[] Num2 = { 0.1f, 0.2f, 0.3f, 0.4f };
string[] Num3 = { "일", "이", "삼", "사" };
IntPrint(Num1);
FloatPrint(Num2);
StringPrint(Num3);
}
static void IntPrint(int[] Num)
{
foreach(int num in Num)
{
Console.Write($"{num} / ");
}
Console.WriteLine();
}
static void FloatPrint(float[] Num)
{
foreach (float num in Num)
{
Console.Write($"{num} / ");
}
Console.WriteLine();
}
static void StringPrint(string[] Num)
{
foreach (string num in Num)
{
Console.Write($"{num} / ");
}
Console.WriteLine();
}
}
}
중복 코드를 발생시키지 않기 위해 object
타입을 떠올릴 수 있다.
object
타입으로 코드 중복을 해결해보면??
namespace GenericTest
{
internal class Program
{
static void Main(string[] args)
{
int[] Num1 = { 1, 2, 3, 4 };
float[] Num2 = { 0.1f, 0.2f, 0.3f, 0.4f };
string[] Num3 = { "일", "이", "삼", "사" };
ArrayPrint(Num1.Select(x => (object)x).ToArray());
ArrayPrint(Num2.Select(x => (object)x).ToArray());
ArrayPrint(Num3.Select(x => (object)x).ToArray());
}
static void ArrayPrint(object[] Num)
{
foreach(object num in Num)
{
Console.Write($"{num} / ");
}
Console.WriteLine();
}
}
}
ㄴ 중복 코드는 사라졌지만, LINQ의 Select
를 사용하여 각 배열의 요소를 object
로 변환하고, 그 후 .ToArray()
를 사용하여 변환한 데이터를 배열로 만들어 ArrayPrint()
메서드에 전달해야하는 오히려 머리 아픈 현상이 일어난다..
ㄴ 뿐만 아니라, object
타입 변수에 값 타입 인스턴스를 넣고 캐스팅한다면 박싱/언박싱을 해야하기 떄문에 성능에 지장을 준다..
❌ 그러므로, object
로 서로 다른 타입을 하나로 묶는 것은 좋은 방법은 아니다 ❌
💡이럴 때 사용하는 것이 '제네릭(일반화 프로그래밍)'이다!
- 일반화란?
개별적인 것이나 특수한 것의 공통점을 찾아 일반적인 것으로 만드는 것이다.- 일반화 프로그래밍(제네릭)이란?
데이터 형식을 일반화하는 기법으로 특정 타입에 국한되지 않고 모든 타입을 멤버 변수의 타입으로 설정할 수 있다. 즉, 변수의 데이터 타입 떄문에 여러 개의 메서드/클래스를 작성해야 하는 경우 한 개의 메서드/클래스로 구현할 수 있는 기법이다.
위에 작성했던 코드를 '제네릭 메서드'로 바꿔보자!
namespace GenericTest
{
internal class Program
{
static void ArrayPrint<T>(T[] Num)
{
foreach (T num in Num)
{
Console.Write($"{num} / ");
}
Console.WriteLine();
}
static void Main(string[] args)
{
int[] Num1 = { 1, 2, 3, 4 };
float[] Num2 = { 0.1f, 0.2f, 0.3f, 0.4f };
string[] Num3 = { "일", "이", "삼", "사" };
ArrayPrint<int>(Num1);
ArrayPrint<float>(Num2);
ArrayPrint<string>(Num3);
}
}
}
중복 코드가 사라지고 확실히 코드가 깔끔해졌다! 하나씩 살펴보자.
<T>
의 의미<T>
는 제네릭 형식 매개변수로, 이 메서드가 어떤 형식의 배열에 대해서도 작동할 수 있음을 나타낸다. 일반적으로 Type의 약자인 T로 작성한다.
T[] Num
은 T 형식의 배열을 입력으로 받는 것을 나타낸다.
foreach 루프를 사용하여 배열 Num
의 모든 요소를 반복하고 각 요소를 출력한다. T는 메서드 호출 시에 실제 형식으로 대체된다.
ArrayPrint()
메서드를 호출할 때 int
, float
, string
형식을 각각 인수로 전달한다. 제네릭 메서드를 호출할 때 형식 인수를 제공함으로써 메서드는 해당 형식에 대한 코드를 실행한다.
먼저, 제네릭 클래스가 아닌 중복 코드로 만든 코드이다.
namespace GenericTest
{
internal class Program
{
static void Main(string[] args)
{
IntClass intC = new IntClass();
DoublecClass doubleC = new DoublecClass();
StringClass stringC = new StringClass();
intC.Print(100);
doubleC.Print(1.1);
stringC.Print("백");
}
}
class IntClass
{
public void Print(int val)
{
Console.WriteLine($"val = {val}");
}
}
class DoublecClass
{
public void Print(double val)
{
Console.WriteLine($"val = {val}");
}
}
class StringClass
{
public void Print(string val)
{
Console.WriteLine($"val = {val}");
}
}
}
이를 제네릭 클래스로 바꿔보면,
namespace GenericTest
{
internal class Program
{
static void Main(string[] args)
{
GenericClass<int> intObj = new GenericClass<int>();
GenericClass<double> doubleObj = new GenericClass<double>();
GenericClass<string> stringObj = new GenericClass<string>();
intObj.Print(100);
doubleObj.Print(1.1);
stringObj.Print("백");
}
}
class GenericClass<T>
{
public void Print(T val)
{
Console.WriteLine($"val = {val}");
}
}
}
ㄴ 이렇게 GenericClass
하나로 여러 타입의 값을 사용할 수 있다.
제약 | 설명 |
---|---|
where T : struct | T는 값 형식이어야 한다. |
where T : class | T는 참조 형식이어야 한다. |
where T : new() | T는 반드시 매개변수가 없는 생성자가 있어야 한다. |
where T : 부모 클래스명 | T는 명시한 부모 클래스의 자식 클래스여야 한다. |
where T : 인터페이스명 | T는 명시한 인터페이스를 반드시 구현해야 한다. 복수의 인터페이스를 명시해도 된다. |
where T : U | T는 또 다른 형식 매개변수 U로부터 상속받은 클래스여야 한다. |
ㄴ int, double, string은 모두 IComparable 인터페이스를 상속받기 때문에 오류가 발생하지 않는다! 만약, IComparable을 상속받지 않는 다른 Class나 Struct가 타입으로 들어오면 오류 발생!
List<T>
Queue<T>
Stack<T>
Dictionary<TKey, TValue>
<T>
비일반화 클래스인 ArrayList와 같은 기능을 한다. 다만 아무 형식의 객체나 마구 집어넣을 수 있었던 ArrayList와는 달리 List<T>
는 형식 매개변수에 입력한 형식 외에는 입력을 허용하지 않는다.
<T>
비일반화 클래스인 Queue와 같은 기능을 하며 사용법도 동일하다.
<T>
비일반화 클래스인 Stack과 같은 기능과 사용법이다.
<TKey, TValue>
Hashtable의 일반화 버전이다. 형식 매개변수 2개를 요구하며 TKey는 Key, TValue는 Value를 위한 형식이다.
- 코드의 재사용
: 제네릭 메서드/클래스를 다양한 변수 형식에 재사용할 수 있다.- 형식 안정성
: 컴파일러는 형식 불일치 오류를 방지하므로 안정성이 확보된다.
- 가독성
: 일반적인 메서드를 작성할 때보다 코드가 간결하며 가독성이 좋다.
📄참고자료
- <이것이 c#이다> 3판 - 박상현 지음 (한빛미디어)
- [인프런] c#프로그래밍 기초 - 이교준
- 제네릭 메서드
- 제네릭 클래스
- 제네릭