제네릭 타입 정의(Generic Type Definition)

제네릭 타입 정의(Generic Type Definition)

제네릭을 처음 만나는 순간의 핵심은 타입 자리표시자(placeholder)예요. 보통 type 정의와 거의 비슷한데, 타입이 들어갈 자리에 아직 정해지지 않은 이름을 써 두는 차이가 있어요. 그 이름은 클래스가 특수화될 때 실제 타입으로 채워져요.

출처: Generic type definition

본문

제네릭 타입 정의는 일반 타입 정의와 아주 비슷해요. 차이는 타입을 위한 자리표시자 목록을 포함한다는 점이에요. 아래 구문 다이어그램에서 확인할 수 있어요.

Generic class types

클래스, 객체, 프로시저 타입, 확장 레코드의 경우, 제네릭 타입 선언 뒤에는 **타입 구현(type implementation)**이 따라와야 해요. 일반 클래스 구현과 같지만, 한 가지 예외가 있어요. 바로 템플릿 식별자와 이름이 같은 식별자는 반드시 타입 식별자여야 한다는 점이에요.

그래서 제네릭 타입 선언은 일반 타입 선언과 거의 같아요. 다만 아직 알려지지 않은 타입이 존재한다는 점만 달라요. 그 알 수 없는 타입들은 자리표시자 목록에 나열되고, 클래스가 특수화되기 전까지는 알 수 없어요.

다음은 유효한 제네릭 클래스 정의예요.

Type
   generic TList<_T>=class(TObject)
   Public
     type
        TCompareFunc = function(const Item1, Item2: _T): Integer;
     var
       data : _T;
     procedure Add(item: _T);
     procedure Sort(compare: TCompareFunc);
   end;

이 클래스는 다음과 같이 구현을 뒤에 붙일 수 있어요.

procedure TList.Add(item: _T);
 begin
   data:=item;
 end;

 procedure TList.Sort(compare: TCompareFunc);
 begin
   if compare(data, 20) <= 0 then
     halt(1);
 end;

이 선언과 구현에서 몇 가지 주목할 점이 있어요.

  • 자리표시자는 하나인 _T뿐이에요. 제네릭 클래스가 특수화될 때 타입 식별자로 대체돼요.
  • _T 식별자는 타입 자리표시자 외의 다른 용도로 쓸 수 없어요. 즉 아래 코드는 유효하지 않아요.
procedure TList.Sort(compare: TCompareFunc);
      
      Var
        _t : integer;
      
      begin
        // do something.
      end;
  • 로컬 타입 블록에는 TCompareFunc 타입 하나가 있어요. 제네릭 클래스 정의 안에서는 실제 타입이 아직 알려지지 않아요. 정의에는 자리표시자 _T에 대한 참조만 들어 있죠. 그 외의 모든 식별자 참조는 제네릭 클래스가 특수화될 때가 아니라 정의될 때 알려져 있어야 해요.

로컬 변수 블록은 아래와 동일한 의미예요.

generic TList<_T>=class(TObject)
        Public
          type
             TCompareFunc = function(const Item1, Item2: _T): Integer;
        Public
          data : _T;
          procedure Add(item: _T);
          procedure Sort(compare: TCompareFunc);
        end;
  • 선언과 구현에서 타입 파라미터 이름은 같아야 해요.

제네릭 클래스뿐 아니라 다른 타입도 정의할 수 있어요. 아래는 레코드, 함수 타입, 배열 타입, 인터페이스, 클래스, 배열 별칭까지 다양한 제네릭 정의를 보여주는 예시예요.

{$mode objfpc}
 {$INTERFACES CORBA}
 type

   generic PlanarCoordinate<t> = record
     x,y : t;
   end;

   TScreenCoordinate = specialize PLanarCoordinate<word>;
   TDiscreteCoordinate = specialize PlanarCoordinate<integer>;
   TRealCoordinate = specialize PlanarCoordinate<extended>;

   generic TDistanceFunction<t> = function (x,y : t) : Extended of object;

   TScreenDistance = specialize TDistanceFunction<word>;
   TDiscreteDistance = specialize TDistanceFunction<integer>;
   TRealDistance = specialize TDistanceFunction<Extended>;

   generic TArray<t> = array of t;

   TMyIntegerArray = specialize TArray<integer>;

   generic IList<_T> = Interface
     Function GetItem(AIndex : Integer) : _T;
     Procedure SetItem(AIndex : Integer; AValue : _T);
     Function GetCount : Integer;
     Property Items [AIndex : Integer] : _T Read GetItem Write SetItem;
     Property Count : Integer Read GetCount;
   end;

   generic TList<_T>=class(TObject, specialize IList<_T>)
   public type
     TCompareFunc = function(const Item1, Item2: _T): Integer;
     Function GetItem(AIndex : Integer) : _T;
     Procedure SetItem(AIndex : Integer; AValue : _T);
     Function GetCount : Integer;
   Public
      data : _T;
      procedure Add(item: _T);
      procedure Sort(compare: TCompareFunc);
   end;

   generic TPointSet<t> = array of specialize PlanarCoordinate<t>;

   TScreenPointSet = specialize TPointSet<word>;
   TDiscretePointSet =  specialize TPointSet<integer>;
   TRealPointSet = specialize TPointSet<extended>;

주석: 가시성에 관해 한 마디. 템플릿 타입 T_Tstrict private 타입으로 제공돼요. 즉 이 타입들은 파생 클래스에서 사용할 수 없어요. protected나 private 같은 메커니즘으로 접근 가능하게 만들지 않는 한 그래요. 아래 예시처럼요.

generic TList<_T>=class(TObject)
   public type
     TItemType = _T;
   end;

더 알아보기

  • 제네릭을 특수화하는 방법은 8.3 절에서 다뤄요.
  • 제네릭 타입에 제약을 거는 방법은 8.4 절에서 확인할 수 있어요.
  • 제네릭이 왜 2단계 과정인지는 8.1 절(제네릭 소개)을 참고해요.