범위(scope)에 대한 한마디 — 제네릭 선언에서 이름의 가시성

범위(scope)에 대한 한마디 — 제네릭 선언에서 이름의 가시성

제네릭 클래스를 선언할 때, 템플릿 자리표시자(T)를 제외한 모든 식별자는 그 제네릭이 선언되는 시점에 이미 알려져 있어야 해요. 동시에 템플릿 타입에 대해서는 (제약을 걸지 않는 한) 어떤 가정도 할 수 없어요.

출처: A word about scope — Free Pascal Reference

본문

이 규칙은 여러 방식으로 나타나요.

템플릿 타입에 대한 가정 금지

타입 제약이 없으면 제네릭 코드는 템플릿 타입 T에 대해 아무것도 가정할 수 없어요. 다음 유닛을 보세요.

unit ts;

interface
{$modeswitch advancedrecords}

type
  PListEl = ^TListEl;
  TListEl = packed record
    Prev, Next: PListEl;
  end;


implementation

type
  generic LstEnumerator<T> = record
  private
    lst, lst_save: T;
  public
    constructor Create(const Value: T);
    function MoveNext: boolean;
  end;

function LstEnumerator.MoveNext: boolean;
begin
  Result:=lst <> nil;
  if Result then
    lst:=lst^.next;
end;

constructor LstEnumerator.Create(const Value: T);
begin
  lst:= Value;
  lst_save := nil;
end;


Type
 TMyListEnum = specialize LstEnumerator<TListEl>;

end.

컴파일러는 제네릭 정의를 컴파일할 때 다음 코드가 맞는지 확인할 수 없으므로 오류를 냅니다.

   lst:=lst^.next;

lst는 타입이 T인데, 컴파일러는 (아직) T가 무엇인지 모르기 때문에 Tnext 필드가 있는지도 알 수 없어요.

타입 제약으로 해결

이 문제는 타입 제약으로 해결할 수 있어요.

unit ts;
{$mode delphi}
interface

type
  TListEl = class
    Prev, Next: TListEl;
  end;

  TMyRecord1 = Class(TListEl)
    MyField : String;
  end;

  TMyRecord2 = Class(TListEl)
    MyInteger : Integer;
  end;

implementation

type
  TLstEnumerator<T : TListEl> = class
  private
    lst, lst_save: T;
  public
    constructor Create(const Value: T);
    function MoveNext: boolean;
  end;

function TLstEnumerator<T>.MoveNext: boolean;
begin
  Result:=lst <> T(nil);
  if Result then
    lst:=T(lst.next);
end;

constructor TLstEnumerator<t>.Create(const Value: T);
begin
  lst:= Value;
  lst_save := T(nil);
end;


Type
  TMyRecord1Enum = TLstEnumerator<TMyRecord1>;
  TMyRecord2Enum = TLstEnumerator<TMyRecord2>;

여기서 컴파일러는 lst가 적어도 TListEl 타입이라는 걸 알게 되므로 Prev, Next 멤버가 있다는 것도 알 수 있어요.

제네릭 선언의 다른 타입들도 알려져 있어야

템플릿 타입 외에도 제네릭 선언에 쓰인 모든 다른 타입은 알려져 있어야 해요. 즉 같은 이름의 타입 식별자가 존재해야 한다는 뜻이에요. 다음 유닛은 오류를 냅니다.

{$mode objfpc}
unit myunit;

interface

type
  Generic TMyClass<T> = Class(TObject)
    Procedure DoSomething(A : T; B : TSomeType);
  end;

Type
  TSomeType = Integer;
  TSomeTypeClass = specialize TMyClass<TSomeType>;

Implementation

Procedure TMyClass.DoSomething(A : T; B : TSomeType);

begin
  // Some code.
end;

end.

위 코드는 TSomeType이 선언을 파싱할 때 알려져 있지 않으므로 오류가 나요.

home: >fpc myunit.pp
myunit.pp(8,47) Error: Identifier not found "TSomeType"
myunit.pp(11,1) Fatal: There were 1 errors compiling module, stopping

지역 프로시저 참조

이 규칙이 드러나는 두 번째 방식이 있어요. 다음 유닛을 가정해 보죠.

{$mode objfpc}
unit mya;

interface

type
  Generic TMyClass<T> = Class(TObject)
    Procedure DoSomething(A : T);
  end;


Implementation

Procedure DoLocalThings;

begin
  Writeln('mya.DoLocalThings');
end;


Procedure TMyClass.DoSomething(A : T);

begin
  DoLocalThings;
end;

end.

컴파일러는 DoLocalThings 함수가 제네릭 타입을 특수화할 때 보이지 않으므로 이 유닛의 컴파일을 허용하지 않아요.

Error: Global Generic template references static symtable

이제 유닛을 수정해서 DoLocalThings 함수를 interface 섹션으로 옮기면 유닛은 컴파일돼요. 이 제네릭을 프로그램에서 사용하면:

{$mode objfpc}
program myb;

uses mya;

procedure DoLocalThings;

begin
  Writeln('myb.DoLocalThings');
end;

Type
  TB = specialize TMyClass<Integer>;

Var
  B : TB;

begin
  B:=TB.Create;
  B.DoSomething(1);
end.

제네릭이 특수화 시점에 다시 재생되는 매크로처럼 동작함에도 불구하고, DoLocalThings에 대한 참조는 TB가 정의될 때가 아니라 TMyClass가 정의될 때 해석돼요. 이는 프로그램의 출력이 다음과 같다는 뜻이에요.

home: >fpc -S2 myb.pp
home: >myb
mya.DoLocalThings

이 동작은 안전성과 필연성에 따라 결정돼요.

  1. 클래스를 특수화하는 프로그래머는 어떤 지역 프로시저가 사용되는지 알 수 없으므로 실수로 "오버라이드"할 수 없어요.
  2. 클래스를 특수화하는 프로그래머는 어떤 지역 프로시저가 사용되는지 알 수 없으므로, 파라미터도 모르기 때문에 이것을 구현할 수도 없어요.
  3. 위 예제처럼 implementation 프로시저가 사용되면 그것들은 유닛 밖에서 참조될 수 없어요. 그것들은 전혀 다른 유닛에 있을 수 있고, 프로그래머는 클래스를 특수화하기 전에 포함해야 한다는 것을 알 방법이 없어요.

더 알아보기