문장
문장 (Statements)
함수 내부의 실행 순서는 문장(Statement)들에 의해 제어돼요. 함수 본문은 0개 이상의 문장 시퀀스로 구성돼요. 실행은 어휘적 순서(lexical order)로 발생하며, 일부 문장은 지연된 효과를 가질 수 있어요. 이 문서는 D의 다양한 문장들 — 스코프, 조건/반복/선택(switch), 점프(goto/break/continue/return), with, synchronized, try, scope guard, asm, mixin 등 — 을 다루어요.
출처: Statements
본문
함수 내부의 실행 순서는 문장들에 의해 제어돼요. 함수 본문은 0개 이상의 문장 시퀀스로 구성돼요. 실행은 어휘적 순서로 발생하며, 특정 문장들은 지연된 효과(deferred effects)를 가질 수 있어요. 문장은 값을 갖지 않으며, 그 효과를 위해 실행돼요.
Statement:
EmptyStatement
NonEmptyStatement
ScopeBlockStatement
EmptyStatement:
;
NoScopeNonEmptyStatement:
NonEmptyStatement
BlockStatement
NoScopeStatement:
EmptyStatement
NonEmptyStatement
BlockStatement
NonEmptyOrScopeBlockStatement:
NonEmptyStatement
ScopeBlockStatement
NonEmptyStatement:
NonEmptyStatementNoCaseNoDefault
CaseStatement
CaseRangeStatement
DefaultStatement
NonEmptyStatementNoCaseNoDefault:
LabeledStatement
ExpressionStatement
DeclarationStatement
IfStatement
WhileStatement
DoStatement
ForStatement
ForeachStatement
SwitchStatement
FinalSwitchStatement
ContinueStatement
BreakStatement
ReturnStatement
GotoStatement
WithStatement
SynchronizedStatement
TryStatement
ScopeGuardStatement
AsmStatement
MixinStatement
ForeachRangeStatement
PragmaStatement
ConditionalStatement
StaticForeachStatement
ImportDeclaration
문법에서 Statement와 Declaration 사이의 모호성은 선언(declaration)이 우선하는 것으로 해결돼요. 그런 문장을 괄호로 감싸면 Statement로 해석되도록 명확히 구분(disambiguate)할 수 있어요.
스코프 문장 (Scope Statements)
ScopeStatement:
NonEmptyStatement
BlockStatement
NonEmptyStatement 또는 BlockStatement에 대해 지역 심볼(local symbols)의 새로운 스코프가 도입돼요. 새 스코프가 도입되더라도, 지역 심볼 선언은 같은 함수 안의 다른 지역 심볼 선언을 가릴(shadow) 수 없어요.
void func1(int x)
{
int x; // illegal, x shadows parameter x
int y;
{ int y; } // illegal, y shadows enclosing scope's y
void delegate() dg;
dg = { int y; }; // ok, this y is not in the same function
struct S
{
int y; // ok, this y is a member, not a local
}
{ int z; }
{ int z; } // ok, this z is not shadowing the other z
{ int t; }
{ t++; } // illegal, t is undefined
}
Best Practices: (모범 사례) 함수 내의 지역 선언은 겹치지 않는 스코프에 있더라도 모두 고유한 이름을 가져야 해요.
스코프 블록 문장 (Scope Block Statements)
ScopeBlockStatement:
BlockStatement
스코프 블록 문장은 BlockStatement에 대해 새로운 스코프를 도입해요.
라벨이 붙은 문장 (Labeled Statements)
문장에는 라벨을 붙일 수 있어요. 라벨은 문장 앞에 오는 식별자예요.
LabeledStatement:
Identifier :
Identifier : Statement
빈 문장을 포함해 어떤 문장이든 라벨을 붙일 수 있고, 따라서 goto 문장의 대상이 될 수 있어요. 라벨이 붙은 문장은 break나 continue 문장의 대상으로도 사용될 수 있어요. 라벨은 블록 끝에 뒤따르는 문장 없이 나타날 수 있어요.
라벨은 선언, 변수, 타입 등과 독립적인 이름 공간에 있어요. 그럼에도 라벨은 지역 선언과 같은 이름을 가질 수 없어요. 라벨 이름 공간은 라벨이 나타난 함수의 본문이에요. 라벨 이름 공간은 중첩되지 않아요. 즉 블록 문장 안의 라벨은 그 블록 밖에서도 접근 가능해요. 한 함수의 라벨을 다른 함수에서 참조할 수 없어요.
블록 문장 (Block Statement)
BlockStatement:
{ }
{ StatementList }
StatementList:
Statement
Statement StatementList
블록 문장은 { }로 둘러싸인 문장 시퀀스예요. 문장들은 어휘적 순서로 실행되고, 블록 끝에 도달하거나 문장이 제어를 다른 곳으로 옮길 때까지 실행돼요.
표현식 문장 (Expression Statement)
ExpressionStatement:
Expression ;
표현식이 평가돼요. (x + x)처럼 아무 효과가 없는 표현식은 void로 캐스팅되지 않는 한 표현식 문장으로 불법이에요.
int x;
x++; // ok
x; // illegal
1+1; // illegal
cast(void)(x + x); // ok
선언 문장 (Declaration Statement)
선언 문장은 변수를 정의하고, 타입, 템플릿, 함수, 임포트, 조건부, static foreach, static assert를 선언해요.
DeclarationStatement:
StorageClassesopt Declaration
몇 가지 선언 문장 예시:
int a; // declare a as type int and initialize it to 0
struct S { } // declare struct s
alias myint = int;
If 문장 (If Statement)
if 문장은 문장의 간단한 조건부 실행을 제공해요.
IfStatement:
if ( IfCondition ) ThenStatement
if ( IfCondition ) ThenStatement else ElseStatement
IfCondition:
Expression
IfConditionStorageClasses Identifier = Expression
IfConditionStorageClassesopt BasicType Declarator = Expression
IfConditionStorageClasses:
IfConditionStorageClass
IfConditionStorageClass IfConditionStorageClasses
IfConditionStorageClass:
scope
auto
ref
TypeCtor
ThenStatement:
ScopeStatement
ElseStatement:
ScopeStatement
선언된 Identifier 변수가 있으면 그것이 평가돼요. 그렇지 않으면 Expression이 평가돼요. 결과는 boolean으로 변환돼요. boolean이 true면 ThenStatement가 실행되고, 그렇지 않으면 ElseStatement가 실행돼요.
ElseStatement는 아직 ElseStatement가 연결되지 않은 가장 안쪽의 if 문장과 연결돼요. 2.112부터, 더 이른 IfStatement의 짝이 없는 else 절이 있을 때 else를 사용하는 것은 오류예요:
bool b;
int c;
if (b)
if (c)
return;
else // Error: else is dangling, add { } after condition
c++;
대신 BlockStatement를 사용해 두 번째 IfStatement를 감싸세요.
Rationale: (근거) 매달린 else(dangling else)가 어떤 IfStatement와 관련되는지 모호할 수 있어요. 버그를 유발하는 문법을 불법으로 만들면 버그 가능성이 줄어들어요.
Boolean 변환 (Boolean Conversion)
조건에서 지원되는 타입 인스턴스는 다음과 같아요:
- 0이 아닐 때 true인 산술 타입 (Arithmetic types)
- null이 아닐 때 true인 포인터
- null이 아닐 때 true인 참조 타입
- 유효한
opCast!bool()메서드를 가진 사용자 정의 타입은 결과를 위해 호출돼요. 그렇지 않으면, 대상 타입 자체가 이 규칙 중 하나와 일치하는 경우 어떤alias this멤버 선언이라도 사용돼요. - 기본 타입이 이 규칙 중 하나와 일치하면 true인 enum 타입
이 중 어느 것도 유효하지 않으면, 조건에서 그 값을 사용하는 것은 오류예요.
Note: 동적 배열
a는 조건에서 사용될 수 있는데,a !is []일 때 true예요. 하지만 배열을 boolean 조건으로 사용하는 것은 버그를 유발할 수 있으므로 피해야 해요. 이는 향후 에디션에서 허용되지 않을 수 있어요.
조건 변수 (Condition Variables)
IfCondition의 Identifier 형태가 사용되면, 그 이름으로 변수가 선언되고 Expression의 값으로 초기화돼요.
- IfConditionStorageClasses 중
auto와ref가 모두 있으면, Expression이 lvalue일 때에만 변수는 참조(reference)예요. ref가auto없이 있으면, 변수는 참조이고 Expression은 lvalue여야 해요.auto가ref없이 있으면 변수의 타입은 Expression과 같아요.auto의 존재가ref외의 어떤 IfConditionStorageClasses와 결합해도 효과가 없어요. 다른 IfConditionStorageClasses가 의미가 없을 때만,auto는 변수 선언 옵션을 제공해요. BasicType이 주어졌는데ref없이auto를 사용하는 것은 허용되지 않아요.- TypeCtors가 제공되고 가능하면 BasicType이 주어지면, 변수는 일반 변수 선언과 마찬가지로 선언돼요.
변수의 스코프는 ThenStatement뿐이에요.
import std.regex;
if (auto m = matchFirst("abcdef", "b(c)d"))
{
writefln("[%s]", m.pre); // prints [a]
writefln("[%s]", m.post); // prints [ef]
writefln("[%s]", m[0]); // prints [bcd]
writefln("[%s]", m[1]); // prints [c]
}
else
{
writeln("no match");
//writeln(m.post); // Error: undefined identifier 'm'
}
//writeln(m.pre); // Error: undefined identifier 'm'
While 문장 (While Statement)
WhileStatement:
while ( IfCondition ) ScopeStatement
While 문장은 간단한 루프를 구현해요. IfCondition이 Expression이면 평가되고 boolean으로 변환될 수 있는 타입을 가져야 해요. true면 ScopeStatement가 실행돼요. ScopeStatement가 실행된 후 Expression이 다시 평가되고, true면 ScopeStatement가 다시 실행돼요. Expression이 false로 평가될 때까지 계속돼요.
int i = 0;
while (i < 10)
{
foo(i);
++i;
}
IfCondition이 변수를 선언하면, 매 루프 반복마다 초기화되고 평가돼요. BreakStatement는 루프를 빠져나가요. ContinueStatement는 IfCondition을 다시 평가하는 것으로 직접 이동해요.
Do 문장 (Do Statement)
DoStatement:
do ScopeStatement while ( Expression ) ;
do-while 문장은 간단한 루프를 구현해요. ScopeStatement가 실행돼요. 그런 다음 Expression이 평가되고 boolean으로 변환될 수 있는 타입을 가져야 해요. true면 루프가 다시 반복돼요. Expression이 false로 평가될 때까지 계속돼요.
int i = 0;
do
{
foo(i);
} while (++i < 10);
BreakStatement는 루프를 빠져나가요. ContinueStatement는 Expression을 다시 평가하는 것으로 직접 이동해요.
For 문장 (For Statement)
for 문장은 초기화(init), 테스트, 증분(increment) 절이 있는 루프를 구현해요.
ForStatement:
for ( Initialize Testopt ; Incrementopt ) ScopeStatement
Initialize:
;
NoScopeNonEmptyStatement
Test:
Expression
Increment:
Expression
Initialize가 실행돼요. Test가 평가되고 boolean으로 변환될 수 있는 타입을 가져야 해요. Test가 true면 ScopeStatement가 실행돼요. 실행 후 Increment가 실행돼요. 그런 다음 Test가 다시 평가되고, true면 ScopeStatement가 다시 실행돼요. Test가 false로 평가될 때까지 계속돼요.
BreakStatement는 루프를 빠져나가요. ContinueStatement는 Increment로 직접 이동해요.
ForStatement는 새로운 스코프를 생성해요. Initialize가 변수를 선언하면 그 변수의 스코프는 ScopeStatement를 거쳐 확장돼요. 예를 들어:
for (int i = 0; i < 10; i++)
foo(i);
는 다음과 동등해요:
{
int i;
for (i = 0; i < 10; i++)
foo(i);
}
ScopeStatement는 빈 문장일 수 없어요:
for (int i = 0; i < 10; i++)
; // illegal
대신 다음을 사용하세요:
for (int i = 0; i < 10; i++)
{
}
Initialize는 그냥 ;일 수 있어요. Test는 생략될 수 있고, 생략되면 true로 평가된 것으로 취급돼요.
Best Practices: (모범 사례) ForStatement를 Foreach Statement나 Foreach Range Statement로 바꾸는 것을 고려하세요. Foreach 루프는 이해하기 쉽고, 오류에 덜 취약하며, 리팩터링하기 쉬워요.
Foreach 문장 (Foreach Statement)
foreach 문장은 일련의 값을 순회해요.
AggregateForeach:
Foreach ( ForeachTypeList ; ForeachAggregate )
ForeachStatement:
AggregateForeach NoScopeNonEmptyStatement
Foreach:
foreach
foreach_reverse
ForeachTypeList:
ForeachType
ForeachType , ForeachTypeList
ForeachType:
ForeachTypeAttributesopt BasicType Declarator
ForeachTypeAttributesopt Identifier
ForeachTypeAttributesopt alias Identifier
ForeachTypeAttributesopt ForeachTuplePattern
ForeachTypeAttributes:
ForeachTypeAttribute
ForeachTypeAttribute ForeachTypeAttributes
ForeachTypeAttribute:
enum
ref
scope
TypeCtor
ForeachAggregate:
Expression
ForeachAggregate가 평가돼요. 이는 정적 배열, 동적 배열, 연관 배열, struct, class, delegate, 또는 시퀀스인 표현식으로 평가되어야 해요. NoScopeNonEmptyStatement는 애그리게이트의 각 요소마다 한 번씩 실행돼요.
ForeachTypeList에 선언된 변수의 수는 애그리게이트의 종류에 따라 달라져요. 선언된 변수들은 각 반복 시작 시에 설정돼요.
- 기본적으로 단일 선언 변수는 현재 요소의 복사본이에요.
- ForeachTypeAttribute가
ref면 그 변수는 애그리게이트의 현재 요소에 대한 참조가 돼요. - ForeachTypeAttribute가
scope면 그 변수는 scope 의미론을 가져요.
지정되지 않으면 ForeachType 변수의 타입은 ForeachAggregate의 타입에서 추론될 수 있어요. auto는 유효한 ForeachTypeAttribute가 아니라는 점에 주의하세요. 아래 두 foreach 문장은 동등해요:
int[] arr = [1, 2, 3];
foreach (int n; arr)
writeln(n);
foreach (n; arr) // ok, n is an int
writeln(n);
애그리게이트는 루프 불변(loop invariant)이어야 해요. 즉 NoScopeNonEmptyStatement 안에서 요소를 추가하거나 제거할 수 없음을 의미해요. foreach 본문의 BreakStatement는 루프를 빠져나가요. ContinueStatement는 즉시 다음 반복을 시작해요.
배열에 대한 Foreach (Foreach over Arrays)
애그리게이트가 정적 또는 동적 배열이면, 하나 또는 두 개의 변수를 선언할 수 있어요. 하나면 그 변수는 값(value)이라 불리며 배열의 각 요소로 연속적으로 설정돼요. 지정된 변수의 타입은 배열 요소 타입과 호환되어야 해요 (아래에서 설명하는 문자 요소의 특수 처리를 제외하고). 값 변수는 ref로 선언되면 배열 요소를 수정할 수 있어요.
두 개의 변수가 선언되면 첫 번째는 인덱스(index), 두 번째는 위와 같이 값(value)이라 불려요. 인덱스는 ref로 선언될 수 없어요. 각 반복마다 배열 요소의 인덱스로 설정돼요. 인덱스 타입은 추론될 수 있어요:
char[] a = ['h', 'i'];
foreach (i, char c; a)
{
writefln("a[%d] = '%c'", i, c);
}
동적 배열의 경우, 배열 길이가 더 작은 정수 타입에 정적으로 맞는 것으로 알려지지 않는 한 인덱스 타입은 size_t와 호환되어야 해요. 정적 배열은 배열 길이를 가로지르는 어떤 정수 타입이라도 사용할 수 있어요.
foreach의 경우 배열의 요소는 인덱스 0에서 시작해 배열의 마지막 요소까지 순회돼요. foreach_reverse의 경우 배열 요소는 역순으로 방문돼요.
문자 배열에 대한 Foreach (Foreach over Arrays of Characters)
애그리게이트 표현식이 char, wchar, 또는 dchar의 정적 또는 동적 배열이면, 값 변수의 타입은 char, wchar, dchar 중 아무것이나 될 수 있어요. 이 방식으로 어떤 UTF 배열이든 어떤 UTF 타입으로 디코드될 수 있어요:
char[] a = "\xE2\x89\xA0".dup; // \u2260 encoded as 3 UTF-8 bytes
foreach (dchar c; a)
{
writefln("a[] = %x", c); // prints 'a[] = 2260'
}
dchar[] b = "\u2260"d.dup;
foreach (char c; b)
{
writef("%x, ", c); // prints 'e2, 89, a0, '
}
애그리게이트는 문자열 리터럴일 수 있는데, char, wchar, 또는 dchar 배열로 접근할 수 있어요:
foreach (char c; "ab")
{
writefln("'%s'", c);
}
foreach (wchar w; "xy")
{
writefln("'%s'", w);
}
이는 다음을 출력해요:
'a'
'b'
'x'
'y'
연관 배열에 대한 Foreach (Foreach over Associative Arrays)
애그리게이트 표현식이 연관 배열이면, 하나 또는 두 개의 변수를 선언할 수 있어요. 하나면 그 변수는 값(value)이라 불리며 배열의 각 값으로 하나씩 설정돼요. 변수의 타입이 제공되면 연관 배열 값 타입에서 암시적으로 변환될 수 있어야 해요.
// value type is int
int[string] userAges = ["john":30, "sue":32];
foreach (ref age; userAges)
{
age++;
}
assert(userAges == ["john":31, "sue":33]);
두 개의 변수가 선언되면 첫 번째는 인덱스(index), 두 번째는 값(value)이라 불려요. 인덱스 변수 타입(주어졌다면)은 연관 배열의 키 타입에서 암시적으로 변환될 수 있어야 해요. 인덱스 변수는 연관 배열의 각 값에 대응하는 키로 설정돼요.
// index type is string, value type is double
double[string] aa = ["pi":3.14, "e":2.72];
foreach (string s, double d; aa)
{
writefln("aa['%s'] = %g", s, d);
}
Implementation Defined: (구현 정의 사항) foreach에서 배열 요소가 순회되는 순서는 정해져 있지 않아요. 이것이 연관 배열에 대해
foreach_reverse가 불법인 이유예요.
인덱스나 값 변수가 ref로 선언되면, 주어진 어떤 타입도 연관 배열의 키 또는 값 타입과 각각 일치하거나, 그로부터 const-변환(const-convert)되어야 해요. ref 인덱스 변수는 (추론되든지) const여야 해요. 키는 변경되어서는 안 되기 때문이에요.
opApply가 있는 Struct/Class에 대한 Foreach
애그리게이트 표현식이 struct 또는 class 객체이면, foreach는 특수 opApply 멤버 함수로 정의되고, foreach_reverse 동작은 특수 opApplyReverse 멤버 함수로 정의돼요. 이 함수들은 각각 아래 시그니처를 가져야 해요:
OpApplyDeclaration:
int opApply ( scope int delegate ( OpApplyParameters ) dg ) ;
OpApplyParameters:
OpApplyParameter
OpApplyParameter, OpApplyParameters
OpApplyParameter:
ForeachTypeAttributesopt BasicType Declarator
여기서 dg의 각 OpApplyParameter는 ForeachStatement의 ForeachType과 일치해야 해요. 그렇지 않으면 ForeachStatement가 오류를 일으켜요. 어떤 ForeachTypeAttribute도 enum일 수 없어요.
ref 순회 변수를 지원하려면 delegate가 ref 매개변수를 받아야 해요:
struct S
{
int opApply(scope int delegate(ref uint n) dg);
}
void f(S s)
{
foreach (ref uint i; s)
i++;
}
위에서, i가 ref가 아닐 때도 opApply는 여전히 매칭되므로, ref delegate 매개변수를 사용함으로써 두 형태 모두 지원돼요.
opApply와 opApplyReverse 함수는 여러 개 있을 수 있어요 — dg의 각 매개변수를 ForeachStatement에 선언된 각 ForeachType에 매칭하여 하나가 선택돼요.
apply 함수의 본문은 자신이 모은 요소들을 순회하며, 각각을 연속 호출로 dg delegate에 전달해요. delegate 반환 값은 순회를 중단할지 여부를 결정해요:
- 결과가 0이 아니면, apply는 순회를 중단하고 그 값을 반환해야 해요.
- 결과가 0이면 순회가 계속되어야 해요. 순회할 요소가 더 이상 없으면 apply는 0을 반환해야 해요.
Undefined Behavior: (미정의 동작) delegate 호출의 결과가 아닌 0이 아닌 값을 반환하는 것.
delegate 호출의 결과는 ForeachStatement 본문이 일치하는 BreakStatement, ReturnStatement, 또는 ForeachStatement 밖에 있는 일치하는 라벨을 가진 GotoStatement를 실행하면 0이 아니게 돼요.
예를 들어, 두 요소의 컨테이너인 클래스를 생각해 봐요:
class Foo
{
uint[] array;
int opApply(scope int delegate(ref uint) dg)
{
foreach (e; array)
{
int result = dg(e);
if (result)
return result;
}
return 0;
}
}
void main()
{
import std.stdio;
Foo a = new Foo();
a.array = [73, 82, 2, 9];
foreach (uint u; a)
{
if (u < 5)
break;
writeln(u);
}
}
이는 다음을 출력해요:
73
82
dg 매개변수에 대한 scope 저장소 클래스는 delegate가 opApply 함수의 스코프를 벗어나지 않음을 의미해요 (예: dg를 전역 변수에 할당하는 것). dg가 탈출하지 않음을 정적으로 보장할 수 없으면, 스택 대신 힙에 클로저가 할당될 수 있어요.
Best Practices: (모범 사례) 가능하면 opApply 함수의 delegate 매개변수를
scope로 표시하세요.Important: (중요) opApply가 어떤 예외를 잡는다면, 그 예외가 opApply로 전달된 delegate에서 비롯된 것이 아닌지 확인하세요. 사용자는 foreach 본문에서 던진 예외가 루프를 종료하고 foreach 본문 밖으로 전파되기를 기대할 거예요.
템플릿 opApply (Template opApply)
opApply는 템플릿 함수일 수도 있는데, ForeachStatement에 기반하여 매개변수의 타입을 추론해요. 예를 들어:
struct S
{
import std.traits : ParameterTypeTuple; // introspection template
import std.stdio;
int opApply(Dg)(scope Dg dg)
if (ParameterTypeTuple!Dg.length == 2) // foreach with 2 parameters
{
writeln(2);
return 0;
}
int opApply(Dg)(scope Dg dg)
if (ParameterTypeTuple!Dg.length == 3) // foreach with 3 parameters
{
writeln(3);
return 0;
}
}
void main()
{
foreach (int a, int b; S()) { } // calls first opApply function
foreach (int a, int b, float c; S()) { } // calls second opApply function
}
Range가 있는 Struct/Class에 대한 Foreach
ForeachAggregate가 struct 또는 class 객체인데 opApply가 없으면, foreach 순회는 range 원시 함수(primitives)를 사용해요. 이 원시 함수들은 일련의 요소들을 생성해요.
ForeachAggregate에 대해 다음 속성과 메서드가 정의되어야 해요:
Foreach Range 속성:
| 속성 | 목적 |
|---|---|
.empty |
더 이상 요소가 없으면 true 반환 |
.front |
range의 가장 왼쪽 요소 반환 |
Foreach Range 메서드:
| 메서드 | 목적 |
|---|---|
.popFront() |
range의 왼쪽 가장자리를 오른쪽으로 하나 이동 |
순회에는 ForeachAggregate에서 초기화된 임시 변수가 사용돼요. 다음 문장:
foreach (e; range) { ... }
은 다음으로 변환돼요:
for (auto __r = range; !__r.empty; __r.popFront())
{
auto e = __r.front;
...
}
마찬가지로, opApplyReverse가 정의되지 않을 때 foreach_reverse에 대해 range 원시 함수가 사용돼요. ForeachAggregate에 대해 다음 속성과 메서드가 정의되어야 해요:
Foreach_reverse Range 속성:
| 속성 | 목적 |
|---|---|
.empty |
더 이상 요소가 없으면 true 반환 |
.back |
range의 가장 오른쪽 요소 반환 |
Foreach_reverse Range 메서드:
| 메서드 | 목적 |
|---|---|
.popBack() |
range의 오른쪽 가장자리를 왼쪽으로 하나 이동 |
다음 문장:
foreach_reverse (e; range) { ... }
은 다음으로 변환돼요:
for (auto __r = range; !__r.empty; __r.popBack())
{
auto e = __r.back;
...
}
front (또는 foreach_reverse의 경우 back)가 Ref Function이면 ForeachType 변수는 ref로 선언될 수 있어요:
struct R
{
int[] a;
// range primitives:
bool empty() => a.length == 0;
ref front() => a[0];
void popFront() { a = a[1..$]; }
}
R r = R([1, 2, 3]);
foreach (ref e; r)
e *= 2;
assert(r.a == [2, 4, 6]);
링크드 리스트 예시:
struct Node
{
int i;
Node* next;
}
// range
struct List
{
Node* node;
bool empty() { return node == null; }
ref int front() { return node.i; }
void popFront() { node = node.next; }
}
void main()
{
import std.stdio;
auto n = new Node(1, new Node(2, null));
auto r = List(n);
foreach (e; r)
{
writeln(e);
}
}
여러 요소 값 (Multiple Element Values)
front 속성이 변수의 수와 길이가 일치하는 값 시퀀스(value sequence)로 확장되는 타입을 반환하면, 여러 루프 변수가 허용돼요. 각 변수는 시퀀스의 해당 값에 할당돼요. 변수가 하나뿐이면 그것은 값 시퀀스가 돼요.
import std.stdio;
// simple version of std.typecons.Tuple
struct Tuple(Types...) // takes a TypeSeq
{
Types items; // ValueSeq
alias items this; // decay to a value sequence
}
string s = "hello";
struct TupleRange
{
size_t i;
auto front() => Tuple!(size_t, char)(i, s[i]);
bool empty() => i >= s.length;
void popFront() { i++; }
}
void main()
{
// Tuple destructuring
foreach (i, c; TupleRange())
{
writeln(i, ": ", c);
}
writeln();
// Tuple variable
foreach (tup; TupleRange())
{
writefln("(%s, %s)", tup[0], tup[1]);
}
}
delegate에 대한 Foreach (Foreach over Delegates)
ForeachAggregate가 delegate면, delegate의 타입 시그니처와 동작은 opApply와 같아요. 이는 여러 개의 서로 다른 이름 붙은 루핑 전략이 같은 class나 struct 안에 공존할 수 있게 해줘요.
delegate는 요소를 그때그때(on the fly) 생성할 수 있어요:
// Custom loop implementation, that iterates over powers of 2 with
// alternating sign. The foreach loop body is passed in dg.
int myLoop(scope int delegate(int) dg)
{
for (int z = 1; z < 128; z *= -2)
{
auto ret = dg(z);
// If the loop body contains a break, ret will be non-zero.
if (ret != 0)
return ret;
}
return 0;
}
// Append each value in the iteration to an array
int[] result;
foreach (x; &myLoop)
{
result ~= x;
}
assert(result == [1, -2, 4, -8, 16, -32, 64, -128]);
delegate에 대한 foreach_reverse는 오류예요.
시퀀스에 대한 Foreach (Foreach over Sequences)
애그리게이트 표현식이 시퀀스(sequence)면, 루프 본문은 각 요소마다 한 번씩 정적으로 확장돼요. 이는 시퀀스에 대한 StaticForeachStatement와 유사하지만, 각 확장마다 별도의 스코프가 있어요.
하나 또는 두 개의 순회 심볼을 선언할 수 있어요. 하나면, 시퀀스의 각 요소를 차례로 나타내는 요소 심볼이에요.
- 시퀀스가 TypeSeq면 요소 심볼은 차례로 각 타입으로 설정돼요.
- 시퀀스가 ValueSeq면 요소 심볼은 차례로 각 값으로 설정돼요. 요소 심볼의 타입이 주어지면 모든 시퀀스 요소의 타입과 호환되어야 해요. 타입이 주어지지 않으면 요소 심볼의 타입은 각 시퀀스 요소의 타입과 일치하며, 요소 사이에서 바뀔 수 있어요.
두 개의 심볼이 선언되면 첫 번째는 인덱스(index), 두 번째는 요소(element)예요. 인덱스 심볼은 (지정된다면) int, uint, long, ulong 타입이어야 하고, ref일 수 없으며, 각 시퀀스 요소의 인덱스로 설정돼요.
예제:
import std.meta : AliasSeq;
void main()
{
alias Seq = AliasSeq!(int, "literal", main);
foreach (int i, sym; Seq)
{
pragma(msg, i, ": ", sym.stringof);
}
}
출력:
0: int
1: "literal"
2: main()
예제:
void fun()
{
import std.meta : AliasSeq;
alias values = AliasSeq!(7.4, "hi", [2,5]);
foreach (sym; values)
{
pragma(msg, sym, " has type ", typeof(sym));
}
}
출력:
7.4 has type double
hi has type string
[2, 5] has type int[]
심볼 종류 (Symbol Kinds)
각 ForeachType 선언(alias/enum 없이)은 다음을 생성하려 시도해요:
- 컴파일 타임에 알려진 값에 대한 컴파일 타임 심볼
- 컴파일 타임에 알려지지 않은 값의 요소에 대한 런타임 변수
- 값이 없는 요소(예: 타입)에 대한 심볼릭 앨리어스
ForeachType에서 alias나 enum을 지정할 수 있어요:
alias저장소 클래스는 심볼릭 앨리어스를 생성해요.enum저장소 클래스는 컴파일 타임 상수를 생성해요.
void main()
{
import std.meta : AliasSeq;
int i = 1;
// without `alias`, `a` would be a separate variable initialized from `i`
foreach (alias a; AliasSeq!(i, i))
{
a++;
}
assert(i == 3);
foreach (e; AliasSeq!(i, 1))
{
static if (__traits(compiles, &e))
assert(e == 3); // first e is a runtime variable
else
static assert(e == 1); // second e is a compile-time constant
}
}
Foreach Ref 매개변수 (Foreach Ref Parameters)
ref를 사용해 ForeachAggregate의 요소를 수정할 수 있어요. 이는 lvalue 요소를 노출하는 컨테이너와 lvalue 시퀀스에서 작동해요.
uint[2] a = [7, 8];
foreach (ref u; a)
{
u++;
}
foreach (u; a)
{
writeln(u);
}
이것은 다음을 출력해요:
8
9
ref는 배열 인덱스 변수에는 적용될 수 없어요.
Foreach 제한 (Foreach Restrictions)
foreach가 요소를 순회하는 동안 애그리게이트 자체는 크기 변경(resize), 재할당(reallocate), 해제(free), 재할당(reassign) 또는 소멸(destruct)되어서는 안 돼요.
int[] a = [1, 2, 3];
auto fun = { a ~= 4; };
foreach (int v; a)
{
// resizing is unspecified!
fun();
a ~= 4;
a.length += 10;
// reallocating is unspecified!
a.reserve(10);
// reassigning is unspecified!
a = null;
a = [5, 6];
}
a ~= 4; // OK
a = null; // OK
auto aa = [1: 1, 2: 2];
foreach (v; aa)
{
aa[3] = 3; // unspecified resize
aa.rehash; // unspecified reallocation
aa = [4: 4]; // unspecified reassign
}
aa[3] = 3; // OK
aa = null; // OK
Note: foreach 중에 동적 또는 연관 배열의 크기를 변경하거나 재할당하는 것은 여전히
@safe예요.
Foreach 변수 언패킹 (Unpacking Foreach Variables)
변수 선언 언패킹과 유사하게, AggregateForeach는 ForeachType 선언 언패킹을 지원해요. 튜플 패턴에 대해 auto 저장소 클래스가 암시된다고 간주되며, 덮어쓰지 않는 한 그래요.
ForeachTuplePattern:
( ForeachTupleComponents )
ForeachTupleComponents:
ForeachTupleComponent ,
ForeachTupleComponent , ForeachTupleComponent
ForeachTupleComponent , ForeachTupleComponents
ForeachTupleComponent:
ForeachTypeAttributesopt Identifier
ForeachTypeAttributesopt BasicType TypeSuffixesopt Identifier
ForeachTypeAttributesopt ForeachTuplePattern
다음 foreach 문장들은 동등해요:
import std.typecons : tuple;
auto arr = [tuple(1, "2"), tuple(3, "4"), tuple(5, "6")];
foreach ((x, y); arr)
{
writeln(x, " ", y); // "1 2\n3 4\n5 6\n"
}
foreach ((int x, string y); arr)
{
writeln(x, " ", y);// "1 2\n3 4\n5 6\n"
}
인덱스 변수는 언패킹된 요소 변수와 함께 선언될 수 있어요 (ForeachAggregate가 지원할 때):
import std.typecons : tuple;
auto arr = [tuple(1, 2), tuple(3, 4)];
// declare index for array & unpack tuple elements
foreach (i, (x, y); arr)
{
assert(x == 2 * i + 1);
assert(y == 2 * i + 2);
}
중첩 튜플은 언패킹될 수 있어요. 아래에서 enumerate는 요소 타입 Tuple!(size_t, Tuple!(int, int))의 range를 반환해요. 바깥 튜플은 암시적으로 풀려요:
import std.range : enumerate;
import std.typecons : tuple;
auto arr = [tuple(1, 2), tuple(3, 4)];
// unpack a range of nested tuples
foreach (i, (j, k); enumerate(arr))
{
writeln(i," ",j," ",k); // "0 1 2\n1 3 4\n"
}
인덱스 변수도 언패킹될 수 있어요:
import std.typecons : tuple;
auto aa = [tuple(1, 2): "hi", tuple(3, 4): "bye"];
// unpack AA tuple keys
foreach ((a, b), s; aa)
{
writeln(a, b, s); // "12hi\n34bye\n"
}
일반 ForeachTypeAttribute 저장소 클래스가 지원되고, 튜플 컴포넌트 변수에 적용될 수 있어요:
import std.typecons : tuple;
auto arr = [tuple(1, 2), tuple(3, 4)];
foreach ((ref x, y); arr)
{
x = 2 * y;
}
assert(arr == [tuple(4, 2), tuple(8, 4)]);
foreach (const (x, y); arr)
{
static assert(is(typeof(x) == const(int)));
static assert(is(typeof(y) == const(int)));
assert(x == 2 * y);
}
언패킹은 StaticForeach와 함께 사용될 수 있어요:
import std.typecons : tuple;
static foreach ((a, b); [tuple(1, 2), tuple(3, 4)])
{
pragma(msg, a, b); // "12\n34\n"
}
언패킹은 opApply와 함께 사용될 수도 있어요.
Foreach Range 문장 (Foreach Range Statement)
foreach range 문장은 지정된 범위에 대해 반복해요.
RangeForeach:
Foreach ( ForeachType ; LwrExpression .. UprExpression )
LwrExpression:
Expression
UprExpression:
Expression
ForeachRangeStatement:
RangeForeach ScopeStatement
ForeachType은 명시적 타입 또는 LwrExpression과 UprExpression에서 추론된 공통 타입으로 변수를 선언해요. 그런 다음 ScopeStatement가 n번 실행되는데, n은 UprExpression - LwrExpression의 결과예요. UprExpression이 LwrExpression보다 작거나 같으면 ScopeStatement는 실행되지 않아요.
Foreach가 foreach면 변수는 LwrExpression으로 설정된 다음 각 반복 끝에서 증가돼요. Foreach가 foreach_reverse면 변수는 UprExpression으로 설정된 다음 각 반복 전에 감소돼요. LwrExpression과 UprExpression은 ScopeStatement가 몇 번 실행되든 각각 정확히 한 번만 평가돼요.
import std.stdio;
int foo()
{
write("foo");
return 10;
}
void main()
{
foreach (i; 0 .. foo())
{
write(i);
}
}
출력:
foo0123456789
Switch 문장 (Switch Statement)
switch 문장은 switch 표현식의 값에 따라 일련의 case 문장 중 하나로 실행을 옮겨요.
SwitchStatement:
switch ( IfCondition ) ScopeStatement
CaseStatement:
case ArgumentList : ScopeStatementListopt
DefaultStatement:
default : ScopeStatementListopt
ScopeStatementList:
StatementListNoCaseNoDefault
StatementListNoCaseNoDefault:
StatementNoCaseNoDefault
StatementNoCaseNoDefault StatementListNoCaseNoDefault
StatementNoCaseNoDefault:
EmptyStatement
NonEmptyStatementNoCaseNoDefault
ScopeBlockStatement
IfCondition의 Expression이 평가돼요.
- Expression의 타입이 enum이면 (재귀적으로) 그 EnumBaseType으로 변환돼요.
- 그런 다음 타입이 정수형이면 Expression은 정수 승격(integer promotions)을 거쳐요.
- 그런 다음 Expression의 타입은 정수형이거나 문자열이어야 해요.
Identifier = 접두사가 제공되면 그 이름으로 변수가 선언되고, Expression의 값과 타입으로 초기화돼요. 그 스코프는 초기화된 시점부터 ScopeStatement 끝까지 확장돼요. TypeCtors 및/또는 특정 타입이 Identifier에 제공되면, 그것들이 Expression 값으로부터의 암시적 변환으로 초기화되는 변수 선언에 사용돼요.
결과 값은 각 case 표현식과 비교돼요. 일치하면 해당 case 문장으로 옮겨져요. switch 문장은 정확히 하나의 DefaultStatement를 가져야 해요. 어떤 case 표현식도 일치하지 않으면 제어가 default 문장으로 옮겨져요.
Rationale: (근거) 이는 가능한 모든 case가 의도적으로 처리됨을 명확하게 해요.
Case 및 Default 문장 (Case and Default Statements)
CaseStatement의 ArgumentList는 쉼표로 구분된 표현식 목록이에요. 각 표현식은 컴파일 타임 값이나 배열, 또는 정수형의 런타임 초기화된 const 또는 immutable 변수로 평가되어야 해요. 각 표현식은 switch Expression의 타입으로 암시적으로 변환 가능해야 해요.
컴파일 타임 case 값들은 모두 구별되어야 해요. const 또는 immutable 런타임 case 변수들은 모두 다른 이름을 가져야 해요. 두 case 표현식이 값을 공유하면 그 값을 가진 첫 번째 case 문장이 제어를 받아요.
ScopeStatementList는 새로운 스코프를 도입해요. 일치하는 break 문장은 switch의 ScopeStatement를 빠져나가요.
foreach (i; 2 .. 10)
{
bool prime;
switch (i)
{
case 2, 3, 5, 7:
prime = true;
break;
default:
prime = false;
}
writeln(i, ": ", prime);
}
switch와 연관된 case 문장과 default 문장은 블록 문장 안에 중첩될 수 있어요; 반드시 바깥쪽 블록에 있을 필요는 없어요. 예를 들어, 다음은 허용돼요:
switch (i)
{
case 1:
{
case 2:
}
i++;
break;
default:
}
Implementation Note: (구현 참고) 컴파일러의 코드 생성기는 case 문장이 사용 빈도순으로 정렬되어 있다고 가정할 수 있어요 — 가장 빈번한 것이 먼저, 가장 덜 빈번한 것이 마지막. 프로그램 정확성과는 무관하지만, 성능 관점에서는 관련이 있어요.
Case Range 문장 (Case Range Statement)
CaseRangeStatement:
case FirstExp : .. case LastExp : ScopeStatementListopt
FirstExp:
AssignExpression
LastExp:
AssignExpression
CaseRangeStatement는 FirstExp부터 LastExp까지(양끝 포함) 일련의 case 문장을 나열하는 축약형이에요.
case 1: .. case 3:
위는 다음과 동등해요:
case 1, 2, 3:
암시적 폴 스루 없음 (No Implicit Fall-Through)
ScopeStatementList는 비어 있거나, switch에서 마지막이거나, 다음 중 하나로 끝나야 해요:
- ContinueStatement, BreakStatement, ReturnStatement, GotoStatement
assert(0)표현식 평가noreturn타입의 표현식 평가
위 마지막 2가지 경우의 간단한 형태는 컴파일러가 인식할 수 있지만, 전부는 아니에요.
uint i;
string message;
switch (i)
{
case 1:
message = "one";
// ERROR: implicit fall-through
case 2:
// valid: the body is empty
default:
message = "2 or more";
// valid: no more case statements
}
goto case;는 다음 CaseStatement로의 명시적 폴 스루에 사용될 수 있어요:
string message;
foreach (i; 1..5)
{
switch (i)
{
default: // valid: ends with `noreturn` expression evaluation
throw new Exception("unknown number");
case 3: // valid: ends with 'break' (break out of the 'switch' only)
message ~= "three";
break;
case 4: // valid: ends with 'continue' (continue the enclosing loop)
message ~= "four";
continue; // don't append a comma
case 1: // valid: ends with 'goto' (explicit fall-through to next case.)
message ~= ">";
goto case;
case 2: // valid: this is the last case in the switch statement.
message ~= "one or two";
}
message ~= ", ";
}
writeln(message);
goto는 특정 case나 default case 문장으로 점프하는 것도 지원해요.
문자열 Switch (String Switch)
문자열은 switch 표현식에 사용될 수 있어요. char, wchar 또는 dchar의 정적 또는 동적 배열이 지원돼요. 예를 들어:
string name = "fred";
switch (name)
{
case "fred", "sally":
writeln("Hi ", name);
break;
default:
writeln("Not recognized");
}
Rationale: (근거) 명령줄 스위치 처리 같은 애플리케이션에서, 이는 훨씬 더 간단하고 명확하며 오류에 덜 취약한 코드로 이어질 수 있어요.
Final Switch 문장 (Final Switch Statement)
FinalSwitchStatement:
final SwitchStatement
final switch 문장은 switch 문장과 같지만, 다음을 제외해요:
- DefaultStatement가 허용되지 않아요.
- CaseRangeStatement가 허용되지 않아요.
- IfCondition의 Expression이 enum 타입이면 모든 enum 멤버가 CaseStatement에 나타나야 해요.
- case 표현식은 런타임 초기화된 값으로 평가될 수 없어요.
enum E { unknown, yes, no }
E e;
with (E) final switch (e)
{
case unknown:
writeln("need answer");
break;
case yes, no:
writeln("got answer");
}
Implementation Defined: (구현 정의 사항) Expression 값이 어떤 CaseRangeStatement와도 일치하지 않을 때, 그것이 컴파일 타임에 진단되는지 런타임에 진단되는지.
Continue 문장 (Continue Statement)
ContinueStatement:
continue Identifieropt ;
continue는 자신의 가장 안쪽 둘러싼 루프 문장의 현재 반복을 중단하고 다음 반복을 시작해요. 둘러싼 루프가 for 문장이면 그 Increment 절이 실행돼요.
string[] words = ["OK", "just", "longer", "words", "now"];
foreach (w; words)
{
if (w.length < 4)
continue; // skip writeln
writeln(w);
}
출력:
just
longer
words
continue 뒤에 Identifier가 오면, Identifier는 둘러싼 루프 문장의 라벨이어야 하고, 그 루프의 다음 반복이 실행돼요. 그런 문장이 없으면 오류예요.
outer:
foreach (item; list)
{
// try 3 times
foreach (i; 0 .. 3)
{
if (item.buy())
continue outer; // skip to next item
log("attempt failed");
}
}
개입하는 finally 절들은 실행되고, 개입하는 동기화 객체들은 해제돼요.
Note: finally 절이 finally 밖으로
throw를 실행하면, continue 대상에 절대 도달하지 않아요.
Break 문장 (Break Statement)
BreakStatement:
break Identifieropt ;
break는 가장 안쪽 둘러싼 루프나 switch 문장을 빠져나가고, 그 뒤따르는 문장에서 실행을 재개해요.
const n = 55;
// find the smallest factor of n
foreach (i; 2 .. n)
{
writeln("Trying: ", i);
if (n % i == 0)
{
writeln("smallest factor is ", i);
break; // stop looking
}
}
writeln("finished");
출력:
Trying: 2
Trying: 3
Trying: 4
Trying: 5
smallest factor is 5
finished
break 뒤에 Identifier가 오면, Identifier는 둘러싼 루프나 switch 문장의 라벨이어야 하고, 그 문장이 빠져나가져요. 그런 문장이 없으면 오류예요.
// display messages cyclically until the shop is closed
outer:
while (true)
{
foreach (msg; messages)
{
if (shop.isClosed())
break outer; // end the while loop
display(msg);
}
}
display("opens at 9am");
개입하는 finally 절들은 실행되고, 개입하는 동기화 객체들은 해제돼요.
Note: finally 절이 finally 밖으로
throw를 실행하면, break 대상에 절대 도달하지 않아요.
Return 문장 (Return Statement)
ReturnStatement:
return Expressionopt ;
return은 현재 함수를 빠져나가고 그 반환 값을 제공해요. 함수가 void가 아닌 반환 타입을 지정하면 Expression이 필요해요. Expression은 함수 반환 타입으로 암시적으로 변환돼요. 함수가 void 반환 타입을 지정하면 void 타입의 Expression이 허용돼요. Expression은 평가되지만 아무것도 반환되지 않아요. 이는 제네릭 프로그래밍에서 유용해요.
함수가 실제로 반환하기 전에, scope 저장 기간(storage duration)을 가진 객체들은 파괴되고, 둘러싼 finally 절들이 실행되며, scope(exit) 문장들이 실행되고, scope(success) 문장들이 실행되고, 둘러싼 동기화 객체들이 해제돼요.
둘러싼 finally 절이 finally 밖으로 나가는 return, goto, throw를 하면 함수는 반환하지 않아요. out 사후조건(postcondition)이 있으면, 그 사후조건은 Expression이 평가된 후, 함수가 실제로 반환하기 전에 실행돼요.
int foo(int x)
{
return x + 3;
}
Goto 문장 (Goto Statement)
GotoStatement:
goto Identifier ;
goto default ;
goto case ;
goto case Expression ;
goto는 Identifier로 라벨이 붙은 문장으로 이동해요.
int x = 1;
if (x)
goto L1;
x = 3;
L1:
assert(x == 1);
두 번째 형태인 goto default;는 둘러싼 SwitchStatement의 가장 안쪽 DefaultStatement로 이동해요. 세 번째 형태인 goto case;는 가장 안쪽 둘러싼 SwitchStatement의 다음 CaseStatement로 이동해요. 네 번째 형태인 goto case Expression;은 일치하는 Expression이 있는 가장 안쪽 둘러싼 SwitchStatement의 CaseStatement로 이동해요.
int x = 5;
switch (x)
{
case 3:
writeln("case 3");
goto case;
case 4:
writeln("case 4");
goto default;
case 5:
writeln("case 5");
goto case 4;
default:
writeln("default");
break;
}
개입하는 finally 절들이 실행되고, 개입하는 동기화 mutex들이 해제돼요. GotoStatement가 그것이 포함된 스코프 안의 초기화들을 건너뛰는 데 사용되는 것은 불법이에요.
Best Practices: (모범 사례)
goto Identifier;형태보다는 더 상위 수준의 제어 흐름 구성이나 라벨이 붙은 BreakStatement/ContinueStatement를 선호하세요.
With 문장 (With Statement)
with 문장은 같은 객체에 대한 반복적 참조를 단순화하는 방법이에요.
WithStatement:
with ( IfCondition ) ScopeStatement
with ( Symbol ) ScopeStatement
IfCondition 형태에서 Expression은 사용자 정의 타입 인스턴스 또는 사용자 정의 타입 인스턴스에 대한 포인터여야 해요. ScopeStatement 안에서 참조된 표현식/심볼이 식별자 심볼에 대해 먼저 검색돼요.
enum E { A, B }
void test(E e)
{
with (e) // affects the switch statement
switch (e)
{
case A: // no need for E.A
case B:
default:
break;
}
}
아래에서, ident가 expr 타입의 멤버라면, WithStatement:
with (expr)
{
...
ident;
}
은 의미적으로 다음와 동등해요:
{
auto ref tmp = expr;
...
tmp.ident;
}
Expression은 정확히 한 번만 평가되고 복사되지 않는다는 점에 주의하세요. with 문장은 this나 super가 참조하는 것을 바꾸지 않아요.
Symbol 인자의 경우, 심볼을 찾을 때 해당 스코프가 검색돼요. 예를 들어:
struct Foo
{
alias Y = int;
}
...
Y y; // error, Y undefined
with (Foo)
{
Y y; // same as Foo.Y y;
}
같은 식별자를 가진 지역 심볼을 가리는(shadow) 객체 심볼을 with로 사용하는 것은 허용되지 않아요. 이는 객체 선언에 새 멤버가 추가될 때 with 문장이 우발적으로 깨질 위험을 줄이기 위함이에요.
struct S
{
float x;
}
void main()
{
int x;
S s;
with (s)
{
x++; // error, shadows the int x declaration
}
}
중첩 WithStatement에서 가장 안쪽 스코프가 우선해요. 심볼이 가장 안쪽 스코프에서 해결되지 않으면, 해석이 스코프 계층을 점진적으로 위로 전달돼요.
import std.stdio;
struct Foo
{
void f() { writeln("Foo.f"); }
}
struct Bar
{
void f() { writeln("Bar.f"); }
}
struct Baz
{
// f() is not implemented
}
void f()
{
writeln("f");
}
void main()
{
Foo foo;
Bar bar;
Baz baz;
f(); // prints "f"
with(foo)
{
f(); // prints "Foo.f"
with(bar)
{
f(); // prints "Bar.f"
with(baz)
{
f(); // prints "Bar.f". `Baz` does not implement `f()` so
// resolution is forwarded to `with(bar)`'s scope
}
}
with(baz)
{
f(); // prints "Foo.f". `Baz` does not implement `f()` so
// resolution is forwarded to `with(foo)`'s scope
}
}
with(baz)
{
f(); // prints "f". `Baz` does not implement `f()` so
// resolution is forwarded to `main`'s scope. `f()` is
// not implemented in `main`'s scope, so resolution is
// subsequently forward to module scope.
}
}
Synchronized 문장 (Synchronized Statement)
synchronized 문장은 문장을 mutex 잠금과 잠금 해제로 감싸, 여러 스레드 간의 접근을 동기화해요.
SynchronizedStatement:
synchronized ScopeStatement
synchronized ( Expression ) ScopeStatement
Expression이 없는 synchronized 문장은 mutex를 잠그어 한 번에 하나의 스레드만 ScopeStatement를 실행하도록 허용해요. synchronized 문장 하나마다 전역 mutex 하나가 생성돼요. 서로 다른 synchronized 문장들은 서로 다른 전역 mutex를 가져요.
Expression이 있으면 Object 또는 인터페이스의 인스턴스로 평가되어야 해요. 인터페이스인 경우 그 인터페이스를 구현한 Object 인스턴스로 캐스팅돼요. 사용된 mutex는 그 Object 인스턴스에 특정하며, 그 인스턴스를 참조하는 모든 synchronized 문장에 의해 공유돼요. synchronized 문장에 도달했을 때 객체의 mutex가 이미 잠겨 있으면, 다른 코드가 그 mutex를 잠금 해제할 때까지 모든 스레드가 차단돼요.
void work();
void f(Object o)
{
synchronized (o) work();
}
void g(Object o)
{
synchronized (o) work();
}
f와 g가 서로 다른 스레드에서 호출되지만 같은 인자를 받으면, work 호출은 동시에 실행될 수 없어요. synchronized 문장의 (o) 부분이 한쪽 또는 양쪽 함수에서 제거되면, 둘 다의 work 호출이 동시에 실행될 수 있어요. 서로 다른 mutex로 보호되기 때문이에요.
동기화는 ScopeStatement가 예외, goto, 또는 return으로 종료돼도 해제돼요. 이는 표준 임계 구역(critical section)을 구현해요.
synchronized 문장은 재귀 잠금(recursive locking)을 지원해요. 즉 synchronized로 감싼 함수가 재귀적으로 자신을 호출하는 것이 허용되고, 동작은 예상대로예요: mutex는 재귀 횟수만큼 잠기고 잠금 해제돼요.
Try 문장 (Try Statement)
예외 처리는 try-catch-finally 문장으로 처리돼요.
TryStatement:
try ScopeStatement Catches
try ScopeStatement Catches FinallyStatement
try ScopeStatement FinallyStatement
Catches:
Catch
Catch Catches
Catch:
catch ( CatchParameter ) NoScopeNonEmptyStatement
CatchParameter:
BasicType Identifieropt
FinallyStatement:
finally NoScopeNonEmptyStatement
CatchParameter는 타입 T의 변수 v를 선언하는데, T는 Throwable이거나 Throwable에서 파생된 것이에요. T가 throw 표현식과 같은 타입이거나 그 기본 클래스면 v는 throw 표현식으로 초기화돼요. 예외 객체가 타입 T이거나 T에서 파생된 것이면 catch 절이 실행돼요. 타입 T만 주어지고 변수 v가 없어도 catch 절은 여전히 실행돼요.
어떤 CatchParameter 타입 T1이 이후의 타입 T2를 가진 Catch를 가리는(숨기는) 것은 오류예요. 즉 T1이 T2와 같은 타입이거나 그 기본 클래스면 오류예요.
FinallyStatement는 try ScopeStatement가 goto, break, continue, return, 예외, 또는 폴 스루로 종료되는지와 관계없이 항상 실행돼요. FinallyStatement에서 예외가 발생했고 그것이 원래 예외가 잡히기 전에 잡히지 않으면, Throwable의 next 멤버를 통해 이전 예외에 연결(chained)돼요. 대부분의 다른 프로그래밍 언어와 달리, 새 예외는 원래 예외를 대체하지 않는다는 점에 주의하세요. 대신 이후의 예외들은 첫 번째 예외가 일으킨 '부수적 피해(collateral damage)'로 간주돼요. 원래 예외가 잡혀야 하며, 이는 전체 체인을 포착하게 돼요.
Error 클래스에서 파생된 throw 객체들은 다르게 취급돼요. 그것들은 정상적인 연결 메커니즘을 우회해서, 체인은 첫 번째 Error를 잡아야만 잡힐 수 있어요. 이후 예외 목록에 더해, Error는 우회가 발생했다면 원래 예외(체인의 머리)를 가리키는 포인터도 포함해, 전체 예외 기록이 유지되도록 해요.
import std.stdio;
int main()
{
try
{
try
{
throw new Exception("first");
}
finally
{
writeln("finally");
throw new Exception("second");
}
}
catch (Exception e)
{
writefln("catch %s", e.msg);
}
writeln("done");
return 0;
}
출력:
finally
catch first
done
FinallyStatement는 goto, break, continue, return으로 빠져나갈 수 없고, goto로 들어갈 수도 없어요. FinallyStatement는 어떤 Catches도 포함할 수 없어요. 이 제한은 향후 버전에서 완화될 수 있어요.
Scope Guard 문장 (Scope Guard Statement)
ScopeGuardStatement:
scope ( exit ) NonEmptyOrScopeBlockStatement
scope ( success ) NonEmptyOrScopeBlockStatement
scope ( failure ) NonEmptyOrScopeBlockStatement
NonEmptyOrScopeBlockStatement가 실행되면, 그것은 ScopeGuardStatement가 나타난 지점이 아니라 현재 스코프가 닫힐 때 발생해요:
scope(exit)는 스코프가 정상적으로 종료되거나 예외 언와인딩(unwinding)으로 종료될 때 NonEmptyOrScopeBlockStatement를 실행해요.scope(failure)는 스코프가 예외 언와인딩으로 종료될 때만 NonEmptyOrScopeBlockStatement를 실행해요.scope(success)는 스코프가 정상적으로 종료될 때만 NonEmptyOrScopeBlockStatement를 실행해요.
스코프에 여러 ScopeGuardStatement가 있으면 그것들은 나타난 어휘적 순서의 역순으로 실행돼요. 스코프가 닫힐 때 파괴될 scope 인스턴스가 있으면, 그 파괴는 ScopeGuardStatement들과 나타난 어휘적 순서의 역순으로 인터리브돼요.
write("1");
{
write("2");
scope(exit) write("3");
scope(exit) write("4");
write("5");
}
writeln();
출력:
12543
{
scope(exit) write("1");
scope(success) write("2");
scope(exit) write("3");
scope(success) write("4");
}
writeln();
출력:
4321
struct Foo
{
this(string s) { write(s); }
~this() { write("1"); }
}
try
{
scope(exit) write("2");
scope(success) write("3");
Foo f = Foo("0");
scope(failure) write("4");
throw new Exception("msg");
scope(exit) write("5");
scope(success) write("6");
scope(failure) write("7");
}
catch (Exception e)
{
}
writeln();
출력:
0412
scope(exit) 또는 scope(success) 문장은 throw, goto, break, continue, return으로 빠져나갈 수 없고, goto로 들어갈 수도 없어요. scope(failure) 문장은 return으로 빠져나갈 수 없어요.
import std.stdio;
int foo()
{
scope(exit) writeln("Inside foo()");
return bar();
}
int bar()
{
writeln("Inside bar()");
return 0;
}
int main()
{
foo();
return 0;
}
출력:
Inside bar()
Inside foo()
C++ 클래스 객체 잡기 (Catching C++ Class Objects)
많은 플랫폼에서 C++ 클래스 객체를 잡는 것이 지원돼요. C++ 객체와 D 객체를 같은 TryStatement에서 둘 다 잡는 것은 할 수 없어요. Catch에서 나올 때, C++ 객체의 소멸자들이 실행되고 그에 사용된 저장 공간이 회수돼요. C++ 객체는 @safe 코드에서 잡힐 수 없어요.
Asm 문장 (Asm Statement)
인라인 어셈블러는 asm 문장으로 지원돼요:
AsmStatement:
asm FunctionAttributesopt { AsmInstructionListopt }
AsmInstructionList:
AsmInstruction ;
AsmInstruction ; AsmInstructionList
asm 문장은 어셈블리 언어 명령어를 직접 사용할 수 있게 해줘요. 이는 외부 어셈블러에 의존하지 않고 특수 CPU 기능에 직접 접근하기 쉽게 해줘요. D 컴파일러가 함수 호출 규칙, 스택 설정 등을 처리해요.
명령어의 형식은 당연히 대상 CPU의 네이티브 명령어 집합에 크게 의존하며, 따라서 구현 정의예요. 하지만 형식은 다음 규칙들을 따를 거예요:
- D 언어가 사용하는 것과 같은 토큰을 사용해야 해요.
- 주석 형식은 D 언어 주석과 일치해야 해요.
- Asm 명령어는 줄 끝이 아니라
;로 종료돼요.
이 규칙들은 D 소스 코드가 구문(syntactic) 또는 의미(semantic) 분석과 독립적으로 토큰화될 수 있게 보장하기 위해 존재해요.
예를 들어, Intel Pentium의 경우:
int x = 3;
asm
{
mov EAX,x; // load x and put it in register EAX
}
인라인 어셈블러는 하드웨어에 직접 접근하는 데 사용될 수 있어요:
int gethardware()
{
asm
{
mov EAX, dword ptr 0x1234;
}
}
D를 C로 변환하는 번역기 같은 일부 D 구현에서는 인라인 어셈블러가 의미가 없으며, 구현될 필요가 없어요. version 문장을 사용해 이를 처리할 수 있어요:
version (D_InlineAsm_X86)
{
asm
{
...
}
}
else
{
/* ... some workaround ... */
}
의미적으로 연속된 AsmStatement 사이에 컴파일러가 다른 명령어(예: 레지스터 저장이나 복원)를 삽입해서는 안 돼요.
Pragma 문장 (Pragma Statement)
PragmaStatement를 참고하세요.
Mixin 문장 (Mixin Statement)
MixinStatement:
mixin ( ArgumentList ) ;
ArgumentList의 각 AssignExpression은 컴파일 타임에 평가되고, 결과는 문자열로 표현될 수 있어야 해요. 결과 문자열들은 연결되어 하나의 문자열을 이뤄요. 문자열의 텍스트 내용은 유효한 StatementList로 컴파일 가능해야 하며 그렇게 컴파일돼요.
import std.stdio;
void main()
{
int i = 0;
mixin("
int x = 3;
for (; i < 3; i++)
writeln(x + i, i);
"); // ok
enum s = "int y;";
mixin(s); // ok
y = 4; // ok, mixin declared y
string t = "y = 3;";
//mixin(t); // error, t is not evaluatable at compile time
//mixin("y =") 4; // error, string must be complete statement
mixin("y =" ~ "4;"); // ok
mixin("y =", 2+2, ";"); // ok
}