Statements
Statements (문장)
이 장에서는 D 언어에서 프로그램의 실행 흐름을 만드는 '문장(Statement)' 전부를 살펴봅니다. 함수의 본문은 문장들의 나열이고, 실행은 어휘 순서대로 일어나요. 어떤 문장이 값을 갖는 게 아니라 '효과'를 위해 실행된다는 점, 그리고 문법상 문장과 선언(Declaration) 사이의 모호함이 있을 때는 선언이 우선한다는 규칙부터 잡고 시작할게요.
본문
함수 안에서의 실행 순서는 문장(Statements)에 의해 제어됩니다. 함수의 본문은 0개 이상의 문장들로 이루어진 연속열입니다. 실행은 어휘 순서(lexical order)로 일어나지만, 어떤 문장은 지연된 효과(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
문장(Statements)과 선언(Declarations) 사이의 문법적 모호함은, 선언이 우선하는 방식으로 해결됩니다. 그런 문장을 괄호로 감싸면 문장(Statement)으로 해석되도록 명확히 구분할 수 있어요.
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 (레이블 문장)
문장에는 레이블을 붙일 수 있어요. 레이블은 문장 앞에 오는 식별자(identifier)입니다.
LabeledStatement:
Identifier :
Identifier : Statement
빈 문장을 포함해 어떤 문장이든 레이블을 붙일 수 있고, 그렇게 해서 goto 문장의 대상이 될 수 있습니다. 레이블이 붙은 문장은 break나 continue 문장의 대상이 될 수도 있어요.
레이블 뒤에 문장이 오지 않고 블록의 끝에 그냥 나타날 수도 있습니다.
레이블은 선언, 변수, 타입 등과는 독립적인 이름 공간(name space)에 있습니다. 그럼에도 레이블은 지역 선언과 같은 이름을 가질 수는 없어요. 레이블의 이름 공간은 레이블이 나타나는 함수의 본문입니다. 레이블 이름 공간은 중첩되지 않습니다. 즉 블록 문장 안의 레이블이라도 그 블록 바깥에서 접근할 수 있어요.
어느 한 함수의 레이블을 다른 함수에서 참조할 수는 없습니다.
Block Statement (블록 문장)
BlockStatement:
{ }
{ StatementList }
StatementList:
Statement
Statement StatementList
블록 문장은 { }로 감싼 문장들의 연속열입니다. 문장들은 어휘 순서로 실행되며, 블록의 끝에 도달하거나 어떤 문장이 제어 흐름을 다른 곳으로 옮길 때까지 계속됩니다.
Expression Statement (식 문장)
ExpressionStatement:
Expression ;
식(Expression)이 평가됩니다.
(x + x)처럼 아무 효과도 없는 식은, void로 캐스팅하지 않는 한 식 문장으로 쓰는 것이 불법입니다.
int x;
x++; // ok
x; // illegal
1+1; // illegal
cast(void)(x + x); // ok
Declaration Statement (선언 문장)
선언 문장은 변수를 정의하고, 타입·템플릿·함수·임포트(import)·조건부(conditional)·정적 foreach(static foreach)·정적 assert(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 Statement (if 문장)
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)으로 변환됩니다. 불리언이 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++;
대신 두 번째 IfStatement를 감싸기 위해 BlockStatement를 사용하세요.
Rationale (이유): 매달린(dangling) else가 어느 IfStatement와 관련되는지 모호할 수 있습니다. 버그를 유발하기 쉬운 문법을 불법으로 만들면 버그의 가능성이 줄어듭니다.
Boolean Conversion (불리언 변환)
조건에서 지원되는 타입 인스턴스들은 다음과 같습니다:
- 산술 타입(Arithmetic types)은 0이 아니면
true - 포인터(Pointer)는 null이 아니면
true - 참조 타입(Reference types)은 null이 아니면
true - 유효한
opCast!bool()메서드를 가진 사용자 정의 타입은 그 메서드가 결과를 위해 호출됩니다. 그렇지 않으면, 목표 타입이 이 규칙 중 하나에 부합하는alias this멤버 선언이 사용됩니다. - 열거(enum) 타입은 기본 타입이 이 규칙 중 하나에 부합하면
true
이 중 어느 것도 유효하지 않으면, 그 값을 조건에서 사용하는 것은 오류입니다.
Note: 동적 배열 a는 조건에서 사용할 수 있는데, a !is []일 때 true입니다. 하지만 배열을 불리언 조건으로 쓰는 것은 버그를 유발할 수 있으므로 피해야 해요. 미래 에디션에서는 금지될 수 있습니다.
Condition Variables (조건 변수)
IfCondition의 Identifier 형태를 사용하면, 그 이름으로 변수가 선언되고 Expression의 값으로 초기화됩니다.
IfConditionStorageClasses에auto와ref가 모두 있으면, 변수는Expression이 lvalue일 때에만 참조(reference)입니다.auto없이ref만 있으면, 변수는 참조이고Expression은 lvalue여야 합니다.ref없이auto만 있으면, 변수의 타입은Expression과 같습니다. (첫 번째 불릿을 참고)ref를 제외한 어떤IfConditionStorageClasses와 결합된auto의 존재는 효과가 없어요. 다른IfConditionStorageClasses가 의미가 없을 때만,auto는 변수를 선언할 수 있는 선택지를 제공합니다.BasicType이 주어졌을 때ref없이auto를 쓰는 것은 허용되지 않습니다.TypeCtor들이 제공되고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 Statement (while 문장)
WhileStatement:
while ( IfCondition ) ScopeStatement
while 문장은 간단한 루프를 구현합니다.
IfCondition이 Expression이면 그 식이 평가되고, 불리언으로 변환될 수 있는 타입이어야 합니다. true이면 ScopeStatement가 실행됩니다. ScopeStatement 실행 후 Expression이 다시 평가되고, 여전히 true이면 ScopeStatement가 다시 실행됩니다. 이 과정은 Expression이 false로 평가될 때까지 계속됩니다.
int i = 0;
while (i < 10)
{
foo(i);
++i;
}
IfCondition이 변수를 선언하면, 그 변수는 매 루프 반복마다 초기화되고 평가됩니다.
BreakStatement는 루프를 빠져나갑니다. ContinueStatement는 다시 IfCondition을 평가하는 지점으로 직접 전달합니다.
Do Statement (do 문장)
DoStatement:
do ScopeStatement while ( Expression ) ;
do-while 문장은 간단한 루프를 구현합니다.
ScopeStatement가 실행됩니다. 그다음 Expression이 평가되고 불리언으로 변환될 수 있는 타입이어야 합니다. true이면 루프가 다시 반복됩니다. Expression이 false로 평가될 때까지 계속됩니다.
int i = 0;
do
{
foo(i);
} while (++i < 10);
BreakStatement는 루프를 빠져나갑니다. ContinueStatement는 다시 Expression을 평가하는 지점으로 직접 전달합니다.
For Statement (for 문장)
for 문장은 초기화(initialize)·조건 검사(test)·증가(increment) 절을 가진 루프를 구현합니다.
ForStatement:
for ( Initialize Testopt ; Incrementopt ) ScopeStatement
Initialize:
;
NoScopeNonEmptyStatement
Test:
Expression
Increment:
Expression
Initialize가 실행됩니다. Test가 평가되고 불리언으로 변환될 수 있는 타입이어야 합니다. 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 Statement (foreach 문장)
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 또는 sequence 중 하나로 평가되어야 합니다. NoScopeNonEmptyStatement는 각 애그리거트(aggregate) 원소마다 한 번씩 실행됩니다.
ForeachTypeList에 선언된 변수의 개수는 애그리거트의 종류에 따라 달라집니다. 선언된 변수들은 각 반복의 시작에 설정됩니다.
- 기본적으로 단일 선언 변수는 현재 원소의 복사본(copy)입니다.
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 over Arrays (배열에 대한 foreach)
애그리거트가 정적 또는 동적 배열이면, 변수를 한 개 또는 두 개 선언할 수 있어요. 한 개이면 그 변수를 값(value)이라 부르고, 배열의 각 원소로 차례로 설정됩니다. 변수의 타입이 지정되면 배열 원소 타입과 호환되어야 합니다(아래 설명할 문자 원소의 특별 처리 제외). 값 변수는 ref로 선언하면 배열 원소를 수정할 수 있습니다.
두 변수를 선언하면 첫 번째를 인덱스(index), 두 번째를 위에서처럼 값이라 부릅니다. index는 ref로 선언할 수 없어요. 각 반복에서 배열 원소의 인덱스로 설정됩니다. 인덱스 타입은 추론될 수 있어요:
char[] a = ['h', 'i'];
foreach (i, char c; a)
{
writefln("a[%d] = '%c'", i, c);
}
동적 배열의 경우, 배열 길이가 더 작은 정수 타입에 정적으로 들어맞는 것으로 알려지지 않는 한, 인덱스 타입은 size_t와 호환되어야 합니다. 정적 배열은 배열 길이를 아우르는 어떤 정수 타입이라도 쓸 수 있어요.
foreach의 경우 배열 원소는 인덱스 0에서 시작해 마지막 원소까지 반복됩니다. foreach_reverse의 경우 배열 원소는 역순으로 방문됩니다.
Foreach over Arrays of Characters (문자 배열에 대한 foreach)
애그리거트 식이 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 over Associative Arrays (연관 배열에 대한 foreach)
애그리거트 식이 연관 배열이면, 변수를 한 개 또는 두 개 선언할 수 있어요. 한 개이면 그 변수를 값이라 부르고, 배열의 각 값으로 차례로 설정됩니다. 변수의 타입이 주어지면 연관 배열 값 타입에서 암시적으로 변환되어야 합니다.
// value type is int
int[string] userAges = ["john":30, "sue":32];
foreach (ref age; userAges)
{
age++;
}
assert(userAges == ["john":31, "sue":33]);
두 변수를 선언하면 첫 번째를 인덱스, 두 번째를 값이라 부릅니다. 인덱스 변수 타입(주어진 경우)은 연관 배열의 키 타입에서 암시적으로 변환 가능해야 합니다. 인덱스 변수는 연관 배열의 각 값에 대응하는 키로 설정됩니다.
// 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-변환되어야 합니다. ref 인덱스 변수는 (추론되어) const여야 합니다. 키는 변경되어서는 안 되기 때문이에요.
Foreach over Structs and Classes with opApply (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이 아닌 값을 반환하는 것.
ForeachStatement 본문이 매칭되는 BreakStatement, ReturnStatement, 또는 매칭 레이블이 ForeachStatement 바깥에 있는 GotoStatement를 실행하면, delegate 호출 결과는 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 함수의 스코프를 벗어나 이스케이프(escape)하지 않음을 뜻합니다(예: dg를 전역 변수에 할당하는 경우). dg가 이스케이프하지 않음을 정적으로 보장할 수 없으면, 스택 대신 힙에 클로저(closure)가 할당될 수 있어요.
Best Practices: 가능하면 opApply 함수의 delegate 파라미터를 scope로 주석을 다세요.
Important: opApply가 어떤 예외도 잡는다면, 그 예외가 opApply에 전달된 delegate에서 비롯된 것이 아니도록 하세요. 사용자는 foreach 본문에서 던져진 예외가 루프를 종료시키고 foreach 본문 밖으로 전파되기를 기대합니다.
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
}
Foreach over Structs and Classes with Ranges (Range를 가진 struct/class에 대한 foreach)
ForeachAggregate가 struct 또는 class 객체인데 opApply가 존재하지 않으면, foreach 반복은 range 프리미티브를 사용합니다. 이 프리미티브들은 일련의 원소들을 만들어냅니다.
ForeachAggregate에 대해 다음 속성(property)과 메서드가 정의되어야 합니다:
| Property | Purpose |
|---|---|
| .empty | returns true if no more elements |
| .front | return the leftmost element of the range |
| Method | Purpose |
|---|---|
| .popFront() | move the left edge of the range right by one |
반복을 위해 임시 변수가 사용되는데, 이는 ForeachAggregate로부터 초기화됩니다. 다음 문장:
foreach (e; range) { ... }
은 다음으로 변환됩니다:
for (auto __r = range; !__r.empty; __r.popFront())
{
auto e = __r.front;
...
}
마찬가지로 opApplyReverse가 정의되지 않은 foreach_reverse의 경우 range 프리미티브가 사용됩니다. ForeachAggregate에 대해 다음 속성과 메서드가 정의되어야 합니다:
| Property | Purpose |
|---|---|
| .empty | returns true if no more elements |
| .back | return the rightmost element of the range |
| Method | Purpose |
|---|---|
| .popBack() | move the right edge of the range left by one |
다음 문장:
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]);
연결 리스트(linked list)를 사용한 예:
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]);
}
}
참고: std.typecons.Tuple.
Foreach over Delegates (delegate에 대한 foreach)
ForeachAggregate가 delegate이면, delegate의 타입 시그니처와 동작은 opApply와 같습니다. 이를 통해 서로 다른 이름의 많은 반복 전략이 같은 class나 struct 안에 공존할 수 있어요.
delegate는 원소들을 즉석에서 생성할 수 있습니다:
// 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 over Sequences (시퀀스에 대한 foreach)
애그리거트 식이 시퀀스(sequence)이면, 루프 본문은 각 원소마다 한 번씩 정적으로 확장됩니다. 이는 시퀀스에 대한 StaticForeachStatement와 비슷하지만, 각 확장마다 별도의 스코프가 있다는 점이 다릅니다.
반복 심볼을 한 개 또는 두 개 선언할 수 있어요. 한 개이면 시퀀스의 각 원소를 차례로 나타내는 원소 심볼입니다.
- 시퀀스가 TypeSeq이면, 원소 심볼은 각 타입으로 차례로 설정됩니다.
- 시퀀스가 ValueSeq이면, 원소 심볼은 각 값으로 차례로 설정됩니다. 원소 심볼의 타입이 주어지면 시퀀스의 모든 원소 타입과 호환되어야 합니다. 타입이 주어지지 않으면, 원소 심볼의 타입은 각 시퀀스 원소의 타입과 일치하며, 원소마다 달라질 수 있어요.
두 심볼을 선언하면 첫 번째가 인덱스, 두 번째가 원소입니다. 인덱스 심볼은 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 없이)은 다음을 생성하려 시도합니다:
- 컴파일 타임에 알려진 값에 대한 컴파일 타임 심볼(compile-time symbols)
- 컴파일 타임에 알려지지 않은 값의 원소에 대한 런타임 변수(runtime variables)
- 값이 없는 원소(예: 타입)에 대한 심볼릭 별칭(symbolic aliases)
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 Parameters (foreach ref 파라미터)
ref를 사용해 ForeachAggregate의 원소를 수정할 수 있어요. 이는 lvalue 원소를 노출하는 컨테이너와 lvalue 시퀀스에 대해 동작합니다.
uint[2] a = [7, 8];
foreach (ref u; a)
{
u++;
}
foreach (u; a)
{
writeln(u);
}
다음을 출력합니다:
8
9
ref는 배열 인덱스 변수에는 적용할 수 없습니다.
Foreach Restrictions (foreach 제약)
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입니다.
Unpacking Foreach Variables (foreach 변수 언패킹)
변수 선언의 언패킹(unpacking)과 비슷하게, 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를 반환합니다. 바깥 튜플은 암시적으로 언랩(unwrap)됩니다:
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 Statement (foreach range 문장)
foreach range 문장은 지정된 범위(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 Statement (switch 문장)
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으로 변환됩니다.- 그다음 타입이 정수(integral)이면
Expression은 Integer Promotions를 거칩니다. - 그다음
Expression의 타입은 정수 또는 문자열이어야 합니다.
Identifier = 접두사가 제공되면 그 이름으로 변수가 선언되고, Expression의 값과 타입으로 초기화됩니다. 그 스코프는 초기화된 지점부터 ScopeStatement의 끝까지 확장됩니다.
Identifier에 TypeCtor 및/또는 특정 타입이 제공되면, Expression의 값으로부터의 암시적 변환으로 초기화되는 변수 선언에 그들이 사용됩니다.
결과 값은 각 case 식과 비교됩니다. 일치하면 해당 case 문장으로 전달됩니다.
switch 문장은 정확히 하나의 DefaultStatement를 가져야 합니다. 어떤 case 식도 일치하지 않으면 default 문장으로 제어가 전달됩니다.
Rationale (이유): 이렇게 하면 가능한 모든 경우가 의도적으로 처리됨을 분명히 합니다. 참고: final switch.
Case and Default Statements (case/default 문장)
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 Statement (case 범위 문장)
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, 또는GotoStatementassert(0)식의 평가noreturn타입의 식 평가
위 마지막 두 경우의 간단한 형태는 컴파일러가 인식할 수 있지만, 전부는 아닙니다.
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로의 명시적 폴스루(explicit fall-through)가 가능해요:
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 문장으로 점프하는 것도 지원합니다.
String Switch (문자열 switch)
switch 식에서 문자열을 사용할 수 있어요. char, wchar, dchar의 정적 또는 동적 배열이 지원됩니다. 예를 들어:
string name = "fred";
switch (name)
{
case "fred", "sally":
writeln("Hi ", name);
break;
default:
writeln("Not recognized");
}
Rationale (이유): 명령줄 switch 처리 같은 애플리케이션에서 이는 훨씬 더 직관적인 코드로 이어질 수 있고, 더 명확하며 오류가 덜 발생합니다.
Final Switch Statement (final switch 문장)
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 Statement (continue 문장)
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 절이 그 밖으로 throw를 실행하면, continue 대상은 절대 도달하지 않습니다.
Break Statement (break 문장)
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 절이 그 밖으로 throw를 실행하면, break 대상은 절대 도달하지 않습니다.
Return Statement (return 문장)
ReturnStatement:
return Expressionopt ;
return은 현재 함수를 빠져나가고 그 반환값을 제공합니다.
함수가 void가 아닌 반환 타입을 지정하면 Expression이 필요합니다. Expression은 함수 반환 타입으로 암시적으로 변환됩니다.
함수가 void 반환 타입을 지정하면 void 타입의 Expression이 허용됩니다. 그 Expression은 평가되지만 아무것도 반환되지는 않습니다. 이것은 제네릭 프로그래밍에서 유용합니다.
함수가 실제로 반환하기 전에, scope 저장 기간을 가진 객체는 파괴되고, 둘러싸는 finally 절은 실행되며, scope(exit) 문장은 실행되고, scope(success) 문장은 실행되며, 둘러싸는 동기화 객체는 해제됩니다.
둘러싸는 finally 절이 그 finally 절을 빠져나가는 return, goto 또는 throw를 하면 함수는 반환하지 않습니다.
out 사후조건(postcondition)이 있으면, Expression이 평가된 후 함수가 실제로 반환하기 전에 그 사후조건이 실행됩니다.
int foo(int x)
{
return x + 3;
}
Goto Statement (goto 문장)
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 절은 실행되고, 사이에 있는 동기화 뮤텍스는 해제됩니다.
GotoStatement가 자신을 포함하는 스코프 안의 초기화를 건너뛰는 데 사용되는 것은 불법입니다.
Best Practices: goto Identifier; 형태보다는 더 높은 수준의 제어 흐름 구조나 레이블된 BreakStatement/ContinueStatement를 사용하는 것을 선호하세요.
With Statement (with 문장)
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;
}
같은 식별자의 지역 심볼을 가리는(숨기는) 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 Statement (synchronized 문장)
synchronized 문장은 여러 스레드 간의 접근을 동기화하기 위해 뮤텍스 잠금과 해제로 문장을 감쌉니다.
SynchronizedStatement:
synchronized ScopeStatement
synchronized ( Expression ) ScopeStatement
Expression 없는 synchronized 문장은 뮤텍스를 잠금으로써 한 번에 한 스레드만 ScopeStatement를 실행하게 합니다. synchronized 문장 하나당 하나씩 전역 뮤텍스가 생성됩니다. 서로 다른 synchronized 문장은 서로 다른 전역 뮤텍스를 갖습니다.
Expression이 있으면, 그것은 Object 또는 인터페이스의 인스턴스로 평가되어야 하며, 이 경우 그 인터페이스를 구현한 Object 인스턴스로 캐스팅됩니다. 사용되는 뮤텍스는 그 Object 인스턴스에 특정하며, 그 인스턴스를 참조하는 모든 synchronized 문장이 공유합니다. synchronized 문장에 도달했을 때 객체의 뮤텍스가 이미 잠겨 있으면, 다른 코드가 그 뮤텍스를 해제할 때까지 모든 스레드를 차단합니다.
void work();
void f(Object o)
{
synchronized (o) work();
}
void g(Object o)
{
synchronized (o) work();
}
f와 g가 서로 다른 스레드에서 호출되지만 같은 인자라면, work 호출은 동시에 실행될 수 없습니다. 한쪽 또는 양쪽 함수에서 synchronized 문장의 (o) 부분을 제거하면, 서로 다른 뮤텍스로 보호되므로 두 work 호출이 동시에 실행될 수 있습니다.
ScopeStatement가 예외, goto, 또는 return으로 종료되더라도 동기화는 해제됩니다.
이것은 표준 임계 구역(critical section)을 구현합니다.
synchronized 문장은 재귀적 잠금(recursive locking)을 지원합니다. 즉 synchronized로 감싼 함수가 재귀적으로 자신을 호출해도 동작은 예상대로입니다. 뮤텍스는 재귀 횟수만큼 잠그고 해제됩니다.
참고: synchronized methods.
Try Statement (try 문장)
예외 처리는 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이 뒤따르는 Catch의 타입 T2를 가리면 오류입니다. 즉 T1이 T2와 같은 타입이거나 T2의 기본 클래스이면 오류입니다.
FinallyStatement는 try ScopeStatement가 goto, break, continue, return, 예외, 또는 폴스루로 나가든 항상 실행됩니다.
FinallyStatement에서 예외가 발생했는데 원래 예외가 잡히기 전에 잡히지 않으면, Throwable의 next 멤버를 통해 이전 예외에 연결(chain)됩니다. 대부분의 다른 프로그래밍 언어와 달리, 새 예외가 원래 예외를 대체하지 않는다는 점에 주의하세요. 대신 이후의 예외들은 첫 번째 예외에 의해 생긴 '부수적 피해(collateral damage)'로 간주됩니다. 원래 예외가 잡혀야 하며, 이렇게 함으로써 전체 연결이 포착됩니다.
Error 클래스에서 파생된 던져진 객체는 다르게 처리됩니다. 이들은 정상적인 연결 메커니즘을 우회해서, 연결은 첫 번째 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 Statement (scope guard 문장)
ScopeGuardStatement:
scope ( exit ) NonEmptyOrScopeBlockStatement
scope ( success ) NonEmptyOrScopeBlockStatement
scope ( failure ) NonEmptyOrScopeBlockStatement
NonEmptyOrScopeBlockStatement가 실행되면, 그것은 ScopeGuardStatement가 나타난 지점이 아니라 현재 스코프가 닫힐 때 발생합니다:
scope(exit)는 스코프가 정상적으로 나가거나 예외 언와인딩(exception unwinding)으로 나갈 때NonEmptyOrScopeBlockStatement를 실행합니다.scope(failure)는 스코프가 예외 언와인딩으로 나갈 때에만NonEmptyOrScopeBlockStatement를 실행합니다.scope(success)는 스코프가 정상적으로 나갈 때에만NonEmptyOrScopeBlockStatement를 실행합니다.
스코프에 ScopeGuardStatement가 여러 개 있으면, 나타난 어휘 순서의 역순으로 실행됩니다. 스코프를 닫을 때 파괴되어야 하는 어떤 scope 인스턴스든, 그 파괴는 ScopeGuardStatement와 나타난 어휘 순서의 역순으로 엮여서(interleave) 수행됩니다.
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()
Catching C++ Class Objects (C++ 클래스 객체 잡기)
많은 플랫폼에서 C++ 클래스 객체를 잡는 것을 지원합니다. C++ 객체와 D 객체를 같은 TryStatement에서 둘 다 잡을 수는 없습니다. Catch에서 나갈 때 C++ 객체의 어떤 파괴자라도 실행되고, 그에 사용된 저장 공간은 회수됩니다. C++ 객체는 @safe 코드에서 잡을 수 없습니다.
Asm Statement (asm 문장)
인라인 어셈블러는 asm 문장으로 지원됩니다:
AsmStatement:
asm FunctionAttributesopt { AsmInstructionListopt }
AsmInstructionList:
AsmInstruction ;
AsmInstruction ; AsmInstructionList
asm 문장은 어셈블리 언어 명령어를 직접 사용하게 해줍니다. 이렇게 하면 외부 어셈블러에 의존하지 않고도 특수 CPU 기능에 직접 접근하기 쉬워집니다. D 컴파일러가 함수 호출 규약, 스택 설정 등을 처리합니다.
명령어의 형식은 당연히 대상 CPU의 네이티브 명령어 집합에 크게 의존하며, 그래서 구현 정의입니다. 하지만 형식은 다음 관례를 따릅니다:
- D 언어가 사용하는 것과 같은 토큰을 사용해야 합니다.
- 주석 형식이 D 언어 주석과 일치해야 합니다.
- asm 명령어는 줄의 끝이 아니라
;로 종료됩니다.
이 규칙들은 D 소스 코드가 구문 또는 의미 분석과 독립적으로 토큰화될 수 있도록 보장하기 위해 존재합니다.
예를 들어 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 Statement (pragma 문장)
PragmaStatement를 참고하세요.
Mixin Statement (mixin 문장)
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
}
더 알아보기 (Learn more)
- D 언어 공식 사양 전체 목차: https://dlang.org/spec/spec.html
- 이전 장 (Expressions, 식): https://dlang.org/spec/expression.html
- 다음 장 (Arrays, 배열): https://dlang.org/spec/arrays.html
- D 언어 투어 (Learn): https://tour.dlang.org