Optionals

Optionals

효율이나 가독성을 해치지 않으면서도 안전을 지켜 주는 Zig의 대표적인 장치가 바로 옵셔널 타입이에요.

출처: Zig Documentation

본문

? 물음표가 옵셔널 타입을 나타내요. 타입 앞에 물음표를 붙이면 그 타입의 옵셔널 타입으로 바꿀 수 있어요:

// normal integer
const normal_int: i32 = 1234;

// optional integer
const optional_int: ?i32 = 5678;

이제 optional_int 변수는 i32일 수도 있고, null일 수도 있어요.

정수 대신 포인터를 생각해 볼게요. 널 참조(null reference)는 수많은 런타임 예외의 원인이면서, 심지어 컴퓨터 과학의 최악의 실수라는 비난을 받기도 해요. Zig에는 그런 널 참조가 없어요.

대신 옵셔널 포인터(optional pointer)를 쓸 수 있어요. 옵셔널 타입에서는 0을 널 값으로 쓸 수 있다는 걸 알고 있으니, 이건 내부적으로는 그냥 평범한 포인터로 컴파일돼요. 다만 컴파일러가 여러분의 코드를 검사해서, 널이 될 수 없는 곳에 널을 할당하지 않도록 지켜 줘요.

널이 없다는 것의 단점은 보통 코드를 더 장황하게 써야 한다는 점이에요. 하지만 동등한 C 코드와 Zig 코드를 한번 비교해 볼게요.

할 일: malloc을 호출하고, 결과가 널이면 널을 반환한다.

C 코드

// malloc prototype included for reference
void *malloc(size_t size);

struct Foo *do_a_thing(void) {
    char *ptr = malloc(1234);
    if (!ptr) return NULL;
    // ...
}

Zig 코드

// malloc prototype included for reference
extern fn malloc(size: usize) ?[*]u8;

fn doAThing() ?*Foo {
    const ptr = malloc(1234) orelse return null;
    _ = ptr; // ...
}

이 비교에서 Zig는 C보다 못하지 않고, 오히려 더 편해요. 그리고 여기서 ptr의 타입은 [*]u8이지 ?[*]u8이 아니에요. orelse 키워드가 옵셔널 타입을 풀어 줬기 때문에, 함수 안에서 ptr이 쓰이는 모든 곳에서 널이 아님을 보장받을 수 있어요.

널을 검사하는 또 다른 형태는 이런 모습이에요:

void do_a_thing(struct Foo *foo) {
    // do some stuff

    if (foo) {
        do_something_with_foo(foo);
    }

    // do some stuff
}

Zig에서도 같은 일을 이렇게 할 수 있어요:

const Foo = struct {};
fn doSomethingWithFoo(foo: *Foo) void {
    _ = foo;
}

fn doAThing(optional_foo: ?*Foo) void {
    // do some stuff

    if (optional_foo) |foo| {
        doSomethingWithFoo(foo);
    }

    // do some stuff
}

여기서 주목할 점은, if 블록 안에서는 foo가 더 이상 옵셔널 포인터가 아니라 널이 될 수 없는 일반 포인터라는 거예요.

이렇게 되면 포인터를 인자로 받는 함수에 nonnull 속성(GCC의 __attribute__((nonnull)))을 붙일 수 있다는 이점도 생겨요. 옵티마이저는 포인터 인자가 널이 될 수 없다는 걸 알면 때때로 더 나은 결정을 내릴 수 있어요.

Optional Type §

타입 앞에 ?를 붙이면 옵셔널이 만들어져요. 컴파일 타임 리플렉션을 쓰면 옵셔널의 자식 타입(child type)에 접근할 수 있어요:

const expectEqual = @import("std").testing.expectEqual;

test "optional type" {
    // Declare an optional and coerce from null:
    var foo: ?i32 = null;

    // Coerce from child type of an optional
    foo = 1234;

    // Use compile-time reflection to access the child type of the optional:
    try comptime expectEqual(i32, @typeInfo(@TypeOf(foo)).optional.child);
}
$ zig test test_optional_type.zig
1/1 test_optional_type.test.optional type...OK
All 1 tests passed.

null §

undefined와 마찬가지로 null도 자기만의 타입을 갖고 있어요. 사용하는 방법은 다른 타입으로 캐스팅하는 것뿐이에요:

const optional_value: ?i32 = null;

Optional Pointers §

옵셔널 포인터는 일반 포인터와 크기가 같다는 것이 보장돼요. 그리고 옵셔널의 널은 주소 0이라고 보장돼요.

const expectEqual = @import("std").testing.expectEqual;

test "optional pointers" {
    // Pointers cannot be null. If you want a null pointer, use the optional
    // prefix `?` to make the pointer type optional.
    var ptr: ?*i32 = null;

    var x: i32 = 1;
    ptr = &x;

    try expectEqual(1, ptr.?.*);

    // Optional pointers are the same size as normal pointers, because pointer
    // value 0 is used as the null value.
    try expectEqual(@sizeOf(?*i32), @sizeOf(*i32));
}
$ zig test test_optional_pointer.zig
1/1 test_optional_pointer.test.optional pointers...OK
All 1 tests passed.

더 알아보기