using System.Collections.Generic;
Stack< Employee > employees = new Stack< Employee >();
// Push 메서드 매개변수가 Employee 형이므로, 형 변환이 필요없음
employees.Push( new Employee() );
// Pop 메서드가 Employee 형을 리턴하므로, 형 변환이 필요없음
Employee employee = employees.Pop();
흐음.. 뭔가 좀 좋아진 것 같지요?
아까 좀 심각하다고 했던 코드는 어떻게 되는지 봅시다.
Stack< int > sizes = new Stack< int >();
// No boxing
sizes.Push( 42 );
// No unboxing
int size1 = sizes.Pop();
sizes.Push( 77 );
// 아래 코드는 컴파일 시 에러가 발생한다.
sizes.Push( new Employee() );
// 이제 안심하고 써도 됨 (리턴 타입이 항상 int일테니..)
int size2 = sizes.Pop();
자.. 보다시피 < > 안에 특정 타입을 지정해서 코드 템플릿을 만들 수 있게 해줍니다.
그렇게 해서, 특정한 타입만이 사용되도록 보장해주는 것이죠.
이 기능은 C++에서는 템플릿이라고 불리며, Eiffel, Ada와 같은 다른 언어들에서는 Generic이라 불립니다. (대세를 따라서 C#, VB.NET은 후자를 택한 것 같네요.)
Generic은 클래스, 구조체, 인터페이스, 메서드, 대리자(delegate) 등 다양하게 만들 수 있습니다.
Generic 사용 시의 장점은..
* 컴파일 타입 체킹을 좀 더 빡세게(?) 수행할 수 있습니다.
* 불필요한 런타임 타입 체킹, 박싱, 언박싱을 줄일 수 있습니다.
* 명시적으로 형 변환을 하지 않아도 됩니다.
* 좀 더 깔끔하고, 빠르고, 안전한 코드를 작성할 수 있습니다.
장점을 좀 더 팍팍 와 닿게 하기 위해서.. 다음 그림을 통해 비교해 보죠.
왼쪽의 Generic을 사용하지 않은 경우와 오른쪽의 사용하는 경우를 볼 때 어느게 더 복잡해 보이는지요? Generic을 사용하는 것이 훨씬 더 깔끔하다는 것을 알 수 있습니다.
자, 그러면 이렇게 Generic을 지원하는 클래스는 어떻게 작성할까요? 예를 들어, 위의 스타일처럼 지원하도록 MyStack이라는 클래스를 작성해보도록 하겠습니다.
// Generic MyStack 클래스
public class MyStack< T >
{
private T[] frames;
private int pointer = 0;
public MyStack( int size )
{
frames = new T[ size ];
}
public void Push( T frame )
{
frames[ pointer++ ] =
frame;
}
public T Pop()
{
return
frames[ --pointer ];
}
}
대충 감이 잡히시겠지만, 클래스 명 뒤에 < >를 붙이고.. 그 안에 T라고 썼습니다.
이 T는 임의의 템플릿 타입 파라미터(Template Type Parameter)이므로, 이름은 무엇으로 변경해도 상관없습니다. 클래스 내의 메서드 구현 시 T를 사용하고 있는 것에 주의하시기 바랍니다.
아래는 위 클래스를 사용하는 예제입니다.
MyStack< int > s = new MyStack< int >( 7 );
for ( int f = 0; f < 7; ++f )
s.Push( f );
// '6 5 4 3 2 1 0 ' 을 출력
for ( int f = 0; f < 7; ++f )
Console.Write( s.Pop() + " " );
아참.. VB.NET에서는 어떻게 하냐구요? 다음과 같이 (Of T) 형태의 구문을 사용합니다.
' Generic MyStack 클래스
Public Class MyStack(Of T)
Private frames As T()
Private pointer As Integer = 0
Public Sub New( ByVal size As Integer)
frames = New T(size) {}
End Sub
Public Sub Push( ByVal frame As T)
frames(pointer) = frame
pointer += 1
End Sub
Public Function Pop() As T
pointer -= 1
Return frames(pointer)
End Function
End Class
Dim s As New MyStack(Of Integer)(7)
For f As Integer = 0 To 6
s.Push(f)
Next f
' '6 5 4 3 2 1 0 ' 을 출력
For f As Integer = 0 To 6
Console.Write( s.Pop().ToString())
Next f
자.. 그러면 위의 템플릿 타입 파라미터.. T를 아무 타입이나 못하게 제약을 걸 수는 없을까요?
물론 가능합니다. Generic에서는 where 키워드를 사용하여 다음 3가지 유형의 제약 조건을 걸 수 있습니다.
* Class 제약 : 타입이 특정 베이스 클래스로부터 상속된 것이어야 함
* Interface 제약 : 타입이 특정 인터페이스를 구현한 것이어야 함
* 생성자 제약 : 타입이 public 기본 생성자를 가져야 함
이러한 제약 조건을 걸도록 작성한 Generic 클래스는 다음과 같습니다.
class MyList< K, V >
where K : IComparable, IFormattable
where V : ValueBase, new()
{
// ...
}
K는 IComparable, IFormattable 인터페이스를 구현한 것이어야 하며..
V는 ValueBase로부터 상속받고, public 기본 생성자가 있는 것이야 합니다.
그럼 MyList를 사용하는 예제를 보죠.
// ValueBase 클래스
class ValueBase {}
// Widget 클래스 : ValueBase를 상속
class Widget : ValueBase {}
// Thing 클래스 : ValueBase를 상속하며, public 기본 생성자가 없음
class Thing : ValueBase
{
public Thing( int i ) {}
}
// OK
MyList<int, Widget> list1 = new MyList<int, Widget>();
// Error : string은 ValueBase로부터 상속되지 않음
MyList<int, string> list2 = new MyList<int, string>();
// Error : Thing은 ValueBase로부터 상속되지만, public 기본 생성자가 없음
MyList<int, Thing> list3 = new MyList<int, Thing>();
// Error : Point는 IComparable, IFormattable 인터페이스를 구현하지 않음
MyList<Point, Widget> list4 = new MyList<Point,Widget>();
위에서 볼 수 있듯이, 제약조건을 사용하면..
* 타입이 필요한 기능을 가졌는지를 보장하고,
* 보다 강력한 컴파일 타임 타입 체킹을 할 수 있으며,
* 명시적 형변환을 줄일 수 있고,
* generic의 사용을 제한하는 효과를 가집니다.
지금까지 Generic에 대해서 살펴보았습니다.
어떤가요? 유용할 것 같나요?
뭐, 정답은.. 만드는 사람은 귀찮고, 쓰는 사람은 편하고.. ^^
그럼 나머지 언어 상의 변경사항은 다음에 또 알아보겠습니다.