D 언어 사양 - 클래스
D 언어 사양 - 클래스 (Classes)
D의 객체 지향 기능은 모두 클래스에서 나와요. 이번 챕터에서는 클래스 선언부터 상속, 생성자, 소멸자, 중첩 클래스, 그리고 const/immutable/shared 클래스까지, D 언어의 클래스 전반을 순서대로 살펴볼게요. 처음 배우는 분도 차근차근 따라올 수 있도록, 각 개념을 실용적인 코드 예시와 함께 설명할게요.
본문
개요 (Overview)
D의 객체 지향 기능은 모두 클래스에서 나옵니다. 클래스 계층(hierarchy)의 뿌리는 Object 클래스예요. Object는 파생 클래스마다 갖춰야 할 최소한의 기능과, 그 기능에 대한 기본 구현을 정의해 줍니다.
클래스는 프로그래머가 정의하는 타입입니다. 클래스를 지원한다는 게 D를 객체 지향 언어로 만들어 주는 핵심이고, 이를 통해 캡슐화(encapsulation), 상속(inheritance), 다형성(polymorphism)을 얻을 수 있어요. D 클래스는 단일 상속(single inheritance) 패러다임을 따르며, 인터페이스(interface) 지원을 덧붙여 확장합니다. 클래스 객체는 참조(reference)로만 인스턴스화됩니다.
클래스는 export될 수 있는데, 그 의미는 클래스의 이름과 모든 private이 아닌 멤버가 DLL이나 EXE 외부로 노출된다는 뜻이에요.
클래스 선언 (Class Declarations)
클래스 선언은 이렇게 정의됩니다:
ClassDeclaration:
class Identifier ;
class Identifier BaseClassListopt AggregateBody
ClassTemplateDeclaration
BaseClassList:
: SuperClassOrInterface
: SuperClassOrInterface , Interfaces
SuperClassOrInterface:
BasicType
Interfaces:
Interface
Interface , Interfaces
Interface:
BasicType
클래스는 다음으로 구성됩니다:
- 슈퍼 클래스(super class)
- 인터페이스(interface)
- 동적 필드(dynamic fields), 정적 필드(static fields)
- 중첩 클래스(nested class)
- 멤버 함수(member functions), 정적 멤버 함수(static member functions)
- 가상 함수(Virtual Functions)
- 생성자(Constructors), 소멸자(Destructors)
- 클래스 불변식(Class Invariants)
- 연산자 오버로딩(Operator Overloading)
- 기타 선언들 (DeclDef 참고)
클래스는 이렇게 정의해요:
class Foo
{
... members ...
}
Note: C++과 달리, 클래스 정의를 닫는
}뒤에 세미콜론;을 붙이지 않아요. 그리고 클래스를 정의하면서 같은 줄에 변수var를 선언하는 것도 불가능해요:
class Foo { } var;
그 대신 이렇게 나눠서 쓰면 됩니다:
class Foo { }
Foo var;
접근 제어 (Access Control)
클래스 멤버에 대한 접근은 접근 특성(visibility attributes)으로 제어합니다. 기본 접근 특성은 public이에요.
상속 (Inheritance)
모든 클래스는 슈퍼 클래스로부터 상속받습니다. 슈퍼 클래스를 지정하지 않으면 클래스는 Object로부터 상속받아요. Object는 D 클래스 상속 계층의 뿌리를 이룹니다.
class A { } // A inherits from Object
class B : A { } // B inherits from A
다중 클래스 상속은 지원되지 않지만, 클래스는 여러 인터페이스를 상속받을 수 있어요. 슈퍼 클래스를 선언한다면 반드시 어떤 인터페이스보다 먼저 와야 하고, 상속받는 타입들은 쉼표로 구분합니다.
클래스 인스턴스는 자신이 상속받은 타입으로 암시적으로 변환됩니다. 반대로 베이스 클래스 인스턴스를 서브클래스로 변환하려면 동적 캐스트(dynamic cast)가 필요해요:
class Base {}
class Derived : Base {}
Base b = new Derived(); // implicit conversion
Derived d = cast(Derived) b; // explicit conversion
필드 (Fields)
비정적(non-static) 멤버 변수를 필드(field)라고 부릅니다. 클래스 인스턴스의 멤버는 . 연산자로 접근해요. 베이스 클래스의 멤버는 베이스 클래스 이름 뒤에 점을 붙여서 접근할 수 있습니다:
class A { int a; int a2;}
class B : A { int a; }
void foo(B b)
{
b.a = 3; // accesses field B.a
b.a2 = 4; // accesses field A.a2
b.A.a = 5; // accesses field A.a
}
Implementation Defined: D 컴파일러는 클래스의 필드 순서를 재배치해서 최적으로 패킹할 자유가 있어요. 필드를 함수의 지역 변수처럼 생각해 보세요. 컴파일러는 어떤 변수는 레지스터에 할당하고 어떤 변수는 이리저리 옮기면서 최적의 스택 프레임 배치를 만들죠. 이 덕분에 코드 설계자는 기계어 최적화 규칙에 맞춰 필드를 배치하느라 고생하지 않고, 더 읽기 좋은 순서로 필드를 정리할 수 있어요. 필드 레이아웃을 명시적으로 제어하고 싶다면 클래스가 아니라 struct/union 타입이 그 역할을 담당합니다.
extern(Objective-C) 클래스의 필드는 동적 오프셋(dynamic offset)을 가져요. 즉 베이스 클래스가 (인스턴스 변수를 추가하거나 제거하는 방식으로) 바뀌어도 서브클래스가 다시 컴파일하거나 재링크할 필요가 없다는 뜻입니다.
클래스 속성 (Class Properties)
클래스 속성 (Class Properties)
| 속성 | 설명 |
|---|---|
.tupleof |
클래스에 있는 필드의 심볼 시퀀스(symbol sequence) |
클래스 인스턴스 속성 (Class Instance Properties)
| 속성 | 설명 |
|---|---|
.classinfo |
클래스의 동적 타입(dynamic type)에 대한 정보 |
.outer |
중첩 클래스 인스턴스의 경우, 부모 클래스 인스턴스 또는 (부모 클래스가 없을 때) 부모 함수의 컨텍스트 포인터를 제공 |
.tupleof
.tupleof 속성은 클래스에 있는 모든 비정적 필드의 심볼 시퀀스를 제공해요. 단, 초기 숨김 필드(initial hidden fields)와 베이스 클래스의 필드는 제외합니다. 인스턴스에 사용하면 .tupleof는 lvalue 시퀀스를 줍니다.
튜플 안의 필드 순서는 필드가 선언된 순서와 일치해요.
Note:
.tupleof는extern(Objective-C)클래스에는 사용할 수 없어요. 그 클래스들은 필드에 동적 오프셋이 있기 때문이죠.
class Foo { int x; long y; }
static assert(__traits(identifier, Foo.tupleof[0]) == "x");
static assert(is(typeof(Foo.tupleof)[1] == long));
void main()
{
import std.stdio;
auto foo = new Foo;
foo.tupleof[0] = 1; // set foo.x to 1
foo.tupleof[1] = 2; // set foo.y to 2
foreach (ref x; foo.tupleof)
x++;
assert(foo.x == 2);
assert(foo.y == 3);
auto bar = new Foo;
bar.tupleof = foo.tupleof; // copy fields
assert(bar.x == 2);
assert(bar.y == 3);
}
숨겨진 필드 접근 (Accessing Hidden Fields)
.__vptr 속성은 클래스 객체의 vtbl[]에 접근하게 해 주지만, 사용자 코드에서 쓰면 안 돼요. .__monitor 필드는 druntime에 정의되어 있지만 .tupleof와 traits 기반 인트로스펙션에서는 제외됩니다. 자세한 내용은 ABI를 참고하세요.
계속 보기: 컨텍스트 포인터 (Context Pointer).
필드 속성 (Field Properties)
.offsetof 속성은 클래스 인스턴스의 시작 지점부터 필드까지의 오프셋(바이트 단위)을 알려줘요. 컴파일러가 클래스 필드 오프셋을 재배치할 수 있으니 주의하세요. struct 기반 예시는 align 속성을 참고하면 됩니다.
Note:
.offsetof는extern(Objective-C)클래스의 필드에는 사용할 수 없어요. 그 클래스들의 필드는 동적 오프셋을 갖기 때문이죠.
멤버 함수 (Member Functions, 일명 메서드)
비정적 멤버 함수는 반드시 자기 클래스의 인스턴스에 대해 호출해야 해요. 이 함수들은 this라고 부르는 숨겨진 매개변수를 하나 더 갖는데, 이 this를 통해 클래스 객체의 다른 멤버들에 접근할 수 있습니다. 함수 본문 안에서는 클래스 인스턴스 멤버들이 스코프에 들어와요.
class C
{
int a;
void foo()
{
a = 3; // assign to `this.a`
}
}
auto c = new C;
c.foo();
assert(c.a == 3);
비정적 멤버 함수는 보통의 FunctionAttributes 외에도 const, immutable, shared, inout, scope, return scope 속성을 가질 수 있어요. 이 속성들은 숨겨진 this 매개변수에 적용됩니다.
class C
{
int a;
void foo() const
{
a = 3; // error, 'this' is const
}
void foo() immutable
{
a = 3; // error, 'this' is immutable
}
C bar() @safe scope
{
return this; // error, 'this' is scope
}
}
Objective-C 링키지
Objective-C 링키지를 가진 정적 멤버 함수도 this라는 숨겨진 매개변수를 하나 더 가져서, 이 this를 통해 클래스 객체의 다른 멤버들에 접근할 수 있어요.
Objective-C 링키지를 가진 멤버 함수에는 함수가 호출될 때 사용된 셀렉터(selector)라는, 숨겨지고 이름 없는 매개변수가 하나 더 있습니다.
Objective-C 링키지를 가진 정적 멤버 함수들은 숨겨진 중첩 메타클래스(metaclass) 안에 비정적 멤버 함수로 배치됩니다.
동기화 메서드 호출 (Synchronized Method Calls)
(synchronized가 아닌) 클래스의 멤버 함수는 각각 개별적으로 synchronized로 표시할 수 있어요. 메서드가 호출되는 동안 해당 클래스 인스턴스의 모니터 객체가 잠기고, 호출이 끝나면 잠금이 풀립니다.
synchronized 메서드는 shared 클래스 인스턴스에서만 호출할 수 있어요.
class C
{
void foo();
synchronized int bar();
}
void test(C c)
{
c.foo; // OK
//c.bar; // Error, `c` is not `shared`
shared C sc = new shared C;
//sc.foo; // Error, `foo` not callable using a `shared` object
sc.bar; // OK
}
계속 보기: 동기화 문 (SynchronizedStatement).
동기화 클래스 (Synchronized Classes)
synchronized 클래스의 모든 멤버 함수는 암시적으로 synchronized가 됩니다. 정적 멤버 함수는 그 클래스의 classinfo 객체를 기준으로 동기화되는데, 이는 해당 동기화 클래스의 모든 정적 멤버 함수에 대해 모니터 하나가 사용된다는 뜻이에요. 동기화 클래스의 비정적 함수는 그 클래스 객체의 일부인 모니터를 사용합니다. 예를 들어:
synchronized class Foo
{
void bar() { ...statements... }
}
이 코드는 (모니터 측면에서 보면) 다음과 동일해요:
synchronized class Foo
{
void bar()
{
synchronized (this) { ...statements... }
}
}
Note:
bar는 SynchronizedStatement를 사용해요.
synchronized 클래스의 멤버 필드는 public이 될 수 없습니다:
synchronized class Foo
{
int foo; // Error: public field
}
synchronized class Bar
{
private int bar; // ok
}
Note: struct 타입은
synchronized로 표시할 수 없어요.
클래스 인스턴스화 (Class Instantiation)
필드는 기본적으로 자기 타입의 기본 초기값(default initializer)으로 초기화됩니다. (보통 정수 타입은 0, 부동소수점 타입은 NaN이죠.) 필드 선언에 선택적 Initializer가 있으면 그 값이 기본값 대신 사용돼요.
class Abc
{
int a; // default initializer for a is 0
long b = 7; // default initializer for b is 7
float f; // default initializer for f is NAN
}
void main()
{
Abc obj = new Abc;
assert(obj.b == 7);
}
Initializer는 컴파일 타임에 평가됩니다. 이 초기화는 어떤 생성자가 호출되기 전에 이루어져요.
클래스 객체의 인스턴스는 NewExpression으로 생성합니다.
- 생성자가 없거나 (혹은 인자가 없는 nullary 생성자를 가진) 클래스
C는new C로 인자를 넘기지 않고 인스턴스화됩니다. - 그 외에는 인자 목록을 사용해 인스턴스화할 수 있어요. 예:
new C(arguments). 인자들은 매개변수 목록이 일치하는 생성자로 전달됩니다 (오버로드된 메서드처럼 해석돼요).
기본적으로 클래스 인스턴스는 가비지 컬렉션 힙(GC heap)에 할당됩니다. 반면 scope 클래스 인스턴스는 스택에 할당돼요.
생성자 (Constructors)
생성자는 클래스 인스턴스가 만들어질 때 보통 컴파일러가 호출하는 특별한 멤버 함수입니다.
Constructor:
this Parameters MemberFunctionAttributesopt FunctionBody
this Parameters MemberFunctionAttributesopt MissingFunctionBody
ConstructorTemplate
생성자는 함수 이름이 this이고 반환 타입이 없는 함수로 선언해요:
class A
{
int i;
this() // constructor taking no arguments
{
i = 2; // initialize `this.i`
}
this(int i) // constructor taking an int argument
{
this.i = i; // initialize field `i` from parameter `i`
}
}
void main()
{
A a = new A; // instantiate A and call `A.this()`
assert(a.i == 2);
a = new A(3); // instantiate A and call `A.this(3)`
assert(a.i == 3);
}
생성자에서 값을 명시적으로 반환하는 것은 허용되지 않지만, return;은 함수를 일찍 종료하는 데 사용할 수 있어요.
베이스 클래스 생성 (Base Class Construction)
베이스 클래스 생성은 super라는 이름으로 베이스 클래스 생성자를 호출해서 이루어집니다:
class A { this(int y) { } }
class B : A
{
int j;
this()
{
...
super(3); // call base constructor A.this(3)
...
}
}
위임 생성자 (Delegating Constructors)
생성자는 같은 클래스의 다른 생성자를 호출해서 공통 초기화를 나눠 쓸 수 있어요. 이것을 위임 생성자(delegating constructor)라고 합니다:
class C
{
int j;
this()
{
...
}
this(int i)
{
this(); // delegating constructor call
j = i;
}
}
제약 사항 (Restrictions)
다음 제약이 적용됩니다:
-
생성자들이 서로를 순환 호출하는 것은 불법이에요.
this() { this(1); }this(int i) { this(); }// illegal, cyclic constructor calls. — Implementation Defined: 컴파일러가 순환 생성자 호출을 반드시 탐지해야 하는 것은 아닙니다. — Undefined Behavior: 프로그램이 순환 생성자 호출로 실행되는 경우. -
생성자 코드가 위임/베이스 생성자 호출을 포함하면, 생성자를 통과하는 모든 가능한 실행 경로는 그 호출을 정확히 한 번 해야 해요.
this() { a || super(); }// illegal, 0 or 1 callthis() { (a) ? this(1) : super(); }// OKthis() { super(); this(1); }// illegal, 2 callsthis() { for (...) { super(); // illegal, inside loop } }
-
위임/베이스 생성자 호출을 하기 전에
this를 암시적이든 명시적이든 참조하는 것은 불법이에요. -
위임/베이스 생성자 호출은 라벨(label) 뒤에 올 수 없습니다.
계속 보기: 필드 초기화 (field initialization).
암시적 베이스 클래스 생성 (Implicit Base Class Construction)
클래스에 생성자가 없는데 베이스 클래스에 생성자가 있으면, 다음 형태의 기본 생성자가 암시적으로 생성됩니다:
this() { }
생성자에 위임 생성자나 super 호출이 전혀 없고 베이스 클래스에 nullary 생성자가 있으면, 생성자 맨 앞에 super() 호출이 삽입됩니다. 베이스 클래스에 인자를 요구하는 생성자만 있고 nullary 생성자가 없다면, 그에 맞는 super 호출이 필요해요.
인스턴스화 과정 (Instantiation Process)
다음 단계들이 일어납니다:
- 객체를 위한 저장 공간이 할당됩니다. 할당에 실패하면
null을 돌려주는 대신 OutOfMemoryError가 던져져요. 따라서 null 참조를 일일이 검사하는 번거로운 코드는 필요 없습니다. - 원시 데이터가 클래스 정의에서 제공된 값으로 정적으로 초기화됩니다.
vtbl[](가상 함수를 가리키는 포인터들의 배열)에 대한 포인터가 할당됩니다. 생성자들은 가상 함수를 호출할 수 있는 완전히 형성된 객체를 전달받아요. 이 연산은 객체의 정적 버전을 새로 할당된 객체에 메모리 복사하는 것과 동등합니다. - 클래스에 생성자가 정의되어 있으면, 인자 목록과 일치하는 생성자가 호출됩니다.
- 위임 생성자가 호출되지 않았다면, 베이스 클래스의 기본 생성자 호출이 이루어집니다.
- 생성자의 본문이 실행됩니다.
- 클래스 불변식 검사가 켜져 있으면, 생성자 끝에서 클래스 불변식(class invariant)이 호출됩니다.
생성자 속성 (Constructor Attributes)
생성자는 const, immutable, shared 중 하나의 멤버 함수 속성을 가질 수 있어요. 그러면 한정(qualified) 객체의 생성은 구현된 해당 한정 생성자로 제한됩니다.
class C
{
this(); // non-shared mutable constructor
}
// create mutable object
C m = new C();
// create const object using mutable constructor
const C c2 = new const C();
// a mutable constructor cannot create an immutable object
// immutable C i = new immutable C();
// a mutable constructor cannot create a shared object
// shared C s = new shared C();
생성자는 서로 다른 속성으로 오버로드될 수 있어요.
class C
{
this(); // non-shared mutable constructor
this() shared; // shared mutable constructor
this() immutable; // immutable constructor
}
C m = new C();
shared s = new shared C();
immutable i = new immutable C();
순수 생성자 (Pure Constructors)
생성자가 고유한(unique) 객체를 만들 수 있다면 (예: pure라면), 그 객체는 어떤 한정자로도 암시적으로 변환될 수 있어요.
class C
{
this() pure;
// Based on the definition, this creates a mutable object. But the
// created object cannot contain any mutable global data.
// Therefore the created object is unique.
this(int[] arr) immutable pure;
// Based on the definition, this creates an immutable object. But
// the argument int[] never appears in the created object so it
// isn't implicitly convertible to immutable. Also, it cannot store
// any immutable global data.
// Therefore the created object is unique.
}
immutable i = new immutable C(); // this() pure is called
shared s = new shared C(); // this() pure is called
C m = new C([1,2,3]); // this(int[]) immutable pure is called
생성자 안의 필드 초기화 (Field initialization inside a constructor)
생성자 본문에서, 필드에 대한 첫 번째 대입이 바로 그 필드의 초기화가 됩니다.
class C
{
int num;
this()
{
num = 1; // initialization
num = 2; // assignment
}
}
필드 타입에 opAssign 메서드가 있더라도, 그것은 초기화에는 사용되지 않아요.
struct A
{
this(int n) {}
void opAssign(A rhs) {}
}
class C
{
A val;
this()
{
val = A(1); // val is initialized to the value of A(1)
val = A(2); // rewritten to val.opAssign(A(2))
}
}
필드 타입이 mutable이 아니면, 여러 번 초기화하는 것은 거부됩니다.
class C
{
immutable int num;
this()
{
num = 1; // OK
num = 2; // Error: multiple field initialization
}
}
필드가 어떤 경로에서 초기화되면, 모든 경로에서 초기화되어야 해요.
class C
{
immutable int num;
immutable int ber;
this(int i)
{
if (i)
num = 3; // initialization
else
num = 4; // initialization
}
this(long j)
{
j ? (num = 3) : (num = 4); // ok
j || (ber = 3); // error, intialized on only one path
j && (ber = 3); // error, intialized on only one path
}
}
필드 초기화는 루프 안이나 라벨 뒤에 나올 수 없어요.
class C
{
immutable int num;
immutable string str;
this()
{
foreach (i; 0..2)
{
num = 1; // Error: field initialization not allowed in loops
}
size_t i = 0;
Label:
str = "hello"; // Error: field initialization not allowed after labels
if (i++ < 2)
goto Label;
}
}
필드 타입이 기본 생성(disabled default construction)을 비활성화했다면, 그 필드는 생성자에서 반드시 초기화되어야 해요.
struct S { int y; @disable this(); }
class C
{
S s;
this(S t) { s = t; } // ok
this(int i) { this(); } // ok
this() { } // error, s not initialized
}
소멸자 (Destructors)
Destructor:
~ this ( ) MemberFunctionAttributesopt FunctionBody
~ this ( ) MemberFunctionAttributesopt MissingFunctionBody
소멸자 함수는 다음 경우에 호출됩니다:
- 살아있는 객체가 가비지 컬렉터에 의해 삭제될 때
- 살아있는
scope클래스 인스턴스가 스코프를 벗어날 때 - 객체에 대해
destroy가 호출될 때
예시:
import std.stdio;
class Foo
{
~this() // destructor for Foo
{
writeln("dtor");
}
}
void main()
{
auto foo = new Foo;
destroy(foo);
writeln("end");
}
- 클래스마다 소멸자는 하나만 선언할 수 있어요. 단, 다른 소멸자를 믹스인(mix in)하는 것은 가능합니다.
- 소멸자는 매개변수를 갖지 않아요.
- 소멸자는 항상 가상(virtual)입니다.
소멸자는 객체가 보유한 GC가 아닌(non-GC) 자원을 해제할 것으로 기대됩니다.
프로그램은 살아있는 객체의 소멸자를 destroy로 즉시 명시적으로 호출할 수 있어요. 그러면 런타임이 객체를 표시해서 소멸자가 두 번 호출되지 않게 합니다.
소멸자가 끝나면 슈퍼 클래스의 소멸자가 자동으로 호출됩니다. 슈퍼 클래스 소멸자를 명시적으로 호출할 방법은 없어요.
Implementation Defined: 가비지 컬렉터가 모든 참조되지 않은 객체에 대해 소멸자를 반드시 실행한다는 보장은 없습니다.
Important: 가비지 컬렉터가 참조되지 않은 객체들의 소멸자를 호출하는 순서는 명시되지 않아요. 이 말은, GC 객체에 대한 참조를 멤버로 가진 클래스의 객체에 대해 GC가 소멸자를 호출하면, 그 참조들이 더 이상 유효하지 않을 수 있다는 뜻입니다. 따라서 소멸자는 하위 객체(sub objects)를 참조할 수 없어요. — Note: 이 규칙은
scope클래스 인스턴스나destroy로 소멸된 객체에는 적용되지 않습니다. 그 경우 소멸자가 가비지 컬렉션 사이클 중에 실행되는 게 아니라서 모든 참조가 유효하기 때문이죠.
정적 데이터 세그먼트(static data segment)에서 참조되는 객체는 GC에 의해 수집되지 않아요.
정적 생성자와 소멸자 (Static Constructors and Destructors)
정적 생성자 (Static Constructors)
StaticConstructor:
static this ( ) MemberFunctionAttributesopt FunctionBody
static this ( ) MemberFunctionAttributesopt MissingFunctionBody
정적 생성자는 메인 스레드에서 main() 함수가 제어권을 갖기 전에, 그리고 스레드가 시작될 때, 스레드 로컬 데이터(thread local data)의 초기화를 수행하는 함수예요.
정적 생성자는 컴파일 타임에 계산할 수 없는 값으로 정적 클래스 멤버를 초기화할 때 사용됩니다.
다른 언어에서는 정적 생성자를, 컴파일 타임에 계산할 수 없는 멤버 초기화자를 사용해서 암시적으로 만들어요. 문제는 코드가 정확히 언제 실행되는지 잘 제어할 수 없다는 데서 비롯됩니다. 예를 들어:
class Foo
{
static int a = b + 1;
static int b = a * 2;
}
a와 b는 어떤 값이 되고, 초기화는 어떤 순서로 실행되며, 초기화가 실행되기 전 a와 b의 값은 무엇인가요? 이것은 컴파일 오류일까요, 런타임 오류일까요? 게다가 어떤 초기화자가 정적인지 동적인지 명확하지 않아서 혼란이 더 커집니다.
D는 이 문제를 단순하게 만들었어요. 모든 멤버 초기화는 컴파일러가 컴파일 타임에 결정할 수 있어야 하며, 따라서 멤버 초기화에는 평가 순서 의존성이 없고, 아직 초기화되지 않은 값을 읽는 것도 불가능합니다. 동적 초기화는 static this()라는 특별한 문법으로 정의되는 정적 생성자가 수행합니다.
class Foo
{
static int a; // default initialized to 0
static int b = 1;
static int c = b + a; // error, not a constant initializer
static this() // static constructor
{
a = b + 1; // a is set to 2
b = a * 2; // b is set to 4
}
}
main()이나 스레드가 정상적으로 (예외를 던지지 않고) 반환하면, 정적 소멸자가 스레드 종료 시 호출될 함수 목록에 추가됩니다.
정적 생성자는 빈 매개변수 목록을 가져요.
모듈 안의 정적 생성자들은 나타나는 어휘적 순서(lexical order)대로 실행됩니다. 직접 또는 간접적으로 import된 모듈들의 모든 정적 생성자는, import한 모듈의 정적 생성자보다 먼저 실행돼요.
정적 생성자 선언에서의 static은 속성이 아니에요. 반드시 this 바로 앞에 와야 합니다:
class Foo
{
static this() { ... } // a static constructor
static private this() { ... } // Error: not a static constructor
static
{
this() { ... } // not a static constructor
}
static:
this() { ... } // not a static constructor
}
정적 소멸자 (Static Destructors)
StaticDestructor:
static ~ this ( ) MemberFunctionAttributesopt FunctionBody
정적 소멸자는 static ~this()라는 특별한 정적 함수로 정의됩니다.
class Foo
{
static ~this() // static destructor
{
}
}
정적 소멸자는 스레드 종료 시 호출되는데, 정적 생성자가 성공적으로 완료된 경우에만 호출됩니다. 정적 소멸자는 빈 매개변수 목록을 가져요. 정적 소멸자는 정적 생성자가 호출된 순서의 역순으로 호출됩니다.
정적 소멸자 선언에서의 static은 속성이 아니에요. 반드시 ~this 바로 앞에 와야 합니다:
class Foo
{
static ~this() { ... } // a static destructor
static private ~this() { ... } // Error: not a static destructor
static
{
~this() { ... } // not a static destructor
}
static:
~this() { ... } // not a static destructor
}
공유 정적 생성자 (Shared Static Constructors)
SharedStaticConstructor:
shared static this ( ) MemberFunctionAttributesopt FunctionBody
shared static this ( ) MemberFunctionAttributesopt MissingFunctionBody
공유 정적 생성자(shared static constructor)는 어떤 StaticConstructor보다 먼저 실행되며, 공유 전역 데이터(shared global data)를 초기화하는 용도로 쓰입니다.
공유 정적 소멸자 (Shared Static Destructors)
SharedStaticDestructor:
shared static ~ this ( ) MemberFunctionAttributesopt FunctionBody
shared static ~ this ( ) MemberFunctionAttributesopt MissingFunctionBody
공유 정적 소멸자는 프로그램 종료 시, SharedStaticConstructor들이 실행된 순서의 역순으로 실행됩니다.
클래스 불변식 (Class Invariants)
Invariant:
invariant ( ) BlockStatement
invariant BlockStatement
invariant ( AssertArguments ) ;
클래스 불변식(Class Invariant)은 클래스 인스턴스의 멤버들 사이의 관계를 지정합니다. 이 관계는 공개 인터페이스(public interface)를 통한 인스턴스와의 어떤 상호작용에서도 반드시 성립해야 해요.
불변식은 const 멤버 함수 형태를 취합니다. 불변식 안에서 실행되는 모든 AssertExpression이 성공하면 불변식이 성립한다고 정의됩니다.
class Date
{
this(int d, int h)
{
day = d; // days are 1..31
hour = h; // hours are 0..23
}
invariant
{
assert(1 <= day && day <= 31);
assert(0 <= hour && hour < 24);
}
private:
int day;
int hour;
}
베이스 클래스의 클래스 불변식들은 파생 클래스의 클래스 불변식보다 먼저 적용됩니다.
클래스 안에는 불변식이 여러 개 있을 수 있어요. 그것들은 어휘적 순서대로 적용됩니다.
클래스 불변식은 클래스 생성자(있다면)의 종료 시점과 클래스 소멸자(있다면)의 진입 시점에 반드시 성립해야 합니다. 그리고 모든 public 또는 exported 비정적 멤버 함수의 진입과 종료 시점에도 성립해야 해요. 불변식의 적용 순서는 다음과 같습니다:
- 선행조건 (preconditions)
- 불변식 (invariant)
- 함수 본문 (function body)
- 불변식 (invariant)
- 후행조건 (postconditions)
불변식이 성립하지 않으면 프로그램은 유효하지 않은 상태에 들어갑니다.
Implementation Defined: 클래스 불변식이 런타임에 실행되는지 여부. 이것은 보통 컴파일러 스위치로 제어합니다. 불변식이 성립하지 않을 때의 동작은 보통 AssertExpression이 실패했을 때와 같아요.
Undefined Behavior: 불변식이 성립하지 않은 채로 실행이 계속되는 경우.
public 또는 exported 비정적 멤버 함수는 불변식 안에서 호출될 수 없어요.
class Foo
{
public void f() { }
private void g() { }
invariant
{
f(); // error, cannot call public member function from invariant
g(); // ok, g() is not public
}
}
Best Practices: 클래스 불변식 안에서 exported나 public 멤버 함수를 간접적으로 호출하지 마세요. 무한 재귀(infinite recursion)를 초래할 수 있어요. 불변식이 실행될 수도 있고 실행되지 않을 수도 있으므로, 불변식 안에서 부작용(side effects)에 의존하지 마세요. 또한 불변식을 가진 클래스에서는 mutable public 필드를 피하세요. 그런 필드가 있으면 불변식이 공개 인터페이스를 검증할 수 없기 때문이죠.
Scope 클래스 (Scope Classes)
Note: scope 클래스는 폐기(deprecated)되었어요. 계속 보기:
scope클래스 인스턴스.
scope 클래스는 scope 속성을 가진 클래스입니다:
scope class Foo { ... }
scope 특성은 상속되므로, scope 클래스에서 파생된 어떤 클래스도 scope가 됩니다.
scope 클래스 참조는 함수 지역 변수로만 나타날 수 있어요. 반드시 scope로 선언되어야 합니다:
scope class Foo { ... }
void func()
{
Foo f; // error, reference to scope class must be scope
scope Foo g = new Foo(); // correct
}
scope 클래스 참조가 스코프를 벗어나면 그 참조에 대한 소멸자(있다면)가 자동으로 호출돼요. 이는 예외를 던져 스코프를 빠져나간 경우에도 마찬가지입니다.
추상 클래스 (Abstract Classes)
추상 멤버 함수는 파생 클래스가 반드시 오버라이드(override)해야 해요. 가상 멤버 함수만 abstract로 선언될 수 있으며, 비가상 멤버 함수와 독립 함수(free-standing functions)는 abstract로 선언할 수 없어요.
어떤 클래스의 가상 멤버 함수가 abstract로 선언되었거나 abstract 속성 안에서 정의되었다면, 그 클래스는 추상 클래스(abstract class)입니다. 추상 클래스는 비가상 멤버 함수도 포함할 수 있다는 점에 주의하세요. 추상 클래스는 직접 인스턴스화할 수 없어요. 다른, 비추상 클래스의 베이스 클래스로만 인스턴스화될 수 있습니다.
class C
{
abstract void f();
}
auto c = new C; // error, C is abstract
class D : C {}
auto d = new D; // error, D is abstract
class E : C
{
override void f() {}
}
auto e = new E; // OK
abstract로 선언된 멤버 함수는 여전히 함수 본문을 가질 수 있어요. 오버라이드되어야 하긴 하지만 파생 클래스의 super.foo() 같은 것을 통해 '베이스 클래스 기능'을 제공할 수 있도록 하기 위해서죠. 이 경우에도 그 클래스는 여전히 추상 클래스이며 직접 인스턴스화할 수 없다는 점을 기억하세요.
클래스는 abstract로 선언될 수도 있어요:
abstract class A
{
// ...
}
auto a = new A; // error, A is abstract
class B : A {}
auto b = new B; // OK
Final 클래스 (Final Classes)
final 클래스는 서브클래싱(subclassing)될 수 없어요:
final class A { }
class B : A { } // error, class A is final
final 클래스의 메서드는 항상 final입니다.
중첩 클래스 (Nested Classes)
중첩 클래스는 함수나 다른 클래스의 스코프 안에서 선언된 클래스예요. 중첩 클래스는 자신이 중첩된 클래스와 함수의 변수 및 다른 심볼들에 접근할 수 있습니다:
class Outer
{
int m;
class Inner
{
int foo()
{
return m; // Ok to access member of Outer
}
}
}
void func()
{
int m;
class Inner
{
int foo()
{
return m; // Ok to access local variable m of func()
}
}
}
정적 중첩 클래스 (Static Nested Classes)
중첩 클래스가 static 속성을 가지면, 스택에 국한된(local to the stack) 바깥 스코프의 변수나 this 참조를 필요로 하는 변수에는 접근할 수 없어요:
class Outer
{
int m;
static int n;
static class Inner
{
int foo()
{
return m; // Error, Inner is static and m needs a this
return n; // Ok, n is static
}
}
}
void func()
{
int m;
static int n;
static class Inner
{
int foo()
{
return m; // Error, Inner is static and m is local to the stack
return n; // Ok, n is static
}
}
}
컨텍스트 포인터 (Context Pointer)
비정적 중첩 클래스는 컨텍스트 포인터(context pointer)라는 숨겨진 멤버를 추가로 담는 방식으로 동작합니다. 이 포인터는 중첩 클래스가 함수 안에 중첩되어 있으면 바깥 함수의 프레임 포인터(frame pointer)이고, 클래스 안에 중첩되어 있으면 바깥 클래스 인스턴스의 this 참조예요.
비정적 중첩 클래스가 인스턴스화될 때, 컨텍스트 포인터는 클래스의 생성자가 호출되기 전에 할당됩니다. 따라서 생성자는 바깥쪽 변수들에 완전히 접근할 수 있어요. 비정적 중첩 클래스는 필요한 컨텍스트 포인터 정보가 이용 가능할 때만 인스턴스화될 수 있습니다:
class Outer
{
class Inner { }
static class SInner { }
}
void main()
{
Outer o = new Outer; // Ok
//Outer.Inner oi = new Outer.Inner; // Error, no 'this' for Outer
Outer.SInner os = new Outer.SInner; // Ok
}
void main()
{
class Nested { }
Nested n = new Nested; // Ok
static f()
{
//Nested sn = new Nested; // Error, no 'this' for Nested
}
}
명시적 인스턴스화 (Explicit Instantiation)
내부 클래스 인스턴스 생성에 this 참조를 제공하려면, NewExpression 앞에 this 참조를 붙이면 됩니다:
class Outer
{
int a;
class Inner
{
int foo()
{
return a;
}
}
}
void main()
{
Outer o = new Outer;
o.a = 3;
Outer.Inner oi = o.new Inner;
assert(oi.foo() == 3);
}
여기서 o가 Outer의 내부 클래스 인스턴스에 this 참조를 공급하고 있어요.
outer 속성 (outerProperty)
중첩 클래스 인스턴스에 대해, .outer 속성은 바깥 클래스 인스턴스의 this 참조를 제공합니다. 접근 가능한 부모 클래스 인스턴스가 없으면, 이 속성은 바깥 함수 프레임을 가리키는 void*를 제공해요.
class Outer
{
class Inner1
{
Outer getOuter()
{
return this.outer;
}
}
void foo()
{
Inner1 i = new Inner1;
assert(i.getOuter() is this);
}
}
class Outer
{
void bar()
{
// x is referenced from nested scope, so
// bar makes a closure environment.
int x = 1;
class Inner2
{
Outer getOuter()
{
x = 2;
// The Inner2 instance has access to the function frame
// of bar as a static frame pointer, but .outer returns
// the enclosing Outer class instance property.
return this.outer;
}
}
Inner2 i = new Inner2;
assert(i.getOuter() is this);
}
}
class Outer
{
// baz cannot access an instance of Outer
static void baz()
{
// make a closure environment
int x = 1;
class Inner3
{
void* getOuter()
{
x = 2;
// There's no accessible enclosing class instance, so the
// .outer property returns the function frame of baz.
return this.outer;
}
}
Inner3 i = new Inner3;
assert(i.getOuter() !is null);
}
}
익명 중첩 클래스 (Anonymous Nested Classes)
익명 중첩 클래스는 NewAnonClassExpression으로 정의되고 인스턴스화됩니다:
NewAnonClassExpression:
new PlacementExpressionopt class ConstructorArgsopt AnonBaseClassListopt AggregateBody
ConstructorArgs:
( NamedArgumentListopt )
AnonBaseClassList:
SuperClassOrInterface
SuperClassOrInterface , Interfaces
이것은 다음과 동등합니다:
class Identifier : AnonBaseClassList AggregateBody
// ...
new Identifier ConstructorArgs
여기서 Identifier는 익명 중첩 클래스를 위해 생성된 이름이에요.
interface I
{
void foo();
}
auto obj = new class I
{
void foo()
{
writeln("foo");
}
};
obj.foo();
Const, Immutable 및 Shared 클래스 (Const, Immutable and Shared Classes)
ClassDeclaration이 const, immutable, shared 저장 클래스(storage class)를 가지면, 클래스의 각 멤버가 그 저장 클래스로 선언된 것과 같아져요. 베이스 클래스가 const, immutable 또는 shared라면, 그로부터 파생된 모든 클래스도 const, immutable 또는 shared입니다.
더 알아보기 (Learn more)
- 인터페이스 (Interfaces): https://dlang.org/spec/interface.html
- 구조체와 공용체 (Structs and Unions): https://dlang.org/spec/struct.html
- 전체 D 언어 공식 사양 (Language Reference): https://dlang.org/spec/spec.html