[펌] 델파이 클래스(Class) Delphi 2005/07/07 00:19 http://nsj0409.blog.me/20014525096 |
당시 게시판이 텝문자 지원이 안되서 소스코드의 들여쓰기가 안되있는게
좀 그렇긴 하지만, 쉬운 코드들이니 차근히 보시면 되리라 생각이 된다.
그럼 강좌 원문 나갑니다.
델마당, 델코, 한델 또는 어딘가에서 가져온 걸텐데.. -_-
---------------------------------------------------------------------
컴포넌트 제작을 위한 기초 지식 - 클래스 (Class)
컴포넌트를 만들기 전에 여러분은 클래스(class)라는 자료 구조를 알아야 합니다.
본문의 내용은 파스칼의 기본 문법을 어느 정도 숙지하고 있는 것으로 간주하여 설명합니다. 이미 오브젝트 파스칼의 클래스를 아시거나 C++에서 클래스를 사용해 보신 분이라면 가볍게 읽고 넘어가셔도 됩니다.
클래스(class)란?
클래스는 오브젝트 파스칼의 가장 큰 특징입니다.
자료구조만 보면 레코드형(레코드 형을 모르시는 분은 관련 서적의 자료형을 잠시 둘러 보시길)과 거의 유사하지만 훨씬 강력한 여러가지 기능을 가지고 있습니다. 우리가 앞으로 함께 만들어 볼 컴포넌트도 일종의 클래스라고 볼 수 있습니다. 따라서 반드시 클래스의 기본 개념을 알고 넘어가야 하겠습니다.
클래스는 C의 구조체(struct)나 파스칼의 레코드(record)형처럼 여러개의 요소들로 이루어진 자료들의 모음입니다. 레코드형에서는 레코드 내부에 정의된 요소들(필드(field)라고 부름)을 접근할 때 직접 접근하지만 클래스에서는 이들을 직접 참조하는 대신 클래스가 제공하는 프로시져나 함수를 통해서 작업을 처리합니다. 이런 프로시져나 함수를 메소드라고 합니다. 또 필드를 참조하기 위한 한 방법으로 프로퍼티라는 요소를 가집니다. 이렇게 한 클래스에 속한 필드,메소드,프로퍼티등을 클래스의 멤버(Member)라고 부릅니다.
오브젝트, 인스턴스란?
클래스는 일종의 자료형이고 정의한 변수 자체로는 아직 오브젝트가 아닙니다. 이 변수는 일종의 Pointer형 변수라고 볼 수 있습니다. 오브젝트로 되기 위해서는 클래스를 위한 메모리가 할당되어 변수에 대입되는 인스턴스 과정이 필요합니다. 이런 과정을 생성(Create)이라고 부릅니다. 이렇게 생성된 것을 오브젝트 또는 인스턴스라고 부릅니다. 오브젝트의 사용이 끝나면 사용된 메모리를 시스템에 반납하는 파괴(destroy)라는 과정을 거쳐야 합니다. 클래스 변수는 실제로 Pointer형 변수와 거의 같지만 소스코드를 작성할 때 꺾쇠(^)를 쓰지 않아도 됩니다. 그리고 New 이나 Dispose 를 사용하는 대신 클래스 내부에 정의된 생성자와 소멸자를 이용하여 메모리를 할당 합니다.
조상(Ancestor), 후손(Descendent)
클래스의 또 하나의 특징은 다른 클래스를 상속하여 새로운 클래스를 정의할 수 있다는 것 입니다. 즉 A라는 클래스가 가지는 모든 요소를 가지면서 새로 몇가지 요소가 추가된 B라는 클래스를 만들 수 있습니다. 이때 새로 선언된 B라는 클래스를 A의 후손(Descendent)라고 하고, 참조된 A클래스를 B클래스의 조상(Ancestor)이라고 합니다. 후손 클래스는 새로운 멤버를 추가할 수 있고 조상에서 정의된 멤버를 제거 할 수는 없습니다. 하지만 메소드에 한해서 어떤 것은 나중에 설명될 중첩이라는 방법으로 후손의 후손의 메소드로 대치가 가능 합니다. 모든 클래스는 계승받을 조상을 지정하게 되어있습니다. 조상 클래스를 생략하는 경우 기본적으로 TObject라는 델파이의 기본 클래스를 조상으로 가집니다. 이 TObject라는 클래스는 모든 클래스의 가장 높은 조상이되며 모든 오브젝트가 가질 기본적인 기능을 구현해 놓은 클래스 입니다.
클래스의 선언과 사용
클래스는 Type절에서 선언합니다. 다음은 한 예 입니다.
Type
TMyDate = class(TObject)
Year,Month,Day: integer;
end;
위의 예는 다음과 같은 구조를 가지고 있습니다.
클래스 이름 = class (조상 클래스)
멤버 선언
end;
클래스를 선언할 때는 예약어 class를 사용하며 괄호안에 조상 클래스를 표기합니다. 조상이 TObject라면 괄호를 포함하여 생략이 가능합니다. 이후부터 end까지는 멤버들을 선언합니다. (참고: 클래스 이름을 T로 시작하는 것은 델파이의 관례일 뿐이며 실제로 어떤 문자로 시작하여도 상관이 없습니다. 그러나 클래스명을 T로 시작하면 자료형을 구분하기가 편해지므로 관례에 따르는 편이 좋습니다.)
위에서 선언된 TMyDate의 선언은 레코드 형의 선언과 비슷합니다. 세 필드를 접근하는 방법도 동일 합니다. 자 이제 TMyDate를 생성하고 조작하는 간단한 예제를 보이겠습니다.
var
ADay: TMyDate; // TMyDate형의 변수를 선언.. (일종의 Pointer변수라고 볼 수 있다)
begin
// TMyDate 오브젝트를 생성하고 그 오브젝트를 변수 ADay에 지정한다.
ADay := TMyDate.Create; // ADay.Create가 아님을 기억하기 바람..
// 여기서부터 ADay라는 변수에 할당된 TMyDate 오브젝트를 사용하면 된다.
ADay.Year := 1999;
ADay.Month := 2;
ADay.Day := 22;
....
// ADay라는 변수에 할당된 TMyDate 오브젝트를 제거한다.
ADay.Free;
end;
여기까지는 정말 레코드형과 다를 바가 없습니다.
한가지 다른 점은 TMyDate.Create와 Aday.Free등의 코드 입니다.
이 내용은 아래에서 설명하겠습니다.
이제부터 클래스에 대한 작업을 추가하기 위하여 메소드를 작성하여 보겠습니다.
위에서 만든 TMyDate라는 클래스에 SetValue와 IsLeapYear라는 메소드를 추가합니다.
Type
TMyDate = class(TObject)
Year,Month,Day: integer;
procedure SetValue(y,m,d:integer);
function IsLeapYear: boolean;
end;
// 클래스의 세 필드의 값을 초기화 하는 프로시져
procedure TMyDate.SetValue(y,m,d:Integer);
begin
Year := y;
Month := m;
Day := d;
end;
// 초기화된 년도가 윤년인지 아닌지를 구하는 함수
function TMyDate.IsLeapYear: boolean;
begin
if (Year mod 4 <> 0) then
Result := False
else if (Year mod 100 <>0) then
Result := True
else if (Year mod 400 <>0) then
Result := False
else
Result := True;
end;
다음은 이렇게 작성된 메소드를 사용하는 예제 입니다.
var
ADay: TMyDate;
bLeap: boolean;
begin
ADay := TMyDate.Create; // 오브젝트를 생성
ADay.SetValue(1999,2,22); // 메소드 프로시져를 통해 초기값 지정
bLeap := ADay.IsLeapYear; // 메소드 함수를 통해 윤년인지 여부를 얻는다.
...
ADay.Free; // 오브젝트를 제거
end;
위의 예제들에서 보셨듯이 클래스는 레코드 형과는 다르게 메소드라는 멤버가 존재 합니다. 이들 메소드의 접근에서도 필드들의 접근에서와 같이 <클래스이름>.<메소드>의 형태를 가집니다.
여기서 앞에서 설명하기로 한 Create와 Free라는 함수에 대하여 설명하겠습니다.
Create는 일종의 생성자 입니다. 이 함수를 호출하면 델파이는 TMyDate형의 자료를 보관할 메모리를 시스템으로부터 할당 받습니다. 그러나 TMyDate에는 Create나는 메소드가 존재하지 않습니다.
그럼 과연 이 메소드는 어디에 있을까요?
여기에서 우리는 TMyDate가 TObject의 내용을 물려 받았다는 것을 기억하여야 합니다.
다음은 TObject 클래스의 정의 내용 입니다.
TObject = class
constructor Create;
procedure Free;
class function InitInstance(Instance: Pointer): TObject;
procedure CleanupInstance;
function ClassType: TClass;
class function ClassName: ShortString;
class function ClassNameIs(const Name: string): Boolean;
class function ClassParent: TClass;
class function ClassInfo: Pointer;
class function InstanceSize: Longint;
class function InheritsFrom(AClass: TClass): Boolean;
procedure Dispatch(var Message);
class function MethodAddress(const Name: ShortString): Pointer;
class function MethodName(Address: Pointer): ShortString;
function FieldAddress(const Name: ShortString): Pointer;
function GetInterface(const IID: TGUID; out Obj): Boolean;
class function GetInterfaceEntry(const IID: TGUID): PInterfaceEntry;
class function GetInterfaceTable: PInterfaceTable;
function SafeCallException(ExceptObject: TObject;
ExceptAddr: Pointer): Integer; virtual;
procedure DefaultHandler(var Message); virtual;
class function NewInstance: TObject; virtual;
procedure FreeInstance; virtual;
destructor Destroy; virtual;
end;
보시다시피 우리가 사용한 Create와 Free라는 메소드가 처음에 나타나고 그 외에도 여러 메소드가 있음을 알 수 있습니다. 여기서 주의 깊게 살펴볼 내용으로 Create라는 메소드의 형태가 procedure도 function도 아닌 constructor라는 키워드로 정의되어 있음을 볼 수 있습니다. 그리고 마지막 부분에 destructor라는 키워드로 시작하는 Destroy 라는 메소드도 있습니다. constructor라고 정의된 이 메소드를 생성자라고 하며 destructor라고 정의된 메소드를 소멸자라고 부릅니다. 후손 클래스에서 특별히 이런 생성자나 소멸자를 위한 메소드를 준비하지 않았다면 이들이 호출될 경우 자동으로 조상의 그것이 호출됩니다. 그런데 위에서 이상한 점이 한가지 더 있습니다. 분명 TObject의 소멸자는 Destroy 인데 왜 Free라는 메소드를 호출하여 오브젝트를 제거하였을까요. 물론 Free라는 메소드 대신 Destroy라는 메소드를 사용하더라도 오류는 없습니다. Free라는 메소드를 사용할 이유는 오브젝트가 할당 되었는지를 검사하는 루틴이 필요하였기 때문입니다. 만약 할당도 되어있지 않은 오브젝트를 제거하려 한다면 시스템은 오류를 표시할 것입니다. Free라는 메소드는 이런 경우를 대비하여 오브젝트가 할당되었는지를 검사하고 할당이 되었다면 즉시 Destroy를 호출하게 됩니다. 즉 Free라는 메소드를 호출하는 것은 Destroy라는 소멸자를 호출하는 것과 마찬가지의 기능을 하게 됩니다.
생성자(constructor)와 소멸자(destructor)
오브젝트를 위한 메모리를 할당하기 위해서, 우리는 Create 메소드를 호출했습니다. 그러나, 우리는 실질적으로 오브젝트를 사용하기 이전에 종종 필드들을 초기화 시켜주어야 할 경우가 있습니다. 위에서 우리가 사용한 TMyDate의 경우 SetValue라는 메소드를 통해서 오브젝트를 생성한 후 나중에 초기화 하였습니다.
다른 방법으로, 우리는 직접 만든 생성자를 사용할 수도 있습니다. 이 새로 만든 생성자에 몇 개의 인수를 지정하도록 하여 이 오브젝트를 초기화 할 수 있습니다. 다른 점은 procedure라는 키워드 대신 앞에서 잠깐 언급한 constructor라는 키워드를 사용하면 됩니다.
constructor는 특별한 프로시져로 클래스에 적용하면 델파이는 자동적으로 그 클래스의 오브젝트를 위한 메모리를 할당하기 때문입니다. 우리가 생성자를 직접 만드는 이유는 그 클래스의 데이터를 초기화시키기 위해서 입니다. 오브젝트를 초기화 하지 않을 경우 예기치 못한 에러를 발생시킬 수도 있기 때문입니다. 생성자와 마찬가지로 소멸자도 새로 지정할 수 있습니다. 이것은 destructor라는 키워드로 선언되는 프로시져로로 생성자를 통해서 생성된 메모리를 시스템에 반환 합니다. 만약 클래스 내부에서 사용하는 어떤 메모리가 추가로 필요할 경우에도 생성자 내에서 할당받고 소멸자에서 반환하는 과정을 거치면 됩니다.
그럼 위의 TMyDate라는 클래스에 새로운 생성자를 지정하도록 해보겠습니다.
Type
TMyDate = class(TObject)
Year,Month,Day: integer;
constructor Init(y,m,d:integer);
procedure SetValue(y,m,d:integer);
function IsLeapYear: boolean;
end;
// 클래스의 세 필드의 값을 초기화 하는 프로시져
constructor TMyDate.Init(y,m,d:Integer);
begin
Year := y;
Month := m;
Day := d;
end;
여기서 생성자의 이름을 Create가 아닌 Init로 지정하였습니다. 즉, 생성자의 이름은 무엇이어도 상관이 없습니다. 하지만 일반적으로 create라는 이름을 사용하는 것이 좋습니다. 여기서 중요한 것은 이름이 아니라 constructor라는 키워드 입니다.
새로 만든 생성자를 사용하는 예제는 다음과 같습니다.
var
ADay: TMyDate;
bLeap: boolean;
begin
ADay := TMyDate.Init(1999,2,22); // 오브젝트를 생성하고 동시에 초기값 지정
bLeap := ADay.IsLeapYear; // 메소드 함수를 통해 윤년인지 여부를 얻는다.
...
ADay.Free; // 오브젝트를 제거
end;
클래스 멤버의 가시성 지정 (클래스의 정보 은폐)
클래스 내부에는 여러가지 자료를 가질 수 있습니다. 그리고 이 자료를 사용하는데에는 특별한 주의가 필요한 경우도 있습니다. 예를 들어 앞에서 우리가 사용한 TMyDate 클래스의 경우, 월과 일에 해당하는 곳에 2월 30일처럼 존재하지 않는 날짜를 지정하는 경우도 있을 겁니다. 이처럼 내부의 필드를 직접 조작하면 잘못된 결과를 발생시킬 수 있는 경우 객체지향 방법에서는 이런 데이터를 클래스안에 은폐(또는 캡슐화)하여야 합니다. 갭슐화의 개념은 클래스를 보이는 부분과 보이지 않은 부분으로 나누어 주어 보이는 부분이 나머지 보이지 않는 부분을 조작할 수 있도록 해주는 것입니다. 그렇게 하여 오브젝트를 사용할 때 코드는 대부분 감추어져 있게되고 오브젝트의 내부 데이터를 알 수도 없고, 접근할 수도 없게 합니다. 이런 허가되지 않고 접근할 수 없는 영역은 보이는 부분에서 제공하는 메소드를 사용하는 것으로 처리 하도록 합니다. 이것이 고전적 프로그래밍에 대한 객체지향 방법에서 말하는 '정보 은폐'라는 방법입니다.
클래스에는 이렇든 보이거나 보이지 않는 부분을 지정할 수 있는 방법을 제공합니다.
이들 대상에는 필드뿐 아니라 메소드나 프로퍼티를 포함한 모든 멤버에 대해 외부에서 그것을 참조할 수 있는지의 여부를 상세히 지정할 수 있습니다.
이를 위하여 클래스에는 private, protected, public, published의 지시자가 제공됩니다.
지금부터 이들 지시자 별로 그 의미와 적용대상에 대하여 설명하겠습니다.
private:
선언하는 클래스에서 내부적으로 사용할 용도로 선언한 멤버를 보통 Private으로 선언합니다. 이렇게 하면 해당 클래스의 후손은 물론 외부에서 멤버를 전혀 참조하지 못하기 때문에 내부적인 기능이 완전히 보호됩니다.
하지만 여기에 델파이만의 예외가 있습니다.
그것은 같은 유닛안에 있는 클래스의 모든 멤버들은 public으로 선언된 것과 마찬가지 처럼 취급됩니다. 실제 하나의 유닛은 한 사람이 설계하기 때문에 굳이 클래스 단위로 가기성을 강력하게 규제할 필요가 없습니다. 그래서 위에서 말한 외부란 다른 유닛을 의미합니다.
이런 이유로 관련된 클래스들은 하나의 유닛안에 모아서 선언하면 보다 유연한 클래스 설계가 가능합니다.
protected:
protected로 선언된 멤버는 선언된 그 클래스는 물론이고 계승된 모든 후손 클래스에서도 참조가 허용됩니다. 주로 계승된 클래스에서 내부적으로 사용할 것을 목표로 제공되는 멤버들을 protected로 선언합니다.
public:
가시성에 제한을 주지 않는 것으로 후손이나 외부의 모든 코드에서 접근이 가능합니다.
그 클래스에서 제공하고자 하는 주요 기능들이 대개 public으로 선언됩니다.
published:
published는 가시성에 있어서는 public과 완전히 동일합니다. 이는 특별히 컴포넌트를 작성하는 데 중요한 역할을 합니다. 보통 컴포넌트의 published 부분은 아무 필드나 메소드도 없고, 새로운 요소인 프로퍼티(Property:속성)을 갖고 있습니다. 우리가 Object Inspector에서 보는 컴포넌트의 모든 속성은 published의 가시성을 가집니다. 컴포넌트이 속성중 Published로 지정된 속성만이 Object Inspector에 나타나며 이들 속성과 연관된 필드의 정보만이 편집을 마친후 .dfm 파일에 저장되고 다시 적재될 수 있습니다. 그리고 모든 이벤트에 할당되는 메소드도 published 로 지정되어야 합니다.
다음은 이들의 관계를 보여주는 예제 입니다.
// 첫번째 유닛..
Unit First_Unit;
...
type
TA = class
private
priA: integer;
protected
proA: integer;
public
pubA: integer;
end;
TB = class(TA) // TA에서 상속된 클래스
public
procedure Access;
end;
TC = class(TObject) // TA와 무관한 클래스
public
procedure Access(v: TA);
end;
procedure TB.Access;
begin
priA := 1; // OK .. 같은 Unit 이므로 접근이 가능하다.
proA := 2; // OK
pubA := 3; // OK
end;
procedure TC.Access(v: TA);
begin
v.priA := 1; // OK .. 같은 Unit 이므로 접근이 가능하다.
v.proA := 2; // OK .. 같은 Unit 이므로 접근이 가능하다.
v.pubA := 3; // OK
end;
// 두번째 유닛
Unit Second_Unit;
uses
First_Unit,...;
type
TD = class(TA)
public
procedure Access;
end;
TE = class(TObject)
public
procedure Access(v: TA);
end;
procedure TD.Access;
begin
priA := 1; // Error .. 다른 Unit 이므로 접근 불가능
proA := 2; // OK .. 상속된 클래스 이므로 접근이 가능
pubA := 3; // OK
end;
procedure TE.Access(v: TA);
begin
v.priA := 1; // Error .. 외부 클래스 이므로 불가능
v.proA := 2; // Error .. 외부 클래스 이므로 불가능
v.pubA := 3; // OK
end;
서로 참조하는 클래스
다음 코드와 같이 두 개의 서로 다른 클래스의 선언에서 서로 다른 클래스를 멤버로 가지는 경우가 있습니다.
이런 경우 뒤에 나타나는 클래스를 두 클래스의 선언보다 앞에 이름만 선언하여 줍니다.
type
TFigure = class; // 뒤에 선언되는 TFigure를 선언해 둔다..
TDrawing = class
Figure: TFigure;
// 만약 앞부분에 TFigure를 선언하지 않았다면
// 뒤에 선언될 TFigure가 클래스인지 여부를 알 수 없어
// 에러를 발생 시킨다.
...
end;
TFigure = class
Drawing: TDrawing;
// 클래스의 선언에 앞서 TDrawing 클래스가
// 선언되었으므로 문제가 없다
...
end;
상속,형 호환성,동적바인딩(dynamic binding)과 다형성(polymorphism)
동적바인딩 또는 다형성은 간단히 말해서 조상 클래스에서 정의된 이름과 동일한 프로시져 또는 함수를 상속된 후손 클래스에서 같은 이름으로 다시 지정할 수 있음을 말합니다.
단어적인 의미로 실행할 메소드의 주소가 실행시에 결정되는 것을 말합니다.
어떤 메소드의 호출문을 작성하고 그것을 변수에 대입해도, 어느 메소드가 호출될지는 그 변수에 관계된 오브젝트의 형에 따라 달라진다는 것 입니다. 즉, 델파이가 실행시까지 그 변수가 참조하는 오브젝트의 실제 클래스를 결정할 수 없는데 이는 형 호환성 규칙 때문입니다.
여기서 호환성 규칙은 어떤 클래스 A에서 상속된 클래스 B는 클래스 B형이기도 하고 동시에 클래스 A 형이기도 하다는 것 입니다. 일반적인 규칙으로, 매번 조상 클래스의 오브젝트가 요구될 때마다 자손 클래스의 오브젝트를 사용할 수 있습니다. 그러나 그 역은 성립하지 않습니다. 자손 클래스가 필요할 때 조상 클래스를 사용할 수는 없는 것입니다. 즉, 클래스 A를 인자로 요구하는 함수가 있을 때 클래스 B를 넘겨도 된다는 겁니다. 하지만 클래스 B를 요구하는 함수에 클래스 A를 넘길 수는 없습니다.
만약 클래스 A에도 abc라는 이름의 메소드가 있고 클래스 A에서 상속된 클래스 B와 C에도 abc라는 함수가 있다면 이들 클래스 A를 인수로 전달받는 코드에서는 전달된 클래스가 클래스 A인지 아니면 클래스 B 또는 C인지를 구분하여 클래스 A의 abc메소드를 호출할지 아니면 클래스 B나 C의 abc메소드를 호출할지 결정하여야 합니다. 말로는 너무 햇갈리니 간단란 예제를 들도록 하겠습니다.
다음의 세가지의 클래스를 정의 합니다.
type
TAnimal = class // (TObject)가 생략되었음..
public
function Verse: string; virtual;
end;
TDog = class(TAnimal)
public
function Verse: string; override;
end;
TCat = class(TAnimal)
public
function Verse: string; override;
end;
function TAnimal.Verse: string;
begin
Result := '';
end;
function TDog.Verse: string;
begin
Result := '멍~멍~';
end;
function TCat.Verse: string;
begin
Result := '야옹~';
end;
위와 같이 TAnimal, TDog, TCat이라는 클래스가 선언된 상태에서 다음의 예제를 살펴보겠습니다.
다음은 index에 따라 해당하는 클래스의 Verse를 구하는 함수입니다.
function GetVerseOf(index: integer): string;
var
AAnimal,SomeAnimal: TAnimal;
ADog: TDog;
ACat: TCat;
begin
case index of
0:
begin
AAnimal := TAnimal.Create;
SomeAnimal := AAnimal;
end;
1:
begin
ADog := TDog.Create;
SomeAnimal := ADog;
end;
2:
begin
ACat := TCat.Create;
SomeAnimal := ACat;
end;
else
Result := 'Not found..'
Exit;
end;
Result := SomeAnimal.Verse;
case index of
0:
AAnimal.Free;
1:
ADog.Free;
2:
ACat.Free;
end;
end;
TAnimal과 TDog, TCat에는 Verse라는 메소드를 가지고 있습니다. 이 메소드는 TAnimal 클래스에서는 virtual로 선언되었고 TDog와 TCat 클래스에서는 override로 선언되었습니다. 위에서 말한 호환성 규칙에 따라 ADog와 ACat은 SomeAnimal이라는 변수에 대입이 가능합니다. 참고로 위의 함수는 다음과 같이 간략히 표현될 수 있습니다.
function GetVerseOf(index: integer): string;
var
SomeAnimal: TAnimal;
begin
case index of
0:
SomeAnimal := TAnimal.Create;
1:
SomeAnimal := TDog.Create;
2:
SomeAnimal := TCat.Create;
else
Result := 'Not found..'
Exit;
end;
Result := SomeAnimal.Verse;
SomeAnimal.Free;
end;
이제 위의 예제에서 SomeAnimal.Verse라는 호출문은 어떤 결과를 가져올까요? index라는 값이 무엇이냐에 따라 다릅니다. 만약 SomeAnimal에 TAniaml 클래스의 오브젝트를 가리킨다면, 그것은 TAnimal의 Verse를 호출할 것입니다. 만약 이것이 TDog 클래스의 오브젝트를 가리킨다면, 대신 TDog의 Verse를 호출할 것입니다. 이것은 바로 이 함수가 virtual이기 때문입니다. 조상 메소드가 virtual 일 경우, 후손 클래스에서 이 메소드가 새로 정의되었다면 후손에 정의된 메소드를 호출하게 됩니다. 이때 후손 메소드는 override로 지정하여 이 메소드가 조상 메소드를 대치하는 것임을 지정하여야 합니다. 만일 후손 메소드를 override로 지정하지 않으면 정적 메소드로 정의되어 동적 바인딩을 할 수가 없게 됩니다. 이때 중복되는 이 메소드의 파라미터는 동일하여야 한다는 것을 기억하시기 바랍니다.
다음의 예제를 보시기 바랍니다.
type
TClassA = class
procedure one; virtual;
procedure Two;
end;
TClassB = class(TClassA)
procedure one; Override;
procedure Two;
end;
procedure TClassA.One;
begin
ShowMessage('TClassA.One');
end;
procedure TClassA.Two;
begin
ShowMessage('TClassA.Two');
end;
procedure TClassB.One;
begin
ShowMessage('TClassB.One');
end;
procedure TClassB.Two;
begin
ShowMessage('TClassB.Two');
end;
procedure First;
var
cls: TClassA;
begin
cls := TClassB.Create;
cls.One; // 'TClassB.One'이 출력된다.
cls.Two; // 'TClassA.Two'가 출력된다.
cls.Free;
end;
procedure Second;
var
cls: TClassB;
begin
cls := TClassB.Create;
cls.One; // 'TClassB.One'이 출력된다.
cls.Two; // 'TClassB.Two'가 출력된다.
cls.Free;
end;
위의 예제는 정적인 재정의와 중복 재정의가 어떠한 차이를 나타내는지 보여줍니다.
중복된 함수는 클래스형을 조상 클래스로 지정하여도 자동으로 후손의 함수를 호출하여 줍니다. 하지만 정적으로 재정의된 함수는 클래스 형이 무엇이냐에 따라 호출되는 함수가 달라짐을 알 수 있습니다.
어떤 메소드를 중복하는데는 두 가지의 서로 다른 방법이 있습니다. 하나는 조상 클래스의 메소드를 새 버젼으로 바꾸는 것이고, 다른 하나는 기존의 메소드에 코드를 추가하는 것입니다. 첫번째 방법은 위의 예제와 같은 방법입니다. 그럼 두번째 방법은 어떻게 하여야 할까요. 이 경우에는 조상 클래스의 메소드로 실행하고 동시에 후손의 클래스도 실행되어야 합니다. 이렇게 하려면 조상 클래스와 같은 메소드를 호출하기 위해 inherited 키워드를 사용하면 됩니다. 위의 예제의 TClassB.One을 다음과 같이 하면 됩니다.
procedure TClassB.One;
begin
inherited one; // <<<------ 조상 클래스 TClassA의 Procedure one을 호출한다.
// NewCode..
ShowMessage('TClassB.One');
end;
이때 새로운 코드의 위치는 상황에 따라 inherited 를 호출하는 위치 앞 또는 뒤가 될 수 있습니다. 참고로 virtual이라는 키워드와 거의 동일한 기능을 하는 dynamic이라는 키워드가 있습니다. 이 두키워드의 구문은 완전히 똑같고, 그 사용 결과도 같습니다. 다른 점은 컴파일러가 동적 바인딩을 구현하는 내부적인 메커니즘의 차이가 있습니다. virtual은 가상 메소드 테이블에 기반을 두는 반면, dynamic은 메소드를 가리키는 고유한 번호를 사용합니다. 속도면에서는 virtual이 빠르고 메모리 절약면에서는 dynamic이 좋습니다.
프로그래머의 입장에서 이 중 어느 것을 사용할 지 결정하는 규칙은 다음과 같습니다.
1. 만약 메소드가 거의 모든 자손 클래스에서 중복된다면, Virtual을 사용한다.
2. 만약 메소드가 거의 반복되지 않지만, 그러나 여진히 유연성을 위하여 동적 바인딩을 필요로 한다면, dynamic을 선택한다. 특히 자손 클래스가 많다면 이렇게 하는 편이 좋다.
3. 만약 메소드가 단위시간 동안 많이 호출되는 경우라면, virtual로 한다.
그렇지 않은 경우는 dynamic을 쓰는 것과 실제 속도차가 거의 없다.
생성자/소멸자에서의 동적 바인딩
생성자와 소멸자는 보통 Virtual로 지정되어 작성됩니다 (B.U.T. 그러나 항상 그런것은 아닙니다). 앞에서 보여드린 TObject의 선언에서도 소멸자인 destroy 메소드가 virtual로 지정되었음을 볼 수 있습니다. 보통 생성자와 소멸자에서는 조상 클래스의 생성자와 소멸자를 inherited 지시어를 이용해서 호출하여 줍니다. 이렇게 하는 이유는 조상 클래스에서 초기화 하거나 삭제해야할 코드를 실행 시키기 위해서 입니다. 생성자의 경우 조상 클래스의 생성자를 먼저 호출하고 후손 클래스의 초기화를 진행합니다. 소멸자의 경우 이와 반대로 후손 클래스의 작업를 정리한 후 조상클래스의 소멸자를 호출합니다. 다음은 생성자와 소멸자 코드의 예제 입니다.
constructor TMyClass.Create;
begin
inherited Create; // 먼저 조상의 생성자를 호출
... // 초기화 코드..
end;
destructor TMyClass.Destroy;
begin
... // 정리 코드
inherited Destroy; // 마지막에 조상 의 소멸자를 호출
end;
컴포넌트와 관련하여 실제 델파이의 코드를 보겠습니다..
우리가 앞으로 사용하게 될 TWinControl이라는 클래스는 다음과 같은 상속관계를 가집니다.
TObject
|
TPersistent
|
TComponent
|
TControl
|
TWinControl
이들의 생성자 코드를 살펴보면 다음과 같습니다.
// TObject의 생성자
constructor TObject.Create;
begin
end;
// TPersistent는 TObject의 생성자를 그대로 사용
// TComponent의 생성자
constructor TComponent.Create(AOwner: TComponent);
begin
FComponentStyle := [csInheritable];
if AOwner <> nil then AOwner.InsertComponent(Self);
end;
// TControl의 생성자
constructor TControl.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FWindowProc := WndProc;
FControlStyle := [csCaptureMouse, csClickEvents, csSetCaption,
csDoubleClicks];
FFont := TFont.Create;
FFont.OnChange := FontChanged;
FColor := clWindow;
FVisible := True;
FEnabled := True;
FParentFont := True;
FParentColor := True;
FParentShowHint := True;
FIsControl := False;
FDragCursor := crDrag;
end;
// TWinControl의 생성자
constructor TWinControl.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FObjectInstance := MakeObjectInstance(MainWndProc);
FBrush := TBrush.Create;
FBrush.Color := FColor;
FParentCtl3D := True;
FTabOrder := -1;
FImeMode := imDontCare;
FImeName := Screen.DefaultIme;
end;
코드에서 볼 수 있듯이 어떤 클래스의 조상클래스에는 조상클래스 고유의 생성자가 있고 그 클래스를 위한 초기화 작업이 여기에서 이루어 집니다. 따라서 조상 클래스가 올바르게 초기화될 수 있도록 하기 위해서는 위의 생성자들의 첫줄에서 처럼 바로 윗조상 클래스의 생성자를 호출해 주어야 합니다.
위에서 TComponent의 생성자에서는 조상의 생성자를 호출하지 않고 있는데 이것은 조상인 TPersistent와 TObject의 생성자에서 아무일도 하지 않기 때문입니다. 그러나 TControl의 생성자의 경우 조상인 TComponent의 생성자에서 어떤 초기화 작업이 이루어지므로 이를 호출해 주었습니다.
추상 메소드 (Abstract method)
후손 클래스에는 공통적으로 필요하지만 조상 클래스에는 굳이 필요없는 동적 바인딩 해야할 메소드가 있을 수 있습니다. 앞에서 보여드린 예제중 TAniaml의 Verse메소드가 그런 경우일 수 있습니다. 즉 후손에서 중복된 메소드만이 호출되고 조상의 메소드가 호출될 경우가 없을 경우 굳이 조상인 TAnimal의 Verse 메소드를 정의할 필요가 없습니다. 이렇듯 조상 클래스 변수에 후손 클래스의 오브젝트를 지정해 두고 동적으로 메소드를 호출해야하는 경우, 조상 클래스에 빈 메소드를 정의할 필요없이 메소드 이름만을 선언하고 abstract지시어로서 표시하면 자리만 만들어두는 메소드가 된다. 이 경우 abstract 메소드는 당연히 직접 호출되어서는 안 되고, 후손 클래스의 오브젝트에서 중복하여 정의한 메소드만이 호출될 수 있다. 만약 abstract 로 지정된 조상 클래스의 메소드를 직접 호출하여 프로그램을 종료시켜야 하는 오류가 발생됩니다.
다음은 abstract를 사용한 TAnimal 클래스의 선언입니다.
type
TAnimal = class // (TObject)가 생략되었음..
public
function Verse: string; virtual; abstract;
// 추상형으로 선언되었음..
// 따라서 TAnimal.Verse라는
// 메소드 코드를 작성하지 않아도 된다.
end;
TDog = class(TAnimal)
public
function Verse: string; override;
end;
TCat = class(TAnimal)
public
function Verse: string; override;
end;
//
// 이전의 예제에서 TAniaml.Verse의 코드를 삭제했음..
//
function TDog.Verse: string;
begin
Result := '멍~멍~';
end;
function TCat.Verse: string;
begin
Result := '야옹~';
end;
메시지 핸들러 (Message Handler)
위에서 설명한 동적바인딩과 유사한 형태로 Windows의 Message를 처리 하는 방법이 있습니다. 이러한 목적을 위해 델파이는 또 다른 지시자인 message를 메시지 핸들링 메소드를 위해 제공하고 있습니다. 이 메소드는 반드시 한개의 var 파라미터만을 갖고 있어야 합니다. 그 다음에는 message 지시자와 Windows 메시지 번호에 해당하는 인덱스가 나옵니다. 예를 들면 다음과 같습니다.
type
TForm1 = class(TForm)
...
procedure WMMinMax(var Message:TMessage); message WM_GETMINMAXINFO;
end;
예제에서 파라미터로 넘겨준 TMessage형은 무엇일까요? 델파이에서는 이처럼 처리할 메시지의 종류에 따라 다루기 편리한 여러 종류의 레코드 형을 미리 선언해 두고 있습니다. 위의 TMessage형 역시 이러한 레코드 형중의 하나입니다. 그러나 프로시져의 이름인 WMMinMax나 Message의 자료형으로 어떤 것을 사용할지는 여러분이 직접 정하는 것입니다. 이러한 기능은 Windows 메시지와 API 함수를 이용하는 부분에서 유용하게 사용됩니다.
다음은 실제 윈도우 메시지를 처리하는 메시지 핸들러의 예제입니다.
type
TTextBox = class(TCustomControl)
private
procedure WMChar(var Message:TWMChar); message WM_CHAR;
...
end;
procedure TTextBox.WMChar(var Message:TWMChar);
begin
if chr(Message.CharCode)=#13 then
ProcessEnter
else
inherited;
end;
위 예제에서 네번째 줄의 message 지시어 뒤에 메시지값(WM_CHAR)을 두어, 처리할 메시지를 표시하고 있습니다. 여기서는 파라미터로 TWMChar형을 사용하였습니다. 메시지 핸들러 메소드의 마지막 부분에 inherited로 조상 클래스의 메소드를 호출하고 있습니다. 이것은 메시지 핸들러가 일종의 동적바인딩이라는 것을 보여 줍니다. 만약 조상 클래스에 이를 위한 핸들러 메소드가 없다면 TObject의 DefaultHandler를 호출하게 됩니다.
프로퍼티 (Property)
앞에서 클래스의 멤버에는 필드, 메소드, 프로퍼티가 있다고 언급한 바 있습니다. 오브젝트 파스칼에서는 필드가 안전하게 참조될 수 있도록 하기 위해 다양한 조건을 줄 수 있는 프로퍼티 기능을 제공하고 있습니다.
프로퍼티의 지정 문법은 다음과 같습니다.
property <프로퍼티명>:<프로퍼티의 자료형>
read <프로퍼티 값을 가져올 변수 혹은 함수>
write <프로퍼티 값을 대입할 변수 또는 프로지져>
default <기본값>;
필요에 따라 read, write, default 중 어떤 것들은 생략하는 경우도 있습니다.
다음은 간단한 프로퍼티의 선언 예제 입니다.
// 읽기/쓰기가 모두 가능한 프로퍼티의 예
property Age:integer read GetAge write SetAge default 0;
// 읽기만 가능한 프로퍼티의 예 (write부분이 없다)
property Age:integer read GetAge;
// 쓰기만 가능한 프로퍼티의 예 (read부분이 없다)
property Age:integer write SetAge;
이와같이 Age 프로퍼티를 선언하려면 다음과 같은 코드가 준비되어야 합니다.
type
TPerson = class
private
FAge: integer;
...
function GetAge: TColor;
procedure SetAge(v:integer);
...
published
property Age: integer read GetAge write SetAge;
end;
이렇게 만들어 지면 나중에 TPerson형의 p라는 오브젝트가 있다고 할 때, p.Age라고 하면 p의 Age 프로퍼티를 지칭하게 됩니다. 이 프로퍼티에 값을 주거나 받을 때는 다음과 같이 작성하면 됩니다.
p.Age := 나이값; // (1)
nAge := p.Age; // (2)
(1)의 경우 지정된 나이값은 write로 지정된 SetAge라는 프로시져를 통하여 처리됩니다. 반대로 (2)의 경우처럼 오브젝트의 Age 프로퍼티를 알고자 하면 read로 지정된 GetAge 함수를 통해 값을 얻게 됩니다. 이처럼 어떤 메소드를 통하여 값을 지정하거나 읽는 것은 필드에 지정되는 값에 어떤 제한을 두어 좀더 안전하게 접근하기 위해서 입니다. 다음은 위에서 언급된 메소드 GetAge와 SetAge의 예제 입니다.
function TPerson.GetAge;
begin
Result := FAge;
end;
procedure TPerson.SetAge(v: integer);
begin
if v<0 then
begin
ShowMessage('입력된 나이가 이상합니다요. (<0)');
Exit;
end
else
FAge := v;
end;
위의 예제에서는 나이를 지정하는 과정에서 0보다 작은 나이가 지정되면 에러 메시지를 보이게 하였습니다. read에 해당한 메소드인 GetAge의 경우 단순히 FAge의 값을 되돌려 줍니다. 하지만 어떤 상황에서는 read에 지정될 메소드에서 어떤 처리를 해줄 필요가 있을 수도 있습니다. 위의 경우처럼 값을 읽을 때 특별한 처리가 필요 없다면 다음과 같이 지정하여도 무관합니다.
property Age:integer read FAge write SetAge;
즉 지정된 값이 저장된 필드를 직접 연결 해주면 됩니다. 마찬가지로 Write시에도 특별한 제한이 없다면 다음과 같이 필드를 직접 연결해 주어도 됩니다.
property Age:integer read FAge write FAge;
여기에서는 Age 프로퍼티를 보관하기 위한 필드로 FAge를 사용하였지만, read와 write에 모두 메소드가 지정될 경우 필드를 사용하지 않을 수도 있습니다.
특별히 컴포넌트와 관련된 것으로 컴포넌트 클래스의 published에 나타난 프로퍼티들은 델파이의 Object Inspector에 나타나고 편집시 지정된 정보는 .dfm 파일에 저장됩니다. 즉, .dfm 파일에 저장되어야 하는 프로퍼티들의 가시성은 published로 설정되어야 합니다.
런타임 형 정보 (Run-Time Type Information)
앞부분에서 오브젝트 파스칼의 형 호환성 규칙에 의해, 조상 클래스가 요구될 때 자손 클래스를 이용할 수 있지만 그 역은 성립하지 않는다고 설명드렸습니다. 앞의 예제에서 TDog나 TCat에는 Eat 이라는 메소드가 있지만, TAnimal에는 없다고 가정해 봅시다. 만약 TAnimal형의 변수 MyAnimal가 TDog의 오브젝트를 가리키고 있다면, Eat이라는 메소드를 호출할 수 있어야 합니다. 하지만 어떤 경우에 MyAnimal에는 TAnimal의 오브젝트가 지정될 수도 있습니다. 이 경우에는 Eat 메소드를 호출할 수 없습니다. 만약 TAnimal이 지정된 상태에서 Eat를 호출한다면 프로그램은 런타임 에러를 보이며 투덜댈 것입니다. 이 문제를 해결하기 위해서는 현재 변수 MyAnimal이 가리키는 오브젝트가 TDog 인지 아니면 TAnimal인지를 알아야 할 것입니다. 델파이는 이런 문제를 위해서 is 와 as 라는 연산자를 제공하여 줍니다.
먼저 is 연산자는 파라미터로 클래스의 오브젝트와 클래스 형을 가지면 Boolean형의 리턴값을 돌려 줍니다.
다음은 is 연산자의 사용 예제 입니다.
if MyAnimal is TDog then 어쩌고 저쩌고...; 이 예제의 내용을 MyAnimal이라는 변수가 가리키는 오브젝트가 TDog 클래스 형인지를 판단해줍니다. 이 경우 MyAnimal이 TDog형이거나 TDog 형을 상속받은 클래스 형이하면 결과는 True입니다. 이렇게 하여 MyAnimal이 가리키는 오브젝트가 TDog와 관련있음을 확인하면, 다음과 같이 타입 캐스트(Type-Cast: 형 변환)을 하여 사용할 수 있습니다.
// 변수 MyDog는 TDog형일 때..
if MyAnimal is TDog then
begin
MyDog := TDog(MyAnimal); // MyAnimal을 TDog형으로 형변환
Text := MyDog.Eat;
end;
두번째 연산자 as도 오브젝트와 클래스 형을 파라미터로 가지며, 연산 결과는 새로운 클래스 형으로 변환된 오브젝트 입니다.
다음은 as에 대한 예제 입니다.
// 변수 MyDog는 TDog형일 때..
if MyAnimal is TDog then
begin
MyDog := MyAnimal as TDog;
Text := MyDog.Eat;
end;
as 연산자의 사용에 있어서 만약 오브젝트의 형이 새로 캐스트하려는 형과 호환되지 않는다면 예외상태를 발생 시킵니다. 이때 발생되는 예외는 EInvalidCast 입니다. 이런 예외를 피하려면 마지막 예제처럼 is 연산자를 통해 클래스 형을 확인한 후 사용하면 됩니다. 이 두가지 연산자는 컴포넌트 제작시 종종 사용되므로 숙지하시기 바랍니다.
좀 그렇긴 하지만, 쉬운 코드들이니 차근히 보시면 되리라 생각이 된다.
그럼 강좌 원문 나갑니다.
델마당, 델코, 한델 또는 어딘가에서 가져온 걸텐데.. -_-
---------------------------------------------------------------------
컴포넌트 제작을 위한 기초 지식 - 클래스 (Class)
컴포넌트를 만들기 전에 여러분은 클래스(class)라는 자료 구조를 알아야 합니다.
본문의 내용은 파스칼의 기본 문법을 어느 정도 숙지하고 있는 것으로 간주하여 설명합니다. 이미 오브젝트 파스칼의 클래스를 아시거나 C++에서 클래스를 사용해 보신 분이라면 가볍게 읽고 넘어가셔도 됩니다.
클래스(class)란?
클래스는 오브젝트 파스칼의 가장 큰 특징입니다.
자료구조만 보면 레코드형(레코드 형을 모르시는 분은 관련 서적의 자료형을 잠시 둘러 보시길)과 거의 유사하지만 훨씬 강력한 여러가지 기능을 가지고 있습니다. 우리가 앞으로 함께 만들어 볼 컴포넌트도 일종의 클래스라고 볼 수 있습니다. 따라서 반드시 클래스의 기본 개념을 알고 넘어가야 하겠습니다.
클래스는 C의 구조체(struct)나 파스칼의 레코드(record)형처럼 여러개의 요소들로 이루어진 자료들의 모음입니다. 레코드형에서는 레코드 내부에 정의된 요소들(필드(field)라고 부름)을 접근할 때 직접 접근하지만 클래스에서는 이들을 직접 참조하는 대신 클래스가 제공하는 프로시져나 함수를 통해서 작업을 처리합니다. 이런 프로시져나 함수를 메소드라고 합니다. 또 필드를 참조하기 위한 한 방법으로 프로퍼티라는 요소를 가집니다. 이렇게 한 클래스에 속한 필드,메소드,프로퍼티등을 클래스의 멤버(Member)라고 부릅니다.
오브젝트, 인스턴스란?
클래스는 일종의 자료형이고 정의한 변수 자체로는 아직 오브젝트가 아닙니다. 이 변수는 일종의 Pointer형 변수라고 볼 수 있습니다. 오브젝트로 되기 위해서는 클래스를 위한 메모리가 할당되어 변수에 대입되는 인스턴스 과정이 필요합니다. 이런 과정을 생성(Create)이라고 부릅니다. 이렇게 생성된 것을 오브젝트 또는 인스턴스라고 부릅니다. 오브젝트의 사용이 끝나면 사용된 메모리를 시스템에 반납하는 파괴(destroy)라는 과정을 거쳐야 합니다. 클래스 변수는 실제로 Pointer형 변수와 거의 같지만 소스코드를 작성할 때 꺾쇠(^)를 쓰지 않아도 됩니다. 그리고 New 이나 Dispose 를 사용하는 대신 클래스 내부에 정의된 생성자와 소멸자를 이용하여 메모리를 할당 합니다.
조상(Ancestor), 후손(Descendent)
클래스의 또 하나의 특징은 다른 클래스를 상속하여 새로운 클래스를 정의할 수 있다는 것 입니다. 즉 A라는 클래스가 가지는 모든 요소를 가지면서 새로 몇가지 요소가 추가된 B라는 클래스를 만들 수 있습니다. 이때 새로 선언된 B라는 클래스를 A의 후손(Descendent)라고 하고, 참조된 A클래스를 B클래스의 조상(Ancestor)이라고 합니다. 후손 클래스는 새로운 멤버를 추가할 수 있고 조상에서 정의된 멤버를 제거 할 수는 없습니다. 하지만 메소드에 한해서 어떤 것은 나중에 설명될 중첩이라는 방법으로 후손의 후손의 메소드로 대치가 가능 합니다. 모든 클래스는 계승받을 조상을 지정하게 되어있습니다. 조상 클래스를 생략하는 경우 기본적으로 TObject라는 델파이의 기본 클래스를 조상으로 가집니다. 이 TObject라는 클래스는 모든 클래스의 가장 높은 조상이되며 모든 오브젝트가 가질 기본적인 기능을 구현해 놓은 클래스 입니다.
클래스의 선언과 사용
클래스는 Type절에서 선언합니다. 다음은 한 예 입니다.
Type
TMyDate = class(TObject)
Year,Month,Day: integer;
end;
위의 예는 다음과 같은 구조를 가지고 있습니다.
클래스 이름 = class (조상 클래스)
멤버 선언
end;
클래스를 선언할 때는 예약어 class를 사용하며 괄호안에 조상 클래스를 표기합니다. 조상이 TObject라면 괄호를 포함하여 생략이 가능합니다. 이후부터 end까지는 멤버들을 선언합니다. (참고: 클래스 이름을 T로 시작하는 것은 델파이의 관례일 뿐이며 실제로 어떤 문자로 시작하여도 상관이 없습니다. 그러나 클래스명을 T로 시작하면 자료형을 구분하기가 편해지므로 관례에 따르는 편이 좋습니다.)
위에서 선언된 TMyDate의 선언은 레코드 형의 선언과 비슷합니다. 세 필드를 접근하는 방법도 동일 합니다. 자 이제 TMyDate를 생성하고 조작하는 간단한 예제를 보이겠습니다.
var
ADay: TMyDate; // TMyDate형의 변수를 선언.. (일종의 Pointer변수라고 볼 수 있다)
begin
// TMyDate 오브젝트를 생성하고 그 오브젝트를 변수 ADay에 지정한다.
ADay := TMyDate.Create; // ADay.Create가 아님을 기억하기 바람..
// 여기서부터 ADay라는 변수에 할당된 TMyDate 오브젝트를 사용하면 된다.
ADay.Year := 1999;
ADay.Month := 2;
ADay.Day := 22;
....
// ADay라는 변수에 할당된 TMyDate 오브젝트를 제거한다.
ADay.Free;
end;
여기까지는 정말 레코드형과 다를 바가 없습니다.
한가지 다른 점은 TMyDate.Create와 Aday.Free등의 코드 입니다.
이 내용은 아래에서 설명하겠습니다.
이제부터 클래스에 대한 작업을 추가하기 위하여 메소드를 작성하여 보겠습니다.
위에서 만든 TMyDate라는 클래스에 SetValue와 IsLeapYear라는 메소드를 추가합니다.
Type
TMyDate = class(TObject)
Year,Month,Day: integer;
procedure SetValue(y,m,d:integer);
function IsLeapYear: boolean;
end;
// 클래스의 세 필드의 값을 초기화 하는 프로시져
procedure TMyDate.SetValue(y,m,d:Integer);
begin
Year := y;
Month := m;
Day := d;
end;
// 초기화된 년도가 윤년인지 아닌지를 구하는 함수
function TMyDate.IsLeapYear: boolean;
begin
if (Year mod 4 <> 0) then
Result := False
else if (Year mod 100 <>0) then
Result := True
else if (Year mod 400 <>0) then
Result := False
else
Result := True;
end;
다음은 이렇게 작성된 메소드를 사용하는 예제 입니다.
var
ADay: TMyDate;
bLeap: boolean;
begin
ADay := TMyDate.Create; // 오브젝트를 생성
ADay.SetValue(1999,2,22); // 메소드 프로시져를 통해 초기값 지정
bLeap := ADay.IsLeapYear; // 메소드 함수를 통해 윤년인지 여부를 얻는다.
...
ADay.Free; // 오브젝트를 제거
end;
위의 예제들에서 보셨듯이 클래스는 레코드 형과는 다르게 메소드라는 멤버가 존재 합니다. 이들 메소드의 접근에서도 필드들의 접근에서와 같이 <클래스이름>.<메소드>의 형태를 가집니다.
여기서 앞에서 설명하기로 한 Create와 Free라는 함수에 대하여 설명하겠습니다.
Create는 일종의 생성자 입니다. 이 함수를 호출하면 델파이는 TMyDate형의 자료를 보관할 메모리를 시스템으로부터 할당 받습니다. 그러나 TMyDate에는 Create나는 메소드가 존재하지 않습니다.
그럼 과연 이 메소드는 어디에 있을까요?
여기에서 우리는 TMyDate가 TObject의 내용을 물려 받았다는 것을 기억하여야 합니다.
다음은 TObject 클래스의 정의 내용 입니다.
TObject = class
constructor Create;
procedure Free;
class function InitInstance(Instance: Pointer): TObject;
procedure CleanupInstance;
function ClassType: TClass;
class function ClassName: ShortString;
class function ClassNameIs(const Name: string): Boolean;
class function ClassParent: TClass;
class function ClassInfo: Pointer;
class function InstanceSize: Longint;
class function InheritsFrom(AClass: TClass): Boolean;
procedure Dispatch(var Message);
class function MethodAddress(const Name: ShortString): Pointer;
class function MethodName(Address: Pointer): ShortString;
function FieldAddress(const Name: ShortString): Pointer;
function GetInterface(const IID: TGUID; out Obj): Boolean;
class function GetInterfaceEntry(const IID: TGUID): PInterfaceEntry;
class function GetInterfaceTable: PInterfaceTable;
function SafeCallException(ExceptObject: TObject;
ExceptAddr: Pointer): Integer; virtual;
procedure DefaultHandler(var Message); virtual;
class function NewInstance: TObject; virtual;
procedure FreeInstance; virtual;
destructor Destroy; virtual;
end;
보시다시피 우리가 사용한 Create와 Free라는 메소드가 처음에 나타나고 그 외에도 여러 메소드가 있음을 알 수 있습니다. 여기서 주의 깊게 살펴볼 내용으로 Create라는 메소드의 형태가 procedure도 function도 아닌 constructor라는 키워드로 정의되어 있음을 볼 수 있습니다. 그리고 마지막 부분에 destructor라는 키워드로 시작하는 Destroy 라는 메소드도 있습니다. constructor라고 정의된 이 메소드를 생성자라고 하며 destructor라고 정의된 메소드를 소멸자라고 부릅니다. 후손 클래스에서 특별히 이런 생성자나 소멸자를 위한 메소드를 준비하지 않았다면 이들이 호출될 경우 자동으로 조상의 그것이 호출됩니다. 그런데 위에서 이상한 점이 한가지 더 있습니다. 분명 TObject의 소멸자는 Destroy 인데 왜 Free라는 메소드를 호출하여 오브젝트를 제거하였을까요. 물론 Free라는 메소드 대신 Destroy라는 메소드를 사용하더라도 오류는 없습니다. Free라는 메소드를 사용할 이유는 오브젝트가 할당 되었는지를 검사하는 루틴이 필요하였기 때문입니다. 만약 할당도 되어있지 않은 오브젝트를 제거하려 한다면 시스템은 오류를 표시할 것입니다. Free라는 메소드는 이런 경우를 대비하여 오브젝트가 할당되었는지를 검사하고 할당이 되었다면 즉시 Destroy를 호출하게 됩니다. 즉 Free라는 메소드를 호출하는 것은 Destroy라는 소멸자를 호출하는 것과 마찬가지의 기능을 하게 됩니다.
생성자(constructor)와 소멸자(destructor)
오브젝트를 위한 메모리를 할당하기 위해서, 우리는 Create 메소드를 호출했습니다. 그러나, 우리는 실질적으로 오브젝트를 사용하기 이전에 종종 필드들을 초기화 시켜주어야 할 경우가 있습니다. 위에서 우리가 사용한 TMyDate의 경우 SetValue라는 메소드를 통해서 오브젝트를 생성한 후 나중에 초기화 하였습니다.
다른 방법으로, 우리는 직접 만든 생성자를 사용할 수도 있습니다. 이 새로 만든 생성자에 몇 개의 인수를 지정하도록 하여 이 오브젝트를 초기화 할 수 있습니다. 다른 점은 procedure라는 키워드 대신 앞에서 잠깐 언급한 constructor라는 키워드를 사용하면 됩니다.
constructor는 특별한 프로시져로 클래스에 적용하면 델파이는 자동적으로 그 클래스의 오브젝트를 위한 메모리를 할당하기 때문입니다. 우리가 생성자를 직접 만드는 이유는 그 클래스의 데이터를 초기화시키기 위해서 입니다. 오브젝트를 초기화 하지 않을 경우 예기치 못한 에러를 발생시킬 수도 있기 때문입니다. 생성자와 마찬가지로 소멸자도 새로 지정할 수 있습니다. 이것은 destructor라는 키워드로 선언되는 프로시져로로 생성자를 통해서 생성된 메모리를 시스템에 반환 합니다. 만약 클래스 내부에서 사용하는 어떤 메모리가 추가로 필요할 경우에도 생성자 내에서 할당받고 소멸자에서 반환하는 과정을 거치면 됩니다.
그럼 위의 TMyDate라는 클래스에 새로운 생성자를 지정하도록 해보겠습니다.
Type
TMyDate = class(TObject)
Year,Month,Day: integer;
constructor Init(y,m,d:integer);
procedure SetValue(y,m,d:integer);
function IsLeapYear: boolean;
end;
// 클래스의 세 필드의 값을 초기화 하는 프로시져
constructor TMyDate.Init(y,m,d:Integer);
begin
Year := y;
Month := m;
Day := d;
end;
여기서 생성자의 이름을 Create가 아닌 Init로 지정하였습니다. 즉, 생성자의 이름은 무엇이어도 상관이 없습니다. 하지만 일반적으로 create라는 이름을 사용하는 것이 좋습니다. 여기서 중요한 것은 이름이 아니라 constructor라는 키워드 입니다.
새로 만든 생성자를 사용하는 예제는 다음과 같습니다.
var
ADay: TMyDate;
bLeap: boolean;
begin
ADay := TMyDate.Init(1999,2,22); // 오브젝트를 생성하고 동시에 초기값 지정
bLeap := ADay.IsLeapYear; // 메소드 함수를 통해 윤년인지 여부를 얻는다.
...
ADay.Free; // 오브젝트를 제거
end;
클래스 멤버의 가시성 지정 (클래스의 정보 은폐)
클래스 내부에는 여러가지 자료를 가질 수 있습니다. 그리고 이 자료를 사용하는데에는 특별한 주의가 필요한 경우도 있습니다. 예를 들어 앞에서 우리가 사용한 TMyDate 클래스의 경우, 월과 일에 해당하는 곳에 2월 30일처럼 존재하지 않는 날짜를 지정하는 경우도 있을 겁니다. 이처럼 내부의 필드를 직접 조작하면 잘못된 결과를 발생시킬 수 있는 경우 객체지향 방법에서는 이런 데이터를 클래스안에 은폐(또는 캡슐화)하여야 합니다. 갭슐화의 개념은 클래스를 보이는 부분과 보이지 않은 부분으로 나누어 주어 보이는 부분이 나머지 보이지 않는 부분을 조작할 수 있도록 해주는 것입니다. 그렇게 하여 오브젝트를 사용할 때 코드는 대부분 감추어져 있게되고 오브젝트의 내부 데이터를 알 수도 없고, 접근할 수도 없게 합니다. 이런 허가되지 않고 접근할 수 없는 영역은 보이는 부분에서 제공하는 메소드를 사용하는 것으로 처리 하도록 합니다. 이것이 고전적 프로그래밍에 대한 객체지향 방법에서 말하는 '정보 은폐'라는 방법입니다.
클래스에는 이렇든 보이거나 보이지 않는 부분을 지정할 수 있는 방법을 제공합니다.
이들 대상에는 필드뿐 아니라 메소드나 프로퍼티를 포함한 모든 멤버에 대해 외부에서 그것을 참조할 수 있는지의 여부를 상세히 지정할 수 있습니다.
이를 위하여 클래스에는 private, protected, public, published의 지시자가 제공됩니다.
지금부터 이들 지시자 별로 그 의미와 적용대상에 대하여 설명하겠습니다.
private:
선언하는 클래스에서 내부적으로 사용할 용도로 선언한 멤버를 보통 Private으로 선언합니다. 이렇게 하면 해당 클래스의 후손은 물론 외부에서 멤버를 전혀 참조하지 못하기 때문에 내부적인 기능이 완전히 보호됩니다.
하지만 여기에 델파이만의 예외가 있습니다.
그것은 같은 유닛안에 있는 클래스의 모든 멤버들은 public으로 선언된 것과 마찬가지 처럼 취급됩니다. 실제 하나의 유닛은 한 사람이 설계하기 때문에 굳이 클래스 단위로 가기성을 강력하게 규제할 필요가 없습니다. 그래서 위에서 말한 외부란 다른 유닛을 의미합니다.
이런 이유로 관련된 클래스들은 하나의 유닛안에 모아서 선언하면 보다 유연한 클래스 설계가 가능합니다.
protected:
protected로 선언된 멤버는 선언된 그 클래스는 물론이고 계승된 모든 후손 클래스에서도 참조가 허용됩니다. 주로 계승된 클래스에서 내부적으로 사용할 것을 목표로 제공되는 멤버들을 protected로 선언합니다.
public:
가시성에 제한을 주지 않는 것으로 후손이나 외부의 모든 코드에서 접근이 가능합니다.
그 클래스에서 제공하고자 하는 주요 기능들이 대개 public으로 선언됩니다.
published:
published는 가시성에 있어서는 public과 완전히 동일합니다. 이는 특별히 컴포넌트를 작성하는 데 중요한 역할을 합니다. 보통 컴포넌트의 published 부분은 아무 필드나 메소드도 없고, 새로운 요소인 프로퍼티(Property:속성)을 갖고 있습니다. 우리가 Object Inspector에서 보는 컴포넌트의 모든 속성은 published의 가시성을 가집니다. 컴포넌트이 속성중 Published로 지정된 속성만이 Object Inspector에 나타나며 이들 속성과 연관된 필드의 정보만이 편집을 마친후 .dfm 파일에 저장되고 다시 적재될 수 있습니다. 그리고 모든 이벤트에 할당되는 메소드도 published 로 지정되어야 합니다.
다음은 이들의 관계를 보여주는 예제 입니다.
// 첫번째 유닛..
Unit First_Unit;
...
type
TA = class
private
priA: integer;
protected
proA: integer;
public
pubA: integer;
end;
TB = class(TA) // TA에서 상속된 클래스
public
procedure Access;
end;
TC = class(TObject) // TA와 무관한 클래스
public
procedure Access(v: TA);
end;
procedure TB.Access;
begin
priA := 1; // OK .. 같은 Unit 이므로 접근이 가능하다.
proA := 2; // OK
pubA := 3; // OK
end;
procedure TC.Access(v: TA);
begin
v.priA := 1; // OK .. 같은 Unit 이므로 접근이 가능하다.
v.proA := 2; // OK .. 같은 Unit 이므로 접근이 가능하다.
v.pubA := 3; // OK
end;
// 두번째 유닛
Unit Second_Unit;
uses
First_Unit,...;
type
TD = class(TA)
public
procedure Access;
end;
TE = class(TObject)
public
procedure Access(v: TA);
end;
procedure TD.Access;
begin
priA := 1; // Error .. 다른 Unit 이므로 접근 불가능
proA := 2; // OK .. 상속된 클래스 이므로 접근이 가능
pubA := 3; // OK
end;
procedure TE.Access(v: TA);
begin
v.priA := 1; // Error .. 외부 클래스 이므로 불가능
v.proA := 2; // Error .. 외부 클래스 이므로 불가능
v.pubA := 3; // OK
end;
서로 참조하는 클래스
다음 코드와 같이 두 개의 서로 다른 클래스의 선언에서 서로 다른 클래스를 멤버로 가지는 경우가 있습니다.
이런 경우 뒤에 나타나는 클래스를 두 클래스의 선언보다 앞에 이름만 선언하여 줍니다.
type
TFigure = class; // 뒤에 선언되는 TFigure를 선언해 둔다..
TDrawing = class
Figure: TFigure;
// 만약 앞부분에 TFigure를 선언하지 않았다면
// 뒤에 선언될 TFigure가 클래스인지 여부를 알 수 없어
// 에러를 발생 시킨다.
...
end;
TFigure = class
Drawing: TDrawing;
// 클래스의 선언에 앞서 TDrawing 클래스가
// 선언되었으므로 문제가 없다
...
end;
상속,형 호환성,동적바인딩(dynamic binding)과 다형성(polymorphism)
동적바인딩 또는 다형성은 간단히 말해서 조상 클래스에서 정의된 이름과 동일한 프로시져 또는 함수를 상속된 후손 클래스에서 같은 이름으로 다시 지정할 수 있음을 말합니다.
단어적인 의미로 실행할 메소드의 주소가 실행시에 결정되는 것을 말합니다.
어떤 메소드의 호출문을 작성하고 그것을 변수에 대입해도, 어느 메소드가 호출될지는 그 변수에 관계된 오브젝트의 형에 따라 달라진다는 것 입니다. 즉, 델파이가 실행시까지 그 변수가 참조하는 오브젝트의 실제 클래스를 결정할 수 없는데 이는 형 호환성 규칙 때문입니다.
여기서 호환성 규칙은 어떤 클래스 A에서 상속된 클래스 B는 클래스 B형이기도 하고 동시에 클래스 A 형이기도 하다는 것 입니다. 일반적인 규칙으로, 매번 조상 클래스의 오브젝트가 요구될 때마다 자손 클래스의 오브젝트를 사용할 수 있습니다. 그러나 그 역은 성립하지 않습니다. 자손 클래스가 필요할 때 조상 클래스를 사용할 수는 없는 것입니다. 즉, 클래스 A를 인자로 요구하는 함수가 있을 때 클래스 B를 넘겨도 된다는 겁니다. 하지만 클래스 B를 요구하는 함수에 클래스 A를 넘길 수는 없습니다.
만약 클래스 A에도 abc라는 이름의 메소드가 있고 클래스 A에서 상속된 클래스 B와 C에도 abc라는 함수가 있다면 이들 클래스 A를 인수로 전달받는 코드에서는 전달된 클래스가 클래스 A인지 아니면 클래스 B 또는 C인지를 구분하여 클래스 A의 abc메소드를 호출할지 아니면 클래스 B나 C의 abc메소드를 호출할지 결정하여야 합니다. 말로는 너무 햇갈리니 간단란 예제를 들도록 하겠습니다.
다음의 세가지의 클래스를 정의 합니다.
type
TAnimal = class // (TObject)가 생략되었음..
public
function Verse: string; virtual;
end;
TDog = class(TAnimal)
public
function Verse: string; override;
end;
TCat = class(TAnimal)
public
function Verse: string; override;
end;
function TAnimal.Verse: string;
begin
Result := '';
end;
function TDog.Verse: string;
begin
Result := '멍~멍~';
end;
function TCat.Verse: string;
begin
Result := '야옹~';
end;
위와 같이 TAnimal, TDog, TCat이라는 클래스가 선언된 상태에서 다음의 예제를 살펴보겠습니다.
다음은 index에 따라 해당하는 클래스의 Verse를 구하는 함수입니다.
function GetVerseOf(index: integer): string;
var
AAnimal,SomeAnimal: TAnimal;
ADog: TDog;
ACat: TCat;
begin
case index of
0:
begin
AAnimal := TAnimal.Create;
SomeAnimal := AAnimal;
end;
1:
begin
ADog := TDog.Create;
SomeAnimal := ADog;
end;
2:
begin
ACat := TCat.Create;
SomeAnimal := ACat;
end;
else
Result := 'Not found..'
Exit;
end;
Result := SomeAnimal.Verse;
case index of
0:
AAnimal.Free;
1:
ADog.Free;
2:
ACat.Free;
end;
end;
TAnimal과 TDog, TCat에는 Verse라는 메소드를 가지고 있습니다. 이 메소드는 TAnimal 클래스에서는 virtual로 선언되었고 TDog와 TCat 클래스에서는 override로 선언되었습니다. 위에서 말한 호환성 규칙에 따라 ADog와 ACat은 SomeAnimal이라는 변수에 대입이 가능합니다. 참고로 위의 함수는 다음과 같이 간략히 표현될 수 있습니다.
function GetVerseOf(index: integer): string;
var
SomeAnimal: TAnimal;
begin
case index of
0:
SomeAnimal := TAnimal.Create;
1:
SomeAnimal := TDog.Create;
2:
SomeAnimal := TCat.Create;
else
Result := 'Not found..'
Exit;
end;
Result := SomeAnimal.Verse;
SomeAnimal.Free;
end;
이제 위의 예제에서 SomeAnimal.Verse라는 호출문은 어떤 결과를 가져올까요? index라는 값이 무엇이냐에 따라 다릅니다. 만약 SomeAnimal에 TAniaml 클래스의 오브젝트를 가리킨다면, 그것은 TAnimal의 Verse를 호출할 것입니다. 만약 이것이 TDog 클래스의 오브젝트를 가리킨다면, 대신 TDog의 Verse를 호출할 것입니다. 이것은 바로 이 함수가 virtual이기 때문입니다. 조상 메소드가 virtual 일 경우, 후손 클래스에서 이 메소드가 새로 정의되었다면 후손에 정의된 메소드를 호출하게 됩니다. 이때 후손 메소드는 override로 지정하여 이 메소드가 조상 메소드를 대치하는 것임을 지정하여야 합니다. 만일 후손 메소드를 override로 지정하지 않으면 정적 메소드로 정의되어 동적 바인딩을 할 수가 없게 됩니다. 이때 중복되는 이 메소드의 파라미터는 동일하여야 한다는 것을 기억하시기 바랍니다.
다음의 예제를 보시기 바랍니다.
type
TClassA = class
procedure one; virtual;
procedure Two;
end;
TClassB = class(TClassA)
procedure one; Override;
procedure Two;
end;
procedure TClassA.One;
begin
ShowMessage('TClassA.One');
end;
procedure TClassA.Two;
begin
ShowMessage('TClassA.Two');
end;
procedure TClassB.One;
begin
ShowMessage('TClassB.One');
end;
procedure TClassB.Two;
begin
ShowMessage('TClassB.Two');
end;
procedure First;
var
cls: TClassA;
begin
cls := TClassB.Create;
cls.One; // 'TClassB.One'이 출력된다.
cls.Two; // 'TClassA.Two'가 출력된다.
cls.Free;
end;
procedure Second;
var
cls: TClassB;
begin
cls := TClassB.Create;
cls.One; // 'TClassB.One'이 출력된다.
cls.Two; // 'TClassB.Two'가 출력된다.
cls.Free;
end;
위의 예제는 정적인 재정의와 중복 재정의가 어떠한 차이를 나타내는지 보여줍니다.
중복된 함수는 클래스형을 조상 클래스로 지정하여도 자동으로 후손의 함수를 호출하여 줍니다. 하지만 정적으로 재정의된 함수는 클래스 형이 무엇이냐에 따라 호출되는 함수가 달라짐을 알 수 있습니다.
어떤 메소드를 중복하는데는 두 가지의 서로 다른 방법이 있습니다. 하나는 조상 클래스의 메소드를 새 버젼으로 바꾸는 것이고, 다른 하나는 기존의 메소드에 코드를 추가하는 것입니다. 첫번째 방법은 위의 예제와 같은 방법입니다. 그럼 두번째 방법은 어떻게 하여야 할까요. 이 경우에는 조상 클래스의 메소드로 실행하고 동시에 후손의 클래스도 실행되어야 합니다. 이렇게 하려면 조상 클래스와 같은 메소드를 호출하기 위해 inherited 키워드를 사용하면 됩니다. 위의 예제의 TClassB.One을 다음과 같이 하면 됩니다.
procedure TClassB.One;
begin
inherited one; // <<<------ 조상 클래스 TClassA의 Procedure one을 호출한다.
// NewCode..
ShowMessage('TClassB.One');
end;
이때 새로운 코드의 위치는 상황에 따라 inherited 를 호출하는 위치 앞 또는 뒤가 될 수 있습니다. 참고로 virtual이라는 키워드와 거의 동일한 기능을 하는 dynamic이라는 키워드가 있습니다. 이 두키워드의 구문은 완전히 똑같고, 그 사용 결과도 같습니다. 다른 점은 컴파일러가 동적 바인딩을 구현하는 내부적인 메커니즘의 차이가 있습니다. virtual은 가상 메소드 테이블에 기반을 두는 반면, dynamic은 메소드를 가리키는 고유한 번호를 사용합니다. 속도면에서는 virtual이 빠르고 메모리 절약면에서는 dynamic이 좋습니다.
프로그래머의 입장에서 이 중 어느 것을 사용할 지 결정하는 규칙은 다음과 같습니다.
1. 만약 메소드가 거의 모든 자손 클래스에서 중복된다면, Virtual을 사용한다.
2. 만약 메소드가 거의 반복되지 않지만, 그러나 여진히 유연성을 위하여 동적 바인딩을 필요로 한다면, dynamic을 선택한다. 특히 자손 클래스가 많다면 이렇게 하는 편이 좋다.
3. 만약 메소드가 단위시간 동안 많이 호출되는 경우라면, virtual로 한다.
그렇지 않은 경우는 dynamic을 쓰는 것과 실제 속도차가 거의 없다.
생성자/소멸자에서의 동적 바인딩
생성자와 소멸자는 보통 Virtual로 지정되어 작성됩니다 (B.U.T. 그러나 항상 그런것은 아닙니다). 앞에서 보여드린 TObject의 선언에서도 소멸자인 destroy 메소드가 virtual로 지정되었음을 볼 수 있습니다. 보통 생성자와 소멸자에서는 조상 클래스의 생성자와 소멸자를 inherited 지시어를 이용해서 호출하여 줍니다. 이렇게 하는 이유는 조상 클래스에서 초기화 하거나 삭제해야할 코드를 실행 시키기 위해서 입니다. 생성자의 경우 조상 클래스의 생성자를 먼저 호출하고 후손 클래스의 초기화를 진행합니다. 소멸자의 경우 이와 반대로 후손 클래스의 작업를 정리한 후 조상클래스의 소멸자를 호출합니다. 다음은 생성자와 소멸자 코드의 예제 입니다.
constructor TMyClass.Create;
begin
inherited Create; // 먼저 조상의 생성자를 호출
... // 초기화 코드..
end;
destructor TMyClass.Destroy;
begin
... // 정리 코드
inherited Destroy; // 마지막에 조상 의 소멸자를 호출
end;
컴포넌트와 관련하여 실제 델파이의 코드를 보겠습니다..
우리가 앞으로 사용하게 될 TWinControl이라는 클래스는 다음과 같은 상속관계를 가집니다.
TObject
|
TPersistent
|
TComponent
|
TControl
|
TWinControl
이들의 생성자 코드를 살펴보면 다음과 같습니다.
// TObject의 생성자
constructor TObject.Create;
begin
end;
// TPersistent는 TObject의 생성자를 그대로 사용
// TComponent의 생성자
constructor TComponent.Create(AOwner: TComponent);
begin
FComponentStyle := [csInheritable];
if AOwner <> nil then AOwner.InsertComponent(Self);
end;
// TControl의 생성자
constructor TControl.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FWindowProc := WndProc;
FControlStyle := [csCaptureMouse, csClickEvents, csSetCaption,
csDoubleClicks];
FFont := TFont.Create;
FFont.OnChange := FontChanged;
FColor := clWindow;
FVisible := True;
FEnabled := True;
FParentFont := True;
FParentColor := True;
FParentShowHint := True;
FIsControl := False;
FDragCursor := crDrag;
end;
// TWinControl의 생성자
constructor TWinControl.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FObjectInstance := MakeObjectInstance(MainWndProc);
FBrush := TBrush.Create;
FBrush.Color := FColor;
FParentCtl3D := True;
FTabOrder := -1;
FImeMode := imDontCare;
FImeName := Screen.DefaultIme;
end;
코드에서 볼 수 있듯이 어떤 클래스의 조상클래스에는 조상클래스 고유의 생성자가 있고 그 클래스를 위한 초기화 작업이 여기에서 이루어 집니다. 따라서 조상 클래스가 올바르게 초기화될 수 있도록 하기 위해서는 위의 생성자들의 첫줄에서 처럼 바로 윗조상 클래스의 생성자를 호출해 주어야 합니다.
위에서 TComponent의 생성자에서는 조상의 생성자를 호출하지 않고 있는데 이것은 조상인 TPersistent와 TObject의 생성자에서 아무일도 하지 않기 때문입니다. 그러나 TControl의 생성자의 경우 조상인 TComponent의 생성자에서 어떤 초기화 작업이 이루어지므로 이를 호출해 주었습니다.
추상 메소드 (Abstract method)
후손 클래스에는 공통적으로 필요하지만 조상 클래스에는 굳이 필요없는 동적 바인딩 해야할 메소드가 있을 수 있습니다. 앞에서 보여드린 예제중 TAniaml의 Verse메소드가 그런 경우일 수 있습니다. 즉 후손에서 중복된 메소드만이 호출되고 조상의 메소드가 호출될 경우가 없을 경우 굳이 조상인 TAnimal의 Verse 메소드를 정의할 필요가 없습니다. 이렇듯 조상 클래스 변수에 후손 클래스의 오브젝트를 지정해 두고 동적으로 메소드를 호출해야하는 경우, 조상 클래스에 빈 메소드를 정의할 필요없이 메소드 이름만을 선언하고 abstract지시어로서 표시하면 자리만 만들어두는 메소드가 된다. 이 경우 abstract 메소드는 당연히 직접 호출되어서는 안 되고, 후손 클래스의 오브젝트에서 중복하여 정의한 메소드만이 호출될 수 있다. 만약 abstract 로 지정된 조상 클래스의 메소드를 직접 호출하여 프로그램을 종료시켜야 하는 오류가 발생됩니다.
다음은 abstract를 사용한 TAnimal 클래스의 선언입니다.
type
TAnimal = class // (TObject)가 생략되었음..
public
function Verse: string; virtual; abstract;
// 추상형으로 선언되었음..
// 따라서 TAnimal.Verse라는
// 메소드 코드를 작성하지 않아도 된다.
end;
TDog = class(TAnimal)
public
function Verse: string; override;
end;
TCat = class(TAnimal)
public
function Verse: string; override;
end;
//
// 이전의 예제에서 TAniaml.Verse의 코드를 삭제했음..
//
function TDog.Verse: string;
begin
Result := '멍~멍~';
end;
function TCat.Verse: string;
begin
Result := '야옹~';
end;
메시지 핸들러 (Message Handler)
위에서 설명한 동적바인딩과 유사한 형태로 Windows의 Message를 처리 하는 방법이 있습니다. 이러한 목적을 위해 델파이는 또 다른 지시자인 message를 메시지 핸들링 메소드를 위해 제공하고 있습니다. 이 메소드는 반드시 한개의 var 파라미터만을 갖고 있어야 합니다. 그 다음에는 message 지시자와 Windows 메시지 번호에 해당하는 인덱스가 나옵니다. 예를 들면 다음과 같습니다.
type
TForm1 = class(TForm)
...
procedure WMMinMax(var Message:TMessage); message WM_GETMINMAXINFO;
end;
예제에서 파라미터로 넘겨준 TMessage형은 무엇일까요? 델파이에서는 이처럼 처리할 메시지의 종류에 따라 다루기 편리한 여러 종류의 레코드 형을 미리 선언해 두고 있습니다. 위의 TMessage형 역시 이러한 레코드 형중의 하나입니다. 그러나 프로시져의 이름인 WMMinMax나 Message의 자료형으로 어떤 것을 사용할지는 여러분이 직접 정하는 것입니다. 이러한 기능은 Windows 메시지와 API 함수를 이용하는 부분에서 유용하게 사용됩니다.
다음은 실제 윈도우 메시지를 처리하는 메시지 핸들러의 예제입니다.
type
TTextBox = class(TCustomControl)
private
procedure WMChar(var Message:TWMChar); message WM_CHAR;
...
end;
procedure TTextBox.WMChar(var Message:TWMChar);
begin
if chr(Message.CharCode)=#13 then
ProcessEnter
else
inherited;
end;
위 예제에서 네번째 줄의 message 지시어 뒤에 메시지값(WM_CHAR)을 두어, 처리할 메시지를 표시하고 있습니다. 여기서는 파라미터로 TWMChar형을 사용하였습니다. 메시지 핸들러 메소드의 마지막 부분에 inherited로 조상 클래스의 메소드를 호출하고 있습니다. 이것은 메시지 핸들러가 일종의 동적바인딩이라는 것을 보여 줍니다. 만약 조상 클래스에 이를 위한 핸들러 메소드가 없다면 TObject의 DefaultHandler를 호출하게 됩니다.
프로퍼티 (Property)
앞에서 클래스의 멤버에는 필드, 메소드, 프로퍼티가 있다고 언급한 바 있습니다. 오브젝트 파스칼에서는 필드가 안전하게 참조될 수 있도록 하기 위해 다양한 조건을 줄 수 있는 프로퍼티 기능을 제공하고 있습니다.
프로퍼티의 지정 문법은 다음과 같습니다.
property <프로퍼티명>:<프로퍼티의 자료형>
read <프로퍼티 값을 가져올 변수 혹은 함수>
write <프로퍼티 값을 대입할 변수 또는 프로지져>
default <기본값>;
필요에 따라 read, write, default 중 어떤 것들은 생략하는 경우도 있습니다.
다음은 간단한 프로퍼티의 선언 예제 입니다.
// 읽기/쓰기가 모두 가능한 프로퍼티의 예
property Age:integer read GetAge write SetAge default 0;
// 읽기만 가능한 프로퍼티의 예 (write부분이 없다)
property Age:integer read GetAge;
// 쓰기만 가능한 프로퍼티의 예 (read부분이 없다)
property Age:integer write SetAge;
이와같이 Age 프로퍼티를 선언하려면 다음과 같은 코드가 준비되어야 합니다.
type
TPerson = class
private
FAge: integer;
...
function GetAge: TColor;
procedure SetAge(v:integer);
...
published
property Age: integer read GetAge write SetAge;
end;
이렇게 만들어 지면 나중에 TPerson형의 p라는 오브젝트가 있다고 할 때, p.Age라고 하면 p의 Age 프로퍼티를 지칭하게 됩니다. 이 프로퍼티에 값을 주거나 받을 때는 다음과 같이 작성하면 됩니다.
p.Age := 나이값; // (1)
nAge := p.Age; // (2)
(1)의 경우 지정된 나이값은 write로 지정된 SetAge라는 프로시져를 통하여 처리됩니다. 반대로 (2)의 경우처럼 오브젝트의 Age 프로퍼티를 알고자 하면 read로 지정된 GetAge 함수를 통해 값을 얻게 됩니다. 이처럼 어떤 메소드를 통하여 값을 지정하거나 읽는 것은 필드에 지정되는 값에 어떤 제한을 두어 좀더 안전하게 접근하기 위해서 입니다. 다음은 위에서 언급된 메소드 GetAge와 SetAge의 예제 입니다.
function TPerson.GetAge;
begin
Result := FAge;
end;
procedure TPerson.SetAge(v: integer);
begin
if v<0 then
begin
ShowMessage('입력된 나이가 이상합니다요. (<0)');
Exit;
end
else
FAge := v;
end;
위의 예제에서는 나이를 지정하는 과정에서 0보다 작은 나이가 지정되면 에러 메시지를 보이게 하였습니다. read에 해당한 메소드인 GetAge의 경우 단순히 FAge의 값을 되돌려 줍니다. 하지만 어떤 상황에서는 read에 지정될 메소드에서 어떤 처리를 해줄 필요가 있을 수도 있습니다. 위의 경우처럼 값을 읽을 때 특별한 처리가 필요 없다면 다음과 같이 지정하여도 무관합니다.
property Age:integer read FAge write SetAge;
즉 지정된 값이 저장된 필드를 직접 연결 해주면 됩니다. 마찬가지로 Write시에도 특별한 제한이 없다면 다음과 같이 필드를 직접 연결해 주어도 됩니다.
property Age:integer read FAge write FAge;
여기에서는 Age 프로퍼티를 보관하기 위한 필드로 FAge를 사용하였지만, read와 write에 모두 메소드가 지정될 경우 필드를 사용하지 않을 수도 있습니다.
특별히 컴포넌트와 관련된 것으로 컴포넌트 클래스의 published에 나타난 프로퍼티들은 델파이의 Object Inspector에 나타나고 편집시 지정된 정보는 .dfm 파일에 저장됩니다. 즉, .dfm 파일에 저장되어야 하는 프로퍼티들의 가시성은 published로 설정되어야 합니다.
런타임 형 정보 (Run-Time Type Information)
앞부분에서 오브젝트 파스칼의 형 호환성 규칙에 의해, 조상 클래스가 요구될 때 자손 클래스를 이용할 수 있지만 그 역은 성립하지 않는다고 설명드렸습니다. 앞의 예제에서 TDog나 TCat에는 Eat 이라는 메소드가 있지만, TAnimal에는 없다고 가정해 봅시다. 만약 TAnimal형의 변수 MyAnimal가 TDog의 오브젝트를 가리키고 있다면, Eat이라는 메소드를 호출할 수 있어야 합니다. 하지만 어떤 경우에 MyAnimal에는 TAnimal의 오브젝트가 지정될 수도 있습니다. 이 경우에는 Eat 메소드를 호출할 수 없습니다. 만약 TAnimal이 지정된 상태에서 Eat를 호출한다면 프로그램은 런타임 에러를 보이며 투덜댈 것입니다. 이 문제를 해결하기 위해서는 현재 변수 MyAnimal이 가리키는 오브젝트가 TDog 인지 아니면 TAnimal인지를 알아야 할 것입니다. 델파이는 이런 문제를 위해서 is 와 as 라는 연산자를 제공하여 줍니다.
먼저 is 연산자는 파라미터로 클래스의 오브젝트와 클래스 형을 가지면 Boolean형의 리턴값을 돌려 줍니다.
다음은 is 연산자의 사용 예제 입니다.
if MyAnimal is TDog then 어쩌고 저쩌고...; 이 예제의 내용을 MyAnimal이라는 변수가 가리키는 오브젝트가 TDog 클래스 형인지를 판단해줍니다. 이 경우 MyAnimal이 TDog형이거나 TDog 형을 상속받은 클래스 형이하면 결과는 True입니다. 이렇게 하여 MyAnimal이 가리키는 오브젝트가 TDog와 관련있음을 확인하면, 다음과 같이 타입 캐스트(Type-Cast: 형 변환)을 하여 사용할 수 있습니다.
// 변수 MyDog는 TDog형일 때..
if MyAnimal is TDog then
begin
MyDog := TDog(MyAnimal); // MyAnimal을 TDog형으로 형변환
Text := MyDog.Eat;
end;
두번째 연산자 as도 오브젝트와 클래스 형을 파라미터로 가지며, 연산 결과는 새로운 클래스 형으로 변환된 오브젝트 입니다.
다음은 as에 대한 예제 입니다.
// 변수 MyDog는 TDog형일 때..
if MyAnimal is TDog then
begin
MyDog := MyAnimal as TDog;
Text := MyDog.Eat;
end;
as 연산자의 사용에 있어서 만약 오브젝트의 형이 새로 캐스트하려는 형과 호환되지 않는다면 예외상태를 발생 시킵니다. 이때 발생되는 예외는 EInvalidCast 입니다. 이런 예외를 피하려면 마지막 예제처럼 is 연산자를 통해 클래스 형을 확인한 후 사용하면 됩니다. 이 두가지 연산자는 컴포넌트 제작시 종종 사용되므로 숙지하시기 바랍니다.
[출처] [펌] 델파이 클래스(Class)|작성자 상지니
'델파이 > 델파이관련' 카테고리의 다른 글
Graphic32 Contrast, Brightness (0) | 2014.04.29 |
---|---|
델파이 객체지향프로그래밍을 위한 20가지 규칙 (0) | 2012.02.27 |
델파이 VCL 계층구조, class 들의 계층구조 (0) | 2012.02.14 |
델파이 다음 컨트롤 자동 선택하기 (0) | 2012.02.13 |
델파이 HotKey (0) | 2012.02.10 |