posted by 방랑군 2012. 1. 4. 02:45

출처 : http://blog.naver.com/saltynut/

Generic

 

드디어 C#과 VB.NET에 Generic이 적용되었습니다!  '드디어'라고 하니 마치 대단한 것처럼 생각이 되지만, 사실 C++ 프로그래머들이 템플릿 기능이 없다는 것으로 하도 난리법석(?)을 떤 탓에..

우선 Generic이 대체 어디에 쓰는 물건인지부터 알아봐야겠죠?

가장 간단하게, 유식하게 말하자면.. 'Type independent한 코드를 작성하게 해준다'라는 것입니다.

무슨 말인가 하면.. 통상 개발을 하다보면.. 특히 라이브러리 같은 것을 작성하다보면..

여러 타입을 지정할 수 있도록 필드, 파라미터, 리턴 타입 등을 object로 쓰는 경우가 많습니다.

예를 들면 다음과 같습니다.

 

using System.Collections;

 
// Employees 개체를 담을 Stack
Stack employees = new Stack();
// Push 메서드의 매개변수는 object 형이므로, 암시적 형 변환이 일어남
employees.Push( new Employee() );
// Pop 메서드의 리턴 타입은 object 형이므로, 명시적 형 변환을 해야 함
Employee employee = (Employee) employees.Pop();
 
뭐, 위 경우야 일단 별다른 문제가 있는 것은 아닙니다. 하지만 다음 코드는 좀 심각하지요.
 
// Integer를 담을 Stack
Stack sizes = new Stack();
// Box
sizes.Push( 42 );
// Unbox
int size1 = (int) sizes.Pop();
// Integer만 들어가야 하지만.. 어쨌든 컴파일 시는 OK
sizes.Push( 77 );
sizes.Push( new Employee() );
// 역시 컴파일은 되지만, Pop 시키는 것이 Employee 형일 경우..
// InvalidCastException이 발생한다.
int size2 = (int) sizes.Pop();
 
으음.. 이건 문제가 좀 있지요?
이런 경우에 발생가능한 문제점, 단점들에 대해서 정리해보자면..
* 컴파일 시에 타입 체킹이 불가능하므로, 임의의 타입을 집어넣어도 확인할 길이 없습니다.
* value type을 사용할 때, 박싱, 언박싱이 일어납니다.
* ref type일 때도, 런타임 타입 체킹이 계속 일어나며, 형 변환이 필요하므로 오버헤드가 생깁니다.
 
Generic은 이런 문제점을 해결하기 위해 나온 녀석입니다. 백문이 불여일견이라고, 위의 코드를 Generic을 적용해 보도록 하겠습니다.
 
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 );
 
// 0~6까지를 Stack에 집어넣음
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)
 
' 0~6까지를 Stack에 집어넣음
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에 대해서 살펴보았습니다.
어떤가요? 유용할 것 같나요?
뭐, 정답은.. 만드는 사람은 귀찮고, 쓰는 사람은 편하고.. ^^
그럼 나머지 언어 상의 변경사항은 다음에 또 알아보겠습니다.