관리 타입과 참조 카운트
관리 타입과 참조 카운트 (Managed types and reference counts)
Pascal을 쓰다 보면 문자열이나 인터페이스 같은 값이 여러 곳에서 공유될 때가 있어요. Free Pascal은 일부 타입을 **관리 타입(managed type)**으로 취급해서, 데이터가 참조되는 횟수(참조 카운트)를 자동으로 관리해요. 이 참조 카운트가 함수/프로시저의 매개변수 한정자에 따라 어떻게 달라지는지 이해하는 게 중요해요.
출처: Free Pascal Reference - Managed types and reference counts
본문
일부 타입 — Unicodestring, Ansistring, 인터페이스, 동적 배열 — 은 컴파일러가 다소 특별하게 취급해요. 이 타입들의 데이터에는 참조 카운트가 있어서, 데이터에 대한 참조가 몇 개 존재하는지에 따라 카운트가 증가하거나 감소해요.
함수나 프로시저 호출에서 매개변수의 한정자가 관리 타입의 참조 카운트에 어떤 영향을 주는지 정리하면 다음과 같아요.
- 아무것도 (값 전달, pass by value): 매개변수의 참조 카운트는 진입 시 1 증가하고, 종료 시 1 감소해요.
- out: 전달된 값의 참조 카운트는 1 감소하고, 프로시저에 전달된 변수는 "빈(empty)" 상태로 초기화돼요(보통
Nil이지만, 이는 의존하면 안 되는 구현 세부 사항이에요). - var: 참조 카운트에는 아무 일도 일어나지 않아요. 원래 변수에 대한 참조가 전달되고, 그것을 바꾸거나 읽는 것은 원래 변수를 바꾸거나 읽는 것과 정확히 같은 효과를 가져요.
- const: 이 경우는 조금 까다로워요. 값이 아닌 것들을 전달할 수 있기 때문에 참조 카운트에는 아무 일도 일어나지 않아요. 특히 인터페이스 자체 대신 그 인터페이스를 구현하는 클래스를 전달할 수 있는데, 이러면 클래스가 예상치 못하게 해제될 수 있어요.
참고: 함수 결과는 내부적으로 함수의 var 매개변수처럼 취급되고, var 매개변수와 같은 규칙이 적용돼요.
다음 예시는 그 위험성을 보여줘요.
{$mode objfpc}
Type
ITest = Interface
Procedure DoTest(ACount : Integer);
end;
TTest = Class(TInterfacedObject,ITest)
Procedure DoTest(ACount : Integer);
Destructor destroy; override;
end;
Destructor TTest.Destroy;
begin
Writeln('Destroy called');
end;
Procedure TTest.DoTest(ACount : Integer);
begin
Writeln('Test ',ACount,' : ref count: ',RefCount);
end;
procedure DoIt1(x: ITest; ACount : Integer);
begin
// Reference count is increased
x.DoTest(ACount);
// And decreased
end;
procedure DoIt2(const x: ITest; ACount : Integer);
begin
// No change to reference count.
x.DoTest(ACount);
end;
Procedure Test1;
var
y: ITest;
begin
y := TTest.Create;
// Ref. count is 1 at this point.
y.DoTest(1);
// Calling DoIT will increase reference count and decrease on exit.
DoIt1(y,2);
// Reference count is still one.
y.DoTest(3);
end;
Procedure Test2;
var
Y : TTest;
begin
Y := TTest.Create; // no count on the object yet
// Ref. count is 0 at this point.
y.DoTest(3);
// Ref count will remain zero.
DoIt2(y,4);
Y.DoTest(5);
Y.Free;
end;
Procedure Test3;
var
Y : TTest;
begin
Y := TTest.Create; // no count on the object yet
// Ref. count is 0 at this point.
y.DoTest(6);
// Ref count will remain zero.
DoIt1(y,7);
y.DoTest(8);
end;
begin
Test1;
Test2;
Test3;
end.
이 예시의 출력은 다음과 같아요.
Test 1 : ref count: 1
Test 2 : ref count: 2
Test 3 : ref count: 1
Destroy called
Test 3 : ref count: 0
Test 4 : ref count: 0
Test 5 : ref count: 0
Destroy called
Test 6 : ref count: 0
Test 7 : ref count: 1
Destroy called
Test 8 : ref count: 0
보시다시피 test3에서는 DoIt 호출이 끝날 때 참조 카운트가 1에서 0으로 감소해서, 호출이 반환되기 전에 인스턴스가 해제돼요.
다음 작은 프로그램은 문자열에 사용되는 참조 카운트를 보여줘요.
{$mode objfpc}
{$H+}
// Auxiliary function to extract reference count.
function SRefCount(P : Pointer) : integer;
Type
PAnsiRec = ^TAnsiRec;
TAnsiRec = Record
CodePage : TSystemCodePage;
ElementSize : Word;
{$ifdef CPU64}
{ align fields }
Dummy : DWord;
{$endif CPU64}
Ref : SizeInt;
Len : SizeInt;
end;
begin
if P=Nil then
Result:=0
else
Result:=PAnsiRec(P-SizeOf(TAnsiRec))^.Ref;
end;
Procedure ByVar(Var S : string);
begin
Writeln('By var, ref count : ',SRefCount(Pointer(S)));
end;
Procedure ByConst(Const S : string);
begin
Writeln('Const, ref count : ',SRefCount(Pointer(S)));
end;
Procedure ByVal(S : string);
begin
Writeln('Value, ref count : ',SRefCount(Pointer(S)));
end;
Function FunctionResult(Var S : String) : String;
begin
Writeln('Function argument, ref count : ',SRefCount(Pointer(S)));
Writeln('Function result, ref count : ',SRefCount(Pointer(Result)));
end;
Var
S,T : String;
begin
S:='Some string';
Writeln('Constant : ',SrefCount(Pointer(S)));
UniqueString(S);
Writeln('Unique : ',SRefCount(Pointer(S)));
T:=S;
Writeln('After Assign : ',SRefCount(Pointer(S)));
ByVar(S);
ByConst(S);
ByVal(S);
UniqueString(S);
T:=FunctionResult(S);
Writeln('After function : ',SRefCount(Pointer(S)));
end.
더 알아보기
Ansistring,Unicodestring, 동적 배열의 내부 구조를 살펴보면 참조 카운트를 더 잘 이해할 수 있어요.UniqueString루틴은 문자열의 참조 카운트를 강제로 1로 만들어 줄 때 써요.