클래스

클래스 (Classes)

D의 객체 지향 기능은 모두 클래스에서 비롯돼요. 클래스 계층 구조의 뿌리는 Object 클래스예요. Object는 각 파생 클래스가 갖추어야 할 최소한의 기능과 그 기능에 대한 기본 구현을 정의해요.

출처: Classes

본문

클래스는 프로그래머가 정의하는 타입이에요. 클래스에 대한 지원 덕분에 D는 객체 지향 언어가 되어, 캡슐화(encapsulation)와 상속(inheritance), 다형성(polymorphism)을 갖추게 돼요. D 클래스는 단일 상속 패러다임을 지원하며, 인터페이스를 추가로 지원해 이를 확장할 수 있어요. 클래스 객체는 참조(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

클래스는 다음으로 구성돼요:

클래스는 다음과 같이 정의돼요:

class Foo
{
    ... members ...
}

참고: C++과 달리 클래스 정의의 닫는 } 뒤에는 세미콜론 ;이 없어요. 또한 변수 var를 인라인으로 선언할 수도 없어요:

class Foo { } var;

대신 다음과 같이 사용해요:

class Foo { }
Foo var;

접근 제어 (Access Control)

클래스 멤버에 대한 접근은 visibility 속성으로 제어해요. 기본 visibility 속성은 public이에요.

상속 (Inheritance)

모든 클래스는 super class로부터 상속받아요. super class가 지정되지 않으면 클래스는 Object로부터 상속받아요. Object는 D 클래스 상속 계층 구조의 뿌리를 이뤄요.

class A { }     // A inherits from Object
class B : A { } // B inherits from A

다중 클래스 상속은 지원되지 않지만, 클래스는 여러 인터페이스로부터 상속받을 수 있어요. super class가 선언되면 인터페이스보다 앞에 와야 해요. 상속된 타입을 구분하는 데는 쉼표가 사용돼요.

클래스 인스턴스는 상속된 타입으로 암시적으로 변환돼요. base class 인스턴스를 하위 클래스로 변환하려면 동적 캐스트 (dynamic cast)가 필요해요:

class Base {}
class Derived : Base {}

Base b = new Derived();         // implicit conversion
Derived d = cast(Derived) b;    // explicit conversion

필드 (Fields)

비-static 멤버 변수를 필드라고 불러요. 클래스 인스턴스의 멤버는 . 연산자로 접근해요.

base class의 멤버는 base class의 이름 뒤에 점을 붙여 접근할 수 있어요:

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) 클래스의 필드는 동적 오프셋을 가져요. 즉 base class가 변경되더라도(인스턴스 변수를 추가하거나 제거) 하위 클래스가 다시 컴파일하거나 재링크할 필요가 없어요.

클래스 속성 (Class Properties)

클래스 속성

| 속성 | 설명 | | .tupleof | 클래스 내 필드들의 기호 시퀀스(symbol sequence) |

클래스 인스턴스 속성

| 속성 | 설명 | | .classinfo | 클래스의 동적 타입에 대한 정보 | | .outer | 중첩 클래스 인스턴스의 경우, 부모 클래스 인스턴스를 제공하거나, 부모 클래스가 없을 때는 부모 함수의 컨텍스트 포인터를 제공 |

.tupleof

.tupleof 속성은 클래스 내 모든 비-static 필드(초기 hidden 필드와 base class의 필드는 제외)의 기호 시퀀스를 제공해요. 인스턴스에 대해 사용하면 .tupleoflvalue 시퀀스를 제공해요.

튜플에서 필드의 순서는 필드가 선언된 순서와 일치해요.

참고: extern(Objective-C) 클래스의 필드는 동적 오프셋을 가지므로 .tupleof를 사용할 수 없어요.

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);
}

Hidden 필드 접근 (Accessing Hidden Fields)

.__vptr 속성은 클래스 객체의 vtbl[]에 접근할 수 있게 해 주지만, 사용자 코드에서는 사용하지 말아야 해요. .__monitor 필드는 druntime에 정의되어 있지만, .tupleof와 traits로부터의 introspection에서는 제외돼요. 자세한 내용은 ABI를 참고하세요.

참고: 컨텍스트 포인터.

필드 속성 (Field Properties)

.offsetof 속성은 클래스 인스턴스 시작부터 필드까지의 오프셋(바이트)을 제공해요. 컴파일러가 클래스 필드 오프셋을 재배치할 수 있음을 참고하세요. struct 기반 예제는 align 속성을 참고하세요.

참고: extern(Objective-C) 클래스의 필드는 동적 오프셋을 가지므로 .offsetof를 사용할 수 없어요.

멤버 함수 (일명 메서드) (Member Functions)

