레코드·타입 헬퍼의 범위와 수명 — 스택 변수와 이벤트 핸들러
레코드·타입 헬퍼의 범위와 수명 — 스택 변수와 이벤트 핸들러
클래스의 경우 인스턴스의 수명은 프로그래머가 명시적으로 관리해요. 따라서 Self 파라미터가 무엇을 의미하고 언제 유효한지 분명해요.
본문
레코드와 다른 단순 타입은 스택에 할당돼요. 즉 그것들이 사용되는 함수/프로시저/메서드가 종료되면 범위를 벗어나요.
헬퍼 메서드가 클래스 메서드와 타입 호환되고 따라서 이벤트 핸들러로 사용될 수 있다는 사실과 결합하면 놀라운 상황이 생길 수 있어요. 헬퍼 메서드의 데이터 포인터는 변수의 주소로 설정돼요.
다음 예제를 보세요.
{$mode objfpc}
{$modeswitch typehelpers}
uses
Classes;
type
TInt32Helper = type helper for Int32
procedure Foo(Sender: TObject);
end;
procedure TInt32Helper.Foo(Sender: TObject);
begin
Writeln(Self);
end;
var
i: Int32 = 10;
m: TNotifyEvent;
begin
m := @i.Foo;
WriteLn('Data : ',PtrUInt(TMethod(m).Data));
m(nil);
end.
이것은 대략 이렇게 출력돼요(데이터의 실제 값은 다를 수 있어요).
Data : 6848896
10
변수 i는 m이 호출될 때 여전히 범위 안에 있어요.
하지만 코드를 이렇게 바꾸면:
{$mode objfpc}
{$modeswitch typehelpers}
uses
Classes;
type
TInt32Helper = type helper for Int32
procedure Foo(Sender: TObject);
end;
procedure TInt32Helper.Foo(Sender: TObject);
begin
Writeln(Self);
end;
Function GetHandler :TNotifyEvent;
var
i: Int32 = 10;
begin
Result:[email protected];
end;
Var
m: TNotifyEvent;
begin
m := GetHandler;
WriteLn(PtrUInt(TMethod(m).Data));
m(nil);
end.
출력은 다음과 같아요.
140727246638796
0
실제 출력은 아키텍처에 따라 달라지지만, 요점은 i가 더 이상 범위 안에 있지 않아서 그 값의 출력이 의미가 없어지고, 심지어 접근 위반(access violation)과 프로그램 충돌로 이어질 수 있다는 거예요.
핵심은 헬퍼 메서드를 이벤트 핸들러처럼 사용할 때 데이터 포인터(Self)가 스택 변수의 주소를 가리킨다는 거예요. 그 변수가 여전히 살아있는 동안(첫 예제)은 값이 유효하지만, 변수를 만든 함수가 반환된 뒤(두 번째 예제)에는 그 주소가 유효하지 않아서 예측 불가능한 값이나 충돌이 생겨요.
더 알아보기
- 타입 헬퍼의 지원 범위는 Considerations for (simple) type helpers를 참고하세요
- 헬퍼 상속은 Inheritance에서 다뤄요