인터페이스
인터페이스 (Interfaces)
본문
인터페이스는 C++의 순수 가상 클래스만큼이나 단순하게 시작하지만, D에서는 좀 더 많은 일을 할 수 있게 확장돼 있어요. 이번 장에서 인터페이스가 무엇인지, 어떻게 선언하고, 클래스가 어떻게 이를 구현하는지, 그리고 D만의 특별한 인터페이스 변형(COM/C++)까지 차근차근 살펴볼게요.
인터페이스 선언 (Interface Declarations)
인터페이스(interface) 는 인터페이스에서 상속 받은 클래스가 반드시 구현해야 하는 함수 목록을 기술해요.
InterfaceDeclaration:
interface Identifier ;
interface Identifier BaseInterfaceList_opt AggregateBody
InterfaceTemplateDeclaration
BaseInterfaceList:
: Interfaces
Implementation Defined: 특수화된 인터페이스가 지원될 수 있어요:
- COM 인터페이스 — Windows용 COM/OLE/ActiveX 객체와 이진 호환이 돼요.
- C++ 인터페이스 — C++ 추상 클래스와 이진 호환이 돼요.
- Objective-C 인터페이스 — Objective-C 프로토콜과 이진 호환이 돼요.
인터페이스에서 상속받은 클래스는 그 인터페이스 타입으로 암시적으로 변환될 수 있어요.
인터페이스는 클래스에서 파생될 수 없어요. 오직 다른 인터페이스에서만 파생될 수 있죠. 인터페이스 인스턴스는 상속받은 어떤 인터페이스로든 암시적으로 변환돼요. 그리고 클래스는 같은 인터페이스를 여러 번 파생할 수 없어요.
interface I
{
void foo();
}
class A : I, I // error, duplicate interface
{
}
인터페이스의 인스턴스는 직접 만들 수 없어요.
interface I
{
void foo();
}
...
I iface = new I(); // error, cannot create instance of interface
인터페이스 인스턴스는 Object로 캐스트하거나, 다른 클래스 타입으로 동적으로 캐스트할 수 있어요.
인터페이스 메서드 본문 (Interface Method Bodies)
가상 인터페이스 멤버 함수는 구현 본문을 가지지 않아요. 하지만 인터페이스는 static이나 final 함수는 구현할 수 있어요.
interface I
{
void bar() { } // error, implementation not allowed
static void foo() { } // ok
final void abc() { } // ok
}
인터페이스는 멤버에 함수 템플릿을 가질 수 있어요. 이때 인스턴스화된 모든 함수는 암시적으로 final이 돼요.
interface I
{
void foo(T)() { } // ok, it's implicitly final
}
인터페이스를 상속받은 클래스는 final 또는 static 인터페이스 멤버 함수를 오버라이드할 수 없어요.
interface I
{
void bar();
static void foo() { }
final void abc() { }
}
class C : I
{
void bar() { } // ok
void foo() { } // error, cannot override static I.foo()
void abc() { } // error, cannot override final I.abc()
}
인터페이스 구현 (Implementing Interfaces)
인터페이스에서 상속받은 클래스에는 모든 가상 인터페이스 함수가 정의되어야 해요.
interface I
{
void foo();
}
class A : I
{
void foo() { } // ok, provides implementation
}
class B : I
{
int foo() { } // error, no `void foo()` implementation
}
인터페이스는 기본 클래스에서 상속될 수 있고, 인터페이스 함수도 오버라이드할 수 있어요.
interface I
{
int foo();
}
class A : I
{
int foo() { return 1; }
}
class B : A
{
override int foo() { return 2; }
}
B b = new B();
assert(b.foo() == 2);
I i = b; // ok since B inherits A's I implementation
assert(i.foo() == 2);
여기서 핵심은 B가 I의 구현을 A로부터 물려받았기 때문에 B의 인스턴스를 I 타입으로 넘기는 게 문제없다는 거예요. 그리고 i.foo()가 2를 돌려주는 걸 보면 가상 호출이 제대로 B.foo로 향하는 걸 확인할 수 있죠.
인터페이스 재구현 (Reimplementing Interfaces)
인터페이스는 파생 클래스에서 다시 구현(reimplement) 될 수 있어요.
interface I
{
int foo();
}
class A : I
{
int foo() { return 1; }
}
class B : A, I // reimplement I
{
override int foo() { return 2; }
}
B b = new B();
assert(b.foo() == 2);
I i = b;
assert(i.foo() == 2);
A a = b;
I i2 = a;
assert(i2.foo() == 2); // i2 has A's virtual pointer for foo which points to B.foo
마지막에 a(타입은 A)를 I로 변환한 i2도 B.foo를 가리킨다는 게 중요해요. A의 가상 포인터가 이미 B.foo를 향하고 있기 때문이에요.
재구현된 인터페이스는 슈퍼 클래스에서 함수를 물려받지 않고, 인터페이스의 모든 함수를 스스로 구현해야 해요.
interface I
{
int foo();
}
class A : I
{
int foo() { return 1; }
}
class B : A, I
{
} // error, no foo() for interface I
A에 이미 foo()가 있지만, B가 I를 재구현하면서 foo()를 따로 제공하지 않았기 때문에 컴파일 오류가 나요.
인터페이스 메서드 계약 (Interface Method Contracts)
인터페이스 멤버 함수는 본문이 없어도 계약(contract) 을 가질 수 있어요. 이 계약들은 그 인터페이스 멤버 함수를 구현하는 모든 클래스 멤버 함수에 상속돼요.
interface I
{
int foo(int i)
in { assert(i > 7); }
out (result) { assert(result & 1); }
void bar();
}
즉, 인터페이스 단계에서 in/out 조건을 걸어 두면, 이를 구현하는 클래스 메서드에서도 그 조건이 자동으로 검사돼요.
const 및 immutable 인터페이스 (Const and Immutable Interfaces)
인터페이스가 const나 immutable storage class를 갖는다면, 인터페이스의 모든 멤버가 const 또는 immutable이 돼요. 이 storage class는 상속되지 않아요.
COM 인터페이스 (COM Interfaces)
인터페이스의 한 변형으로 COM 인터페이스가 있어요. COM 인터페이스는 Windows COM 객체에 직접 매핑되도록 설계됐어요. 어떤 COM 객체든 COM 인터페이스로 표현할 수 있고, COM 인터페이스를 가진 어떤 D 객체든 외부 COM 클라이언트에서 사용할 수 있어요.
COM 인터페이스는 core.sys.windows.com.IUnknown 인터페이스에서 파생되는 것으로 정의돼요. COM 인터페이스는 일반 D 인터페이스와 다음 점에서 달라요:
core.sys.windows.com.IUnknown인터페이스에서 파생돼요.destroy의 인자가 될 수 없어요.- 참조를 둘러싼 클래스 객체로 업캐스트할 수 없고, 파생 인터페이스로 다운캐스트할 수도 없어요. 다른 COM 인터페이스로 변환하려면 표준 COM 방식대로 해당 인터페이스의
QueryInterface()를 구현해야 해요. - COM 인터페이스에서 파생된 클래스는 COM 클래스가 돼요.
- COM 클래스 멤버 함수의 기본 링키지는
extern(System)이에요.
Note:
IUnknown에서 상속되지 않은 D 인터페이스나 클래스의 기본 클래스 메서드를 구현하거나 오버라이드할 때는 명시적으로extern(D)링키지로 표시해야 해요.
import core.sys.windows.windows;
import core.sys.windows.com;
interface IText
{
void write();
}
abstract class Printer : IText
{
void print() { }
}
class C : Printer, IUnknown
{
// Implements the IText `write` class method.
extern(D) void write() { }
// Overrides the Printer `print` class method.
extern(D) override void print() { }
// Overrides the Object base class `toString` method.
extern(D) override string toString() { return "Class C"; }
// Methods of class implementing the IUnknown interface have
// the extern(System) calling convention by default.
HRESULT QueryInterface(const(IID)*, void**);
uint AddRef();
uint Release();
}
같은 규칙이 opCmp, toHash 등 다른 Object 메서드에도 적용돼요.
- COM
vtbl[]의 첫 번째 멤버는InterfaceInfo에 대한 포인터가 아니라 첫 번째 가상 함수 포인터예요.
더 자세한 내용은 Modern COM Programming in D를 참고해요.
C++ 인터페이스 (C++ Interfaces)
C++ 인터페이스는 C++ 링키지로 선언된 인터페이스예요.
extern (C++) interface Ifoo
{
void foo();
void bar();
}
이는 다음 C++ 선언에 대응하려는 의도예요.
class Ifoo
{
virtual void foo();
virtual void bar();
};
C++ 인터페이스에서 파생된 인터페이스도 모두 C++ 인터페이스예요. C++ 인터페이스는 D 인터페이스와 다음 점에서 달라요:
destroy의 인자가 될 수 없어요.- 참조를 둘러싼 클래스 객체로 업캐스트할 수 없고, 파생 인터페이스로 다운캐스트할 수도 없어요.
- 멤버 함수의 기본 호출 규약이 C++ 호출 규약이에요. D 호출 규약이 아니라요.
vtbl[]의 첫 번째 멤버는Interface에 대한 포인터가 아니라 첫 번째 가상 함수 포인터예요.
더 알아보기 (Learn more)
- Classes — 인터페이스의 기반이 되는 클래스와 상속
- Enums — 다음 챕터
- D Language Reference 목차