비-static 멤버 함수는 자기 클래스의 인스턴스 위에서 호출해야 해요. 이 함수들은 this라고 하는 추가적인 숨겨진 매개변수를 가지며, 이를 통해 클래스 객체의 다른 멤버들에 접근할 수 있어요. 함수 본문 안에서는 클래스 인스턴스 멤버들이 스코프 안에 있어요.

class C
{
    int a;

    void foo()
    {
        a = 3; // assign to `this.a`
    }
}

auto c = new C;
c.foo();
assert(c.a == 3);

비-static 멤버 함수는 일반적인 FunctionAttribute 외에도 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 linkage)

Objective-C 연동을 가진 static 멤버 함수도 클래스 객체의 다른 멤버에 접근할 수 있게 해 주는 this라는 추가적인 숨겨진 매개변수를 가져요.

Objective-C 연동을 가진 멤버 함수는 함수가 호출될 때 사용된 셀렉터(selector)인 추가적이고 익명인 매개변수를 하나 더 가져요.

Objective-C 연동을 가진 static 멤버 함수는 숨겨진 중첩 메타클래스에 비-static 멤버 함수로 배치돼요.

동기화 메서드 호출 (Synchronized Method Calls)

(비-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 클래스의 각 멤버 함수는 암시적으로 동기화돼요. static 멤버 함수는 해당 클래스의 classinfo 객체에 대해 동기화되는데, 이는 동기화 클래스의 모든 static 멤버 함수에 하나의 모니터가 사용된다는 뜻이에요. 동기화 클래스의 비-static 함수의 경우, 사용되는 모니터는 클래스 객체의 일부예요. 예를 들어:

synchronized class Foo
{
    void bar() { ...statements... }
}

는 (모니터의 관점에서) 다음과 동일해요:

synchronized class Foo
{
    void bar()
    {
        synchronized (this) { ...statements... }
    }
}

참고: barSynchronizedStatement를 사용해요.

동기화 클래스의 멤버 필드는 public일 수 없어요:

synchronized class Foo
{
    int foo;  // Error: public field
}

synchronized class Bar
{
    private int bar;  // ok
}

참고: 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)로 인스턴스화할 수 있어요. 인자는 일치하는 매개변수 목록을 가진 생성자(그리고 오버로드된 메서드처럼 해석됨)에 전달돼요.

기본적으로 클래스 인스턴스는 가비지 컬렉션 힙에 할당돼요. 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 클래스 생성 (Base Class Construction)

Base 클래스의 생성은 super라는 이름으로 base 클래스 생성자를 호출하여 이루어져요:

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): 프로그램이 순환 생성자 호출로 실행되는 경우.

  • 생성자의 코드에 위임/base 생성자 호출이 포함되어 있으면, 생성자를 통한 모든 가능한 실행 경로가 그 호출들 중 정확히 하나를 만들어야 해요:
this() { a || super(); } // illegal, 0 or 1 call

this() { (a) ? this(1) : super(); } // OK

this() { super(); this(1); } // illegal, 2 calls

this()
{
    for (...)
    {
        super();  // illegal, inside loop
    }
}
  • 위임/base 생성자 호출을 하기 전에 this를 암시적으로나 명시적으로 참조하는 것은 불법이에요.
  • 위임/base 생성자 호출은 레이블 뒤에 나타날 수 없어요.

참고: 필드 초기화.

암시적 Base 클래스 생성 (Implicit Base Class Construction)

클래스에 생성자가 없지만 base 클래스에 생성자가 있으면, 다음과 같은 형태의 기본 생성자가 암시적으로 생성돼요:

this() { }

생성자에 위임 생성자나 super 호출이 나타나지 않고 base 클래스에 무인자(nullary) 생성자가 있으면, 생성자 시작 부분에 super() 호출이 삽입돼요. 그 base 클래스에 인자가 필요한 생성자만 있고 무인자 생성자가 없으면, 일치하는 super 호출이 필요해요.

인스턴스화 과정 (Instantiation Process)

다음 단계가 발생해요:

  • 객체를 위한 저장 공간이 할당돼요. 실패하면 null을 반환하는 대신 OutOfMemoryError가 던져져요. 따라서 null 참조에 대한 번거로운 검사가 필요 없어요.
  • 원시 데이터는 클래스 정의에 제공된 값을 사용해 정적으로 초기화돼요. vtbl[](가상 함수 포인터 배열)에 대한 포인터가 할당돼요. 생성자는 가상 함수를 호출할 수 있는 완전히 형성된 객체를 전달받아요. 이 연산은 객체의 정적 버전을 새로 할당된 객체에 메모리 복사하는 것과 동일해요.
  • 클래스에 대해 정의된 생성자가 있으면, 인자 목록과 일치하는 생성자가 호출돼요.
  • 위임 생성자가 호출되지 않으면, base 클래스의 기본 생성자에 대한 호출이 발행돼요.
  • 생성자의 본문이 실행돼요.
  • 클래스 불변식 검사가 켜져 있으면, 생성자 끝에 클래스 불변식이 호출돼요.

생성자 속성 (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)

생성자 본문에서 필드 할당의 첫 번째 발생이 그 필드의 초기화(initialization)예요.

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;
    }
}

필드 타입이 기본 생성이 비활성화된 경우, 생성자에서 초기화되어야 해요.

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 리소스를 해제할 것으로 기대돼요.

프로그램은 destroy로 살아있는 객체의 소멸자를 즉시 명시적으로 호출할 수 있어요. 런타임은 객체를 표시해서 소멸자가 두 번 호출되지 않도록 해요.

소멸자가 끝나면 super class의 소멸자가 자동으로 호출돼요. super class 소멸자를 명시적으로 호출할 방법은 없어요.

구현 정의 (Implementation Defined): 가비지 컬렉터가 모든 참조되지 않은 객체에 대해 소멸자를 반드시 실행한다는 보장은 없어요.

중요 (Important): 가비지 컬렉터가 참조되지 않은 객체에 대해 소멸자를 호출하는 순서는 명시되어 있지 않아요. 이는 가비지 컬렉터가 가비지 컬렉션된 객체를 참조하는 멤버를 가진 클래스의 객체에 대해 소멸자를 호출할 때, 그 참조들이 더 이상 유효하지 않을 수 있다는 뜻이에요. 따라서 소멸자는 하위 객체를 참조할 수 없어요.

참고: 이 규칙은 scope 클래스 인스턴스나 destroy로 소멸된 객체에는 적용되지 않아요. 그런 경우 소멸자가 가비지 컬렉션 주기 동안 실행되지 않으므로 모든 참조가 유효해요.

정적 데이터 세그먼트에서 참조되는 객체는 GC에 의해 수집되지 않아요.

정적 생성자와 소멸자 (Static Constructors and Destructors)

정적 생성자 (Static Constructors)

StaticConstructor:
    static this ( ) MemberFunctionAttributesopt FunctionBody
    static this ( ) MemberFunctionAttributesopt MissingFunctionBody

정적 생성자는 메인 스레드에서 main() 함수가 제어권을 얻기 전과 스레드 시작 시에 스레드 로컬 데이터의 초기화를 수행하는 함수예요.

정적 생성자는 컴파일 타임에 계산할 수 없는 값으로 정적 클래스 멤버를 초기화하는 데 사용돼요.

다른 언어에서는 컴파일 타임에 계산할 수 없는 멤버 이니셜라이저를 사용해 정적 생성자를 암시적으로 만드는데, 이는 코드가 정확히 언제 실행되는지에 대한 좋은 제어가 없어서 문제가 돼요. 예를 들어:

class Foo
{
    static int a = b + 1;
    static int b = a * 2;
}

ab는 어떤 값을 갖게 되고, 초기화는 어떤 순서로 실행되며, 초기화가 실행되기 전에 ab의 값은 무엇이며, 이것이 컴파일 오류인지 런타임 오류인지 — 추가적인 혼란은 이니셜라이저가 static인지 동적인지가 명확하지 않다는 데서 옵니다.

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()이나 스레드가 정상적으로(예외를 던지지 않고) 반환하면, 정적 소멸자가 스레드 종료 시 호출될 함수 목록에 추가돼요.

정적 생성자는 빈 매개변수 목록을 가져요.

모듈 내의 정적 생성자는 나타난 어휘적 순서대로 실행돼요. 직접 또는 간접적으로 import된 모듈들의 모든 정적 생성자는 importer의 정적 생성자보다 먼저 실행돼요.

정적 생성자 선언의 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

공유 정적 생성자는 StaticConstructor들보다 먼저 실행되며, 공유 전역 데이터를 초기화하기 위한 것이에요.

공유 정적 소멸자 (Shared Static Destructors)

SharedStaticDestructor:
    shared static ~ this ( ) MemberFunctionAttributesopt FunctionBody
    shared static ~ this ( ) MemberFunctionAttributesopt MissingFunctionBody

공유 정적 소멸자는 SharedStaticConstructor들이 실행된 역순으로 프로그램 종료 시에 실행돼요.

클래스 불변식 (Class Invariants)

Invariant:
    invariant ( ) BlockStatement
    invariant BlockStatement
    invariant ( AssertArguments ) ;

클래스 Invariant는 클래스 인스턴스 멤버들 사이의 관계를 명시해요. 그 관계들은 인스턴스와 공개 인터페이스의 모든 상호작용에 대해 유지되어야 해요.

불변식은 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;
}

base 클래스에 대한 클래스 불변식은 파생 클래스의 클래스 불변식보다 먼저 적용돼요.

클래스에는 여러 불변식이 있을 수 있어요. 그것들은 어휘적 순서로 적용돼요.

클래스 Invariant는 클래스 생성자가 종료될 때(있는 경우)와 클래스 소멸자가 시작될 때(있는 경우) 유지되어야 해요.

클래스 Invariant는 모든 public 또는 exported 비-static 멤버 함수의 시작과 종료 시에 유지되어야 해요. 불변식의 적용 순서는:

  • 전제 조건 (preconditions)
  • 불변식 (invariant)
  • 함수 본문
  • 불변식 (invariant)
  • 후제 조건 (postconditions)

불변식이 유지되지 않으면 프로그램은 잘못된 상태로 들어가요.

구현 정의 (Implementation Defined):

  • 클래스 Invariant가 런타임에 실행되는지 여부. 이는 보통 컴파일러 스위치로 제어돼요.
  • 불변식이 유지되지 않을 때의 동작은 보통 AssertExpression이 실패할 때와 동일해요.

정의되지 않은 동작 (Undefined Behavior): 불변식이 유지되지 않고 실행이 계속되는 경우 발생해요.

Public 또는 exported 비-static 멤버 함수는 불변식 내에서 호출될 수 없어요.

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 멤버 함수를 간접적으로 호출하지 마세요. 무한 재귀를 초래할 수 있어요.
  • 불변식에서 부작용에 의존하지 마세요. 불변식은 실행될 수도 있고 실행되지 않을 수도 있으니까요.
  • 불변식을 가진 클래스의 mutable public 필드를 두지 마세요. 그러면 불변식이 공개 인터페이스를 검증할 수 없게 되니까요.

Scope 클래스 (Scope Classes)

참고: Scope 클래스는 [더 이상 사용되지 않습니다 (deprecated)](../deprecate.html#scope as a type constraint). 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)

추상 멤버 함수는 파생 클래스에서 반드시 오버라이드되어야 해요. 가상 멤버 함수만 abstract로 선언될 수 있어요. 비가상 멤버 함수와 독립 함수는 abstract로 선언될 수 없어요.

클래스는 가상 멤버 함수 중 어떤 것이 abstract로 선언되거나 abstract 속성 안에서 정의되면 추상 클래스가 돼요. 추상 클래스는 비가상 멤버 함수도 포함할 수 있음을 참고하세요. 추상 클래스는 직접 인스턴스화될 수 없어요. 오직 다른 비-추상 클래스의 base 클래스로만 인스턴스화될 수 있어요.

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()를 통해 'base 클래스 기능'을 제공할 수 있게 하기 위함이에요. 클래스는 여전히 abstract이며 직접 인스턴스화될 수 없음을 참고하세요.

클래스는 abstract로 선언될 수 있어요:

abstract class A
{
    // ...
}

auto a = new A; // error, A is abstract

class B : A {}

auto b = new B; // OK

Final 클래스 (Final Classes)

final 클래스는 서브클래싱될 수 없어요:

final class A { }
class B : A { }  // error, class A is final

final 클래스의 메서드는 항상 final이에요.

중첩 클래스 (Nested Classes)

*중첩 클래스 (nested class)*는 함수나 다른 클래스의 스코프 안에서 선언된 클래스예요. 중첩 클래스는 자신이 중첩된 클래스와 함수의 변수와 기타 기호들에 접근할 수 있어요:

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 속성을 가지면, 스택에 로컬이거나 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)

비-static 중첩 클래스는 컨텍스트 포인터(constext pointer)라고 하는 추가적인 숨겨진 멤버를 포함해서 동작해요. 함수 안에 중첩된 경우에는 감싸는 함수의 프레임 포인터이고, 클래스 안에 중첩된 경우에는 감싸는 클래스 인스턴스의 this 참조예요.

비-static 중첩 클래스가 인스턴스화될 때, 컨텍스트 포인터는 클래스의 생성자가 호출되기 전에 할당되어, 생성자는 감싸는 변수들에 완전히 접근할 수 있어요. 비-static 중첩 클래스는 필요한 컨텍스트 포인터 정보가 사용 가능할 때만 인스턴스화될 수 있어요:

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)

내부 클래스 인스턴스의 생성에는 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);
}

여기서 oOuter의 내부 클래스 인스턴스에 this 참조를 제공해요.

outer 속성 (outer Property)

중첩 클래스 인스턴스의 경우, .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)

ClassDeclarationconst, immutable 또는 shared 저장 클래스를 가지면, 클래스의 각 멤버가 그 저장 클래스로 선언된 것과 같아요. base 클래스가 const, immutable 또는 shared이면, 그로부터 파생된 모든 클래스도 const, immutable 또는 shared예요.

Struct와 Union 인터페이스

더 알아보기