범위(scope)에 대한 한마디 — 제네릭 선언에서 이름의 가시성
범위(scope)에 대한 한마디 — 제네릭 선언에서 이름의 가시성
제네릭 클래스를 선언할 때, 템플릿 자리표시자(T)를 제외한 모든 식별자는 그 제네릭이 선언되는 시점에 이미 알려져 있어야 해요. 동시에 템플릿 타입에 대해서는 (제약을 걸지 않는 한) 어떤 가정도 할 수 없어요.
본문
이 규칙은 여러 방식으로 나타나요.
템플릿 타입에 대한 가정 금지
타입 제약이 없으면 제네릭 코드는 템플릿 타입 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가 무엇인지 모르기 때문에 T에 next 필드가 있는지도 알 수 없어요.
타입 제약으로 해결
이 문제는 타입 제약으로 해결할 수 있어요.
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
이 동작은 안전성과 필연성에 따라 결정돼요.
- 클래스를 특수화하는 프로그래머는 어떤 지역 프로시저가 사용되는지 알 수 없으므로 실수로 "오버라이드"할 수 없어요.
- 클래스를 특수화하는 프로그래머는 어떤 지역 프로시저가 사용되는지 알 수 없으므로, 파라미터도 모르기 때문에 이것을 구현할 수도 없어요.
- 위 예제처럼 implementation 프로시저가 사용되면 그것들은 유닛 밖에서 참조될 수 없어요. 그것들은 전혀 다른 유닛에 있을 수 있고, 프로그래머는 클래스를 특수화하기 전에 포함해야 한다는 것을 알 방법이 없어요.
더 알아보기
- 제네릭 특수화 결과의 타입 호환 규칙은 Type compatibility에서 다뤄요
- 제네릭에서 연산자 오버로딩은 Operator overloading and generics를 참고하세요