Enums
Enums (열거형)
D 언어의 enum은 상수들의 그룹을 한 번에 정의하는 문법이에요. 그냥 상수 몇 개를 나열하는 단순한 기능처럼 보이지만, 실제로는 이름 있는 열거형(named enum), 이름 없는 열거형(anonymous enum), 그리고 **매니페스트 상수(manifest constant)**까지 세 가지 용도를 담고 있어서 처음엔 조금 헷갈릴 수 있어요. 이 장에서는 각각이 무엇이고, 언제 쓰는지, 어떤 규칙이 따라오는지 하나씩 풀어볼게요.
본문
먼저 enum 선언의 문법부터 볼게요. 문법 규칙은 다음과 같아요.
EnumDeclaration:
enum Identifier EnumBody
enum Identifier : EnumBaseType EnumBody
AnonymousEnumDeclaration
EnumBaseType:
Type
EnumBody:
{ EnumMembers }
;
EnumMembers:
EnumMember
EnumMember ,
EnumMember , EnumMembers
EnumMember:
EnumMemberAttributesopt Identifier
EnumMemberAttributesopt Identifier = AssignExpression
EnumMemberAttributes:
EnumMemberAttribute
EnumMemberAttribute EnumMemberAttributes
EnumMemberAttribute:
DeprecatedAttribute
UserDefinedAttribute
@ disable
AnonymousEnumDeclaration:
enum : EnumBaseType { EnumMembers }
enum { AnonymousEnumMembers }
AnonymousEnumMembers:
AnonymousEnumMember
AnonymousEnumMember ,
AnonymousEnumMember , AnonymousEnumMembers
AnonymousEnumMember:
EnumMember
EnumMemberAttributesopt Type Identifier = AssignExpression
enum 선언은 상수들의 그룹을 정의하는 데 쓰여요. 선언에 따라 이름 있는 열거형이 될 수도, 이름 없는 열거형이 될 수도 있죠. 하나씩 살펴볼게요.
Named Enums (이름 있는 열거형)
**이름 있는 열거형(named enum)**은 서로 관련된 상수들을 선언하고, 그것들에 고유한 타입을 부여해 묶어 주는 역할을 해요. EnumMembers는 이름 있는 열거형의 스코프 안에서 선언되며, 이름 있는 열거형은 새로운 타입을 선언하고, 모든 EnumMembers가 그 타입을 가지게 돼요.
예를 들어 다음 코드는 X.A = 0, X.B = 1, X.C = 2 값을 가지는 **새 타입 X**를 정의해요.
enum X { A, B, C } // named enum
변수는 이 이름 있는 열거형 타입을 가질 수 있어요. 이때 변수의 **기본 초기값(default initializer)**은 열거형 타입에 정의된 첫 번째 멤버예요.
enum X { A = 3, B, C }
X x;
assert(x == X.A);
x |= X.B;
assert(x & X.A);
이름 있는 열거형에 대한 이항 연산(binary operation)의 결과 타입이 어떻게 정해지는지는 별도로 정의되어 있어요. 기본 규칙은 이렇습니다.
EnumBaseType이 명시적으로 설정되지 않고, 첫 번째EnumMember에AssignExpression이 있으면EnumBaseType은 그AssignExpression의 타입으로 설정돼요.- 그렇지 않으면 기본값인
int타입으로 설정돼요.
변환 규칙도 꼭 기억해야 해요. 방향이 한쪽으로만 열려 있어요.
- 이름 있는 열거형 멤버는 자신의
EnumBaseType으로 암시적 형변환될 수 있어요. - 하지만
EnumBaseType의 인스턴스는 이름 있는 열거형 타입으로 암시적 형변환될 수 없어요.
enum Foo { E }
Foo f;
int i;
i = f; // OK
f = i; // error
f = cast(Foo)i; // OK
f = 0; // error
f = Foo.E; // OK
또, 이름 있는 열거형 멤버는 자체 Type을 선언할 수 없어요.
참고로, 이름 있는 열거형에 대한 final switch에 대해서도 함께 알아두면 좋아요.
Enum Member Values (열거 멤버 값)
EnumMember의 값은 AssignExpression이 있으면 그 값으로 정해져요. AssignExpression이 없는 경우를 나눠볼게요.
- **첫 번째
EnumMember**라면 그 값은EnumBaseType으로0을 변환한 값이 돼요. - 첫 번째가 아니라면 이전
EnumMember의 값에+1한 값이 돼요.
여기에 몇 가지 예외 규칙이 붙어 있어요.
- 이전
EnumMember의 값이 **EnumBaseType.max**라면 오류예요. 이 규칙이 값 오버플로(overflow)를 막아 주죠. 이전 멤버를 컴파일 타임에EnumBaseType.max와 비교할 수 없다면 그것도 오류예요. - 기반 타입이 컴파일 타임에 평가 가능한
+1연산을 정의하지 않으면 오류예요. - 이전 멤버 값에
+1한 값이 이전 멤버 값과 같다면 오류예요. (부동소수점 타입에서 이런 일이 발생할 수 있어요.)
enum E : char
{
a,
b = char.max,
c // overflow
}
static assert(E.a == 0);
모든 EnumMembers는 AssignExpression 안에서 스코프에 들어와 있어요. 즉 멤버끼리 서로를 참조할 수 있죠. 다만 순환 참조는 조심해야 해요.
enum A = 3;
enum B
{
A = A // error, circular reference
}
enum C
{
A = B, // A = 4
B = D, // B = 4
C = 3, // C = 3
D // D = 4
}
enum E : C
{
E1 = C.D,
E2 // error, C.D is C.max
}
빈 열거형 몸체는 **불투명 열거형(opaque enum)**을 뜻해요. 이 경우 열거형 멤버들이 알려지지 않은 상태가 돼요.
enum X; // opaque enum
writeln(X.init); // error: enum X is opaque and has no default initializer
Enum Properties (열거 프로퍼티)
열거 프로퍼티(enum properties)는 이름 있는 열거형에만 존재해요. 이름 없는 열거형에는 없어요. 이름 있는 열거형이 제공하는 프로퍼티는 다음과 같아요.
| 프로퍼티 | 설명 |
|---|---|
.init |
첫 번째 열거 멤버 값 |
.min |
가장 작은 열거 멤버 값 |
.max |
가장 큰 열거 멤버 값 |
.sizeof |
열거 값 하나를 저장하는 데 필요한 크기 |
예를 들어볼게요.
enum X { A = 3, B = 1, C = 4, D, E = 2 }
X.init // is X.A
X.min // is X.B
X.max // is X.D
X.sizeof // is same as int.sizeof
이름 있는 열거형의 EnumBaseType은 .max와 .min 프로퍼티를 계산하려면 비교 연산을 지원해야 해요.
Enum Copying and Assignment (열거 복사와 할당)
이름 있는 열거형 타입은 어떤 경우에도 복사 생성자, postblit, ID 할당 오버로드(identity assignment overload)를 갖지 않아요. 설령 그것의 EnumBaseType이 그런 것을 정의하고 있더라도요. 이 규칙 덕분에 열거형 복사·할당은 항상 단순한 값 복사로 동작해요.
기반 타입이 복사 생성자를 가진 구조체인데, 이름 있는 열거형 값을 복사하면 복사 생성자는 호출되지 않아요.
struct S
{
this(ref S rhs) { assert(0); }
}
enum E : S { A = S.init }
void main()
{
E e1;
E e2 = e1; // ok - copy constructor not called
}
기반 타입이 postblit을 가진 구조체여도, 이름 있는 열거형 값을 복사하면 postblit은 호출되지 않아요.
struct S
{
this(this) { assert(0); }
}
enum E : S { A = S.init }
void main()
{
E e1;
E e2 = e1; // ok - postblit not called
}
같은 타입의 다른 객체에 이름 있는 열거형 값을 할당할 때, 값들의 기반 타입이 ID 할당 오버로드를 가진 구조체라면, 그 오버로드도 호출되지 않아요.
struct S
{
void opAssign(S rhs) { assert(0); }
}
enum E : S { A = S.init }
void main()
{
E e1, e2;
e2 = e1; // ok - opAssign not called
}
Anonymous Enums (이름 없는 열거형)
enum 뒤에 Identifier가 없다면, 그 열거형은 **이름 없는 열거형(anonymous enum)**이 돼요. 이때 EnumMembers는 EnumDeclaration이 나타난 스코프에 직접 선언되며, 새로운 타입은 만들어지지 않아요.
이름 없는 열거형의 EnumMembers는 서로 다른 타입을 가질 수 있어요. 각 멤버의 타입은 다음 순서로 정해져요. 앞에 해당하는 규칙이 있을수록 우선순위가 높아요.
- **
Type**이 있으면 그 타입. (EnumBaseType이 있을 때는Type을 쓸 수 없어요.) - **
EnumBaseType**이 있으면 그 타입. AssignExpression이 있으면 그 표현식의 타입.- 이전
EnumMember가 있으면 이전 멤버의 타입. - 위 어디에도 해당하지 않으면
int.
enum { A, B, C } // anonymous enum
위 코드는 A = 0, B = 1, C = 2 상수를 정의하며, 세 상수 모두 int 타입이에요.
열거형은 최소한 하나의 멤버를 가져야 해요.
값을 정하는 규칙도 이름 있는 열거형과 조금 달라요. EnumMember의 값은 AssignExpression이 있으면 그 값이에요. 없을 때는 이렇게 나뉘어요.
- **첫 번째
EnumMember**라면 값은 해당 멤버 타입의.init프로퍼티예요. - 첫 번째가 아니라면 이전
EnumMember의 값에+1한 값이에요.
역시 오류 규칙이 따라와요.
- 이전
EnumMember의 값이 이전 멤버 타입의.max프로퍼티라면 오류예요. 값 오버플로를 막기 위한 규칙이죠. 이전 멤버를 컴파일 타임에 자신의.max프로퍼티와 비교할 수 없다면 그것도 오류예요. - 이전 멤버의 타입이 컴파일 타임에 평가 가능한
+1연산을 정의하지 않으면 오류예요. - 이전 멤버 값에
+1한 값이 이전 멤버 값과 같다면 오류예요. (부동소수점 타입에서 발생할 수 있어요.)
모든 EnumMembers는 AssignExpression 안에서 스코프에 들어와 있어요.
enum { A, B = 5 + 7, C, D = 8 + C, E }
이 코드는 A = 0, B = 12, C = 13, D = 21, E = 22를 설정하며, 모두 int 타입이에요.
EnumBaseType을 붙이면 모든 멤버가 그 타입이 돼요.
enum : long { A = 3, B }
이 코드는 A = 3, B = 4를 설정하며, 둘 다 long 타입이에요.
enum : string
{
A = "hello",
B = "betty",
C // error, cannot add 1 to "betty"
}
타입과 값을 섞어 쓰는 것도 가능해요. 각 멤버의 타입은 앞에서 본 우선순위 규칙에 따라 정해져요.
enum
{
A = 1.2f, // A is 1.2f of type float
B, // B is 2.2f of type float
int C = 3, // C is 3 of type int
D // D is 4 of type int
}
Single Member Syntax (단일 멤버 문법)
이름 없는 열거형에 멤버가 하나뿐이라면, { }를 생략할 수 있어요. 문법적으로 보면 이것은 VarDeclarations의 한 형태예요.
enum i = 4; // i is 4 of type int
enum long l = 3; // l is 3 of type long
이 형태를 **매니페스트 상수(manifest constant)**라고 불러요.
Manifest Constants (매니페스트 상수)
열거 멤버는 매니페스트 상수이며, 컴파일 타임에만 존재해요.
매니페스트 상수는 lvalue가 아니에요. 즉 그 주소를 취할 수 없어요. 그것은 오직 컴파일러의 메모리 안에만 존재하기 때문이에요.
enum size = __traits(classInstanceSize, Foo); // evaluated at compile-time
매니페스트 상수의 초기화식은 **컴파일 타임 함수 평가(compile-time function evaluation)**로 계산돼요.
template Foo(T)
{
// Not bad, but the 'size' variable will be located in the executable.
const size_t size = T.sizeof; // evaluated at compile-time
// ... use of 'size' at compile-time ...
}
template Bar(T)
{
// Better, the manifest constant has no runtime location in the executable.
enum size_t size = T.sizeof; // evaluated at compile-time
// ... use of 'size' at compile time ...
// Taking the address of Foo!T.size also causes it to go into the exe file.
auto p = &Foo!T.size;
}
매니페스트 상수는 실행 파일에 런타임 위치를 갖지 않는다는 점이 포인트예요. 그래서 위의 Bar 템플릿처럼 매니페스트 상수를 쓰면 실행 파일을 더 가볍게 유지할 수 있어요.
더 알아보기 (Learn more)
enum과 함께 자주 나오는final switch는 Switch Statement에서 다뤄요.- 매니페스트 상수와 관련된 **컴파일 타임 함수 평가(CTFE)**는 D 언어 사양의 컴파일 타임 실행 관련 내용을 참고하세요.
- 열거형의 기반 타입이 되는 타입 규칙은 Type 문서를 함께 보면 좋아요.