if — 세 가지 상황을 한 문법으로 처리하는 조건식
if — 세 가지 상황을 한 문법으로 처리하는 조건식
Zig를 쓰다 보면 인생은 if (a != b) … else …만으로 끝나면 좋겠지만, 값이 '있거나 없거나' 하는 경우, '성공이거나 실패이거나' 하는 경우도 다뤄야 하죠. Zig의 if는 그걸 각각 다른 문법 없이 같은 모양 안에서 해결해 줍니다. if 표현식은 쓰임이 정확히 세 가지인데, 그 세 가지는 그냥 '조건이 참인지'를 따지는 것부터, 값이 담겨 있는지, 오류가 없는지까지 — 검사하는 대상의 타입이 달라질 뿐이에요. 각각 bool, ?T, anyerror!T 세 타입에 대응합니다.
본문
먼저 기본이 되는 if 표현식부터 볼게요. 다른 언어에서 삼항 연산자(?:)를 쓰던 자리를, Zig에서는 if 표현식이 대신합니다. 조건이 참이면 첫 값을, 거짓이면 else 쪽 값을 만들어내는 거죠.
test_if.zig
const expect = @import("std").testing.expect;
const expectEqual = @import("std").testing.expectEqual;
test "if expression" {
// If expressions are used instead of a ternary expression.
const a: u32 = 5;
const b: u32 = 4;
const result = if (a != b) 47 else 3089;
try expectEqual(result, 47);
}
test "if boolean" {
// If expressions test boolean conditions.
const a: u32 = 5;
const b: u32 = 4;
if (a != b) {
try expect(true);
} else if (a == 9) {
unreachable;
} else {
unreachable;
}
}
test "if error union" {
// If expressions test for errors.
// Note the |err| capture on the else.
const a: anyerror!u32 = 0;
if (a) |value| {
try expectEqual(value, 0);
} else |err| {
_ = err;
unreachable;
}
const b: anyerror!u32 = error.BadValue;
if (b) |value| {
_ = value;
unreachable;
} else |err| {
try expectEqual(err, error.BadValue);
}
// The else and |err| capture is strictly required.
if (a) |value| {
try expectEqual(value, 0);
} else |_| {}
// To check only the error value, use an empty block expression.
if (b) |_| {} else |err| {
try expectEqual(err, error.BadValue);
}
// Access the value by reference using a pointer capture.
var c: anyerror!u32 = 3;
if (c) |*value| {
value.* = 9;
} else |_| {
unreachable;
}
if (c) |value| {
try expectEqual(value, 9);
} else |_| {
unreachable;
}
}
이 세 테스트를 돌려 보면 모두 통과해요.
$ zig test test_if.zig
1/3 test_if.test.if expression...OK
2/3 test_if.test.if boolean...OK
3/3 test_if.test.if error union...OK
All 3 tests passed.
여기서 놓치기 쉬운 부분을 짚을게요. if boolean 테스트는 아시는 대로 조건이 bool인 경우라 어렵지 않은데, if error union 테스트가 Zig다운 부분이에요. anyerror!u32를 검사할 때 if (a) |value|처럼 성공했을 때의 값을 뽑아내고, else |err|처럼 오류 값을 잡아내는 거죠. 여기서 포인트는 else와 |err| 캡처가 반드시 필요하다는 점입니다. 오류 값만 보고 싶을 때는 if (b) |_| {} else |err| { … }처럼 성공 쪽 바디를 빈 블록으로 비워 두면 돼요. 또 if (c) |*value|처럼 포인터 캡처(|*value|)를 쓰면 값을 참조로 접근해서 value.* = 9처럼 수정할 수도 있어요.
이제 한 단계 더 나아가서, 값이 '있거나 없을 수 있는' optional과 만나는 경우를 볼게요.
if with Optionals
test_if_optionals.zig
const expect = @import("std").testing.expect;
const expectEqual = @import("std").testing.expectEqual;
test "if optional" {
// If expressions test for null.
const a: ?u32 = 0;
if (a) |value| {
try expectEqual(0, value);
} else {
unreachable;
}
const b: ?u32 = null;
if (b) |_| {
unreachable;
} else {
try expect(true);
}
// The else is not required.
if (a) |value| {
try expectEqual(0, value);
}
// To test against null only, use the binary equality operator.
if (b == null) {
try expect(true);
}
// Access the value by reference using a pointer capture.
var c: ?u32 = 3;
if (c) |*value| {
value.* = 2;
}
if (c) |value| {
try expectEqual(2, value);
} else {
unreachable;
}
}
test "if error union with optional" {
// If expressions test for errors before unwrapping optionals.
// The |optional_value| capture's type is ?u32.
const a: anyerror!?u32 = 0;
if (a) |optional_value| {
try expectEqual(0, optional_value.?);
} else |err| {
_ = err;
unreachable;
}
const b: anyerror!?u32 = null;
if (b) |optional_value| {
try expectEqual(null, optional_value);
} else |_| {
unreachable;
}
const c: anyerror!?u32 = error.BadValue;
if (c) |optional_value| {
_ = optional_value;
unreachable;
} else |err| {
try expectEqual(error.BadValue, err);
}
// Access the value by reference by using a pointer capture each time.
var d: anyerror!?u32 = 3;
if (d) |*optional_value| {
if (optional_value.*) |*value| {
value.* = 9;
}
} else |_| {
unreachable;
}
if (d) |optional_value| {
try expectEqual(9, optional_value.?);
} else |_| {
unreachable;
}
}
$ zig test test_if_optionals.zig
1/2 test_if_optionals.test.if optional...OK
2/2 test_if_optionals.test.if error union with optional...OK
All 2 tests passed.
여기서 확인할 점이 몇 개 있어요. 우선 ?u32 같은 optional 타입은 if (a) |value|로 null이 아닐 때 값을 뽑아내는데, else는 필수로 넣지 않아도 됩니다. 값이 null인지 그냥 확인만 하고 싶다면 if (b == null)처럼 이항 동등 연산자를 써도 돼요. 그리고 포인터 캡처를 쓰면 var c: ?u32 = 3;의 값을 value.* = 2처럼 직접 바꿀 수 있어요.
마지막 테스트가 살짝 헷갈릴 수 있는데, anyerror!?u32는 '오류일 수도, null일 수도, 값일 수도' 있죠. 이때 if는 optional을 풀기 전에 먼저 오류부터 검사합니다. 그래서 if (a) |optional_value|에서 잡힌 optional_value의 타입은 이미 ?u32예요. 그래서 안쪽 값에 접근할 때는 optional_value.?처럼 다시 한 번 unwrap 해야 해요. 포인터 캡처를 겹쳐서 쓰면 if (optional_value.*) |*value| { … }처럼 optional 안의 값을 참조로 수정하는 것도 가능합니다.
더 알아보기
if가 다루는 세 타입 가운데?T(optional)와anyerror!T(error union)에 대한 자세한 내용은 각각Optionals,Errors섹션을 참고하세요.- 값을 '있거나 없거나' 하는 타입을 더 깊이 보고 싶다면
Optionals섹션에서, 오류 처리 전반은Errors섹션에서 이어집니다.