레코드·타입 헬퍼의 범위와 수명 — 스택 변수와 이벤트 핸들러

레코드·타입 헬퍼의 범위와 수명 — 스택 변수와 이벤트 핸들러

클래스의 경우 인스턴스의 수명은 프로그래머가 명시적으로 관리해요. 따라서 Self 파라미터가 무엇을 의미하고 언제 유효한지 분명해요.

출처: A note on scope and lifetime — Free Pascal Reference

본문

레코드와 다른 단순 타입은 스택에 할당돼요. 즉 그것들이 사용되는 함수/프로시저/메서드가 종료되면 범위를 벗어나요.

헬퍼 메서드가 클래스 메서드와 타입 호환되고 따라서 이벤트 핸들러로 사용될 수 있다는 사실과 결합하면 놀라운 상황이 생길 수 있어요. 헬퍼 메서드의 데이터 포인터는 변수의 주소로 설정돼요.

다음 예제를 보세요.

{$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

변수 im이 호출될 때 여전히 범위 안에 있어요.

하지만 코드를 이렇게 바꾸면:

{$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)가 스택 변수의 주소를 가리킨다는 거예요. 그 변수가 여전히 살아있는 동안(첫 예제)은 값이 유효하지만, 변수를 만든 함수가 반환된 뒤(두 번째 예제)에는 그 주소가 유효하지 않아서 예측 불가능한 값이나 충돌이 생겨요.

더 알아보기