defer
defer (지연 실행)
defer는 블록을 벗어날 때 무조건 실행될 표현식을 등록해 두는 Zig의 정리(cleanup) 수단이에요. 파일을 열거나 메모리를 할당한 뒤, 코드 도중에 여러 갈래로 빠져나가더라도 정리를 빠뜨리지 않게 해 주죠. "나중에 이건 꼭 실행해 줘"라고 미리 걸어 두는 셈이에요. 어떤 식으로 동작하는지 바로 확인해 볼게요.
본문
defer는 스코프(scope)를 벗어날 때 표현식을 조건 없이(unconditionally) 실행해요.
const std = @import("std");
const expectEqual = std.testing.expectEqual;
const print = std.debug.print;
fn deferExample() !usize {
var a: usize = 1;
{
defer a = 2;
a = 1;
}
try expectEqual(2, a);
a = 5;
return a;
}
test "defer basics" {
try expectEqual(5, (try deferExample()));
}
$ zig test test_defer.zig
1/1 test_defer.test.defer basics...OK
All 1 tests passed.
안쪽 블록 안에서 defer a = 2;를 선언했죠. 블록 안의 a = 1;이 실행된 다음, 블록을 벗어나는 순간 defer에 걸어 둔 a = 2가 실행돼요. 그래서 try expectEqual(2, a);가 통과해요. 정리 코드가 블록 끝이 아니라 선언 위치에 있다는 점이 핵심이에요 — 어디에 있든 스코프를 빠져나갈 때 확실히 실행된다고 보장하는 거죠.
defer 표현식은 역순(reverse order)으로 평가돼요. 즉 나중에 선언한 것부터 먼저 실행돼요.
const std = @import("std");
const print = std.debug.print;
pub fn main() void {
print("\n", .{});
defer {
print("1 ", .{});
}
defer {
print("2 ", .{});
}
if (false) {
// defers are not run if they are never executed.
defer {
print("3 ", .{});
}
}
}
$ zig build-exe defer_unwind.zig
$ ./defer_unwind
2 1
출력을 보면 2 1 순서로 나왔어요. "2 "를 찍는 defer를 나중에 선언했는데, defer는 역순으로 평가되니까 나중에 선언한 쪽이 먼저 실행된 거예요. 마치 스택(stack)처럼 쌓였다가 LIFO(Last-In-First-Out) 순서로 풀리는 셈이죠. 이건 여러 리소스를 열 때, 그 역순으로 정리해야 자원 해제 순서가 맞는 상황에 아주 자연스럽게 맞아요.
여기서 주목할 점이 하나 더 있어요. if (false) 블록 안의 defer는 실행되지 않았어요. 원문 주석 그대로, 절대 실행되지 않는 경로의 defer는 걸린 적이 없으니 실행되지 않는다는 뜻이에요. 즉 defer는 "블록 안에 들어왔을 때"에만 등록되고, 그 블록에 들어가지 않았다면 잊혀지는 거죠.
한 가지 제약이 있어요. defer 표현식 안에서는 return 문을 쓸 수 없어요.
fn deferInvalidExample() !void {
defer {
return error.DeferError;
}
return error.DeferError;
}
$ zig test test_invalid_defer.zig
/home/ci/work/zig-bootstrap/zig/doc/langref/test_invalid_defer.zig:3:9: error: cannot return from defer expression
return error.DeferError;
^~~~~~~~~~~~~~~~~~~~~~~
/home/ci/work/zig-bootstrap/zig/doc/langref/test_invalid_defer.zig:2:5: note: defer expression here
defer {
^~~~~
컴파일러가 cannot return from defer expression이라는 에러를 뱉는 걸 볼 수 있어요. defer는 스코프를 빠져나가는 "뒷정리" 역할만 하도록 설계됐기 때문에, 그 안에서 함수를 끝내려는 return은 막히는 거예요. 정리 코드는 정리만 하고, 함수의 흐름 결정은 원래 자리에서 하라는 의도로 이해하면 돼요.
더 알아보기
defer와 짝을 이루는errdefer는 오류가 발생해 빠져나갈 때만 실행돼서, 오류 처리에서 자원 정리를 더 정밀하게 해 줄 수 있어요. Zig 메뉴얼의 Errors 섹션에서 이어서 볼 수 있어요.