Skip to content
zig 版本:0.15.1

结果位置语义

Result Location Semantics 是 Zig 类型系统的核心机制之一,它允许编译器根据上下文自动推断类型。

在 Zig 中,许多表达式的类型不是由表达式本身决定的,而是由表达式的使用方式决定的。这种机制被称为结果位置语义(Result Location Semantics)

什么是结果位置语义

结果位置语义是指编译器根据表达式结果的存储位置(即"结果位置")来推断表达式类型的机制。

简单来说,当你写 .{ .x = 1, .y = 2 } 这样的表达式时,编译器会查看这个值将被赋给什么类型的变量,然后自动推断出正确的类型。

zig
const Point = struct {
    x: i32,
    y: i32,
};

pub fn main() void {
    // 编译器从变量类型推断出 .{} 的具体类型
    const pt: Point = .{ .x = 10, .y = 20 };

    // 等价于
    const pt2: Point = Point{ .x = 10, .y = 20 };

    std.debug.print("pt: ({}, {}), pt2: ({}, {})\n", .{ pt.x, pt.y, pt2.x, pt2.y });
}

结果类型

结果类型(Result Type) 是编译器期望表达式产生的类型。它由表达式的上下文决定:

变量声明

当变量有显式类型声明时,右侧表达式的结果类型就是该变量的类型:

zig
const Color = struct {
    r: u8,
    g: u8,
    b: u8,
};

pub fn main() void {
    // 结果类型是 Color
    const red: Color = .{ .r = 255, .g = 0, .b = 0 };
    std.debug.print("red: ({}, {}, {})\n", .{ red.r, red.g, red.b });
}

函数返回值

函数的返回类型决定了 return 表达式的结果类型:

zig
const Vec2 = struct {
    x: f32,
    y: f32,
};

fn origin() Vec2 {
    // 结果类型是 Vec2
    return .{ .x = 0, .y = 0 };
}

pub fn main() void {
    const o = origin();
    std.debug.print("origin: ({d}, {d})\n", .{ o.x, o.y });
}

函数参数

函数参数类型决定了调用时传入表达式的结果类型:

zig
const Size = struct {
    width: u32,
    height: u32,
};

fn calculateArea(size: Size) u64 {
    return @as(u64, size.width) * size.height;
}

pub fn main() void {
    // 调用时,.{} 的结果类型是 Size
    const area = calculateArea(.{ .width = 100, .height = 50 });
    std.debug.print("area: {}\n", .{area});
}

结构体字段默认值

结构体字段的类型决定了默认值表达式的结果类型:

zig
const Config = struct {
    timeout: u32 = 30,
    retries: u8 = 3,
};

const Wrapper = struct {
    // 字段类型是 Config,所以 .{} 的结果类型是 Config
    config: Config = .{},
};

pub fn main() void {
    const w: Wrapper = .{};
    std.debug.print("timeout: {}, retries: {}\n", .{ w.config.timeout, w.config.retries });
}

结果位置

结果位置(Result Location) 是指表达式结果将被存储的内存位置。Zig 编译器会将结果位置的信息传递给子表达式,这使得某些优化成为可能。

嵌套结构的结果位置传播

结果位置信息会传播到嵌套的子表达式:

zig
const Inner = struct {
    value: i32,
};

const Outer = struct {
    inner: Inner,
    name: []const u8,
};

pub fn main() void {
    // 结果位置 Outer 传播到 inner 字段,使其结果类型为 Inner
    const obj: Outer = .{
        .inner = .{ .value = 42 }, // 这里 .{} 的结果类型是 Inner
        .name = "example",
    };
    std.debug.print("inner.value: {}, name: {s}\n", .{ obj.inner.value, obj.name });
}

声明字面量

声明字面量(Decl Literals) 是 Zig 0.14.0 引入的语法特性。它扩展了枚举字面量语法 .foo,使其不仅可以引用枚举成员,还可以引用目标类型上的任何声明。

基本用法

zig
const S = struct {
    x: u32,

    // 类型内的常量声明
    const default: S = .{ .x = 123 };
};

pub fn main() void {
    // .default 会被解析为 S.default
    const val: S = .default;
    std.debug.print("val.x: {}\n", .{val.x});
}

test "decl literal" {
    const val: S = .default;
    try std.testing.expectEqual(123, val.x);
}

由于 val 的类型是 S,编译器知道 .default 应该在 S 的命名空间中查找,因此 .default 等价于 S.default

在结构体字段默认值中使用

声明字面量在设置字段默认值时特别有用:

zig
const Settings = struct {
    x: u32,
    y: u32,

    const default: Settings = .{ .x = 1, .y = 2 };
    const high_performance: Settings = .{ .x = 100, .y = 200 };
};

const Application = struct {
    // 使用声明字面量设置默认值
    settings: Settings = .default,
};

pub fn main() void {
    const app1: Application = .{};
    std.debug.print("app1.settings: ({}, {})\n", .{ app1.settings.x, app1.settings.y });

    // 也可以覆盖为其他预定义值
    const app2: Application = .{ .settings = .high_performance };
    std.debug.print("app2.settings: ({}, {})\n", .{ app2.settings.x, app2.settings.y });
}

test "decl literal in field default" {
    const app1: Application = .{};
    try std.testing.expectEqual(1, app1.settings.x);

    const app2: Application = .{ .settings = .high_performance };
    try std.testing.expectEqual(100, app2.settings.x);
}

调用函数

声明字面量也支持调用函数:

zig
const Point = struct {
    x: i32,
    y: i32,

    fn init(val: i32) Point {
        return .{ .x = val, .y = val };
    }

    fn offset(val: i32, dx: i32, dy: i32) Point {
        return .{ .x = val + dx, .y = val + dy };
    }
};

pub fn main() void {
    // .init(5) 等价于 Point.init(5)
    const p1: Point = .init(5);
    std.debug.print("p1: ({}, {})\n", .{ p1.x, p1.y });

    const p2: Point = .offset(0, 10, 20);
    std.debug.print("p2: ({}, {})\n", .{ p2.x, p2.y });
}

test "call function via decl literal" {
    const p1: Point = .init(5);
    try std.testing.expectEqual(5, p1.x);
    try std.testing.expectEqual(5, p1.y);

    const p2: Point = .offset(0, 10, 20);
    try std.testing.expectEqual(10, p2.x);
    try std.testing.expectEqual(20, p2.y);
}

与错误联合类型配合

声明字面量支持通过 try 调用返回错误联合的函数:

zig
const Buffer = struct {
    data: std.ArrayListUnmanaged(u32),

    fn initCapacity(allocator: std.mem.Allocator, capacity: usize) !Buffer {
        return .{ .data = try .initCapacity(allocator, capacity) };
    }
};

test "decl literal with error union" {
    var buf: Buffer = try .initCapacity(std.testing.allocator, 10);
    defer buf.data.deinit(std.testing.allocator);

    buf.data.appendAssumeCapacity(42);
    try std.testing.expectEqual(42, buf.data.items[0]);
}

避免错误的字段默认值

声明字面量的一个重要应用是避免**错误的字段默认值(Faulty Default Field Values)**问题。

问题示例

考虑以下代码:

zig
/// `ptr` 指向 `[len]u32`
pub const BufferA = extern struct {
    ptr: ?[*]u32 = null,
    len: usize = 0,
};

// 看起来是空 buffer
var empty_buf: BufferA = .{};

// 但用户可以只覆盖部分字段,导致不一致的状态!
var bad_buf: BufferA = .{ .len = 10 }; // ptr 是 null,但 len 是 10

这里的问题是 ptrlen 是相互关联的,但默认值允许用户只覆盖其中一个,导致数据不一致。

使用声明字面量的解决方案

zig
/// `ptr` 指向 `[len]u32`
pub const BufferB = extern struct {
    ptr: ?[*]u32,
    len: usize,

    // 通过声明提供预定义的有效状态
    pub const empty: BufferB = .{ .ptr = null, .len = 0 };
};

// 安全地创建空 buffer
var empty_buf: BufferB = .empty;

// 如果要手动指定值,必须同时指定所有字段
// var custom_buf: BufferB = .{ .ptr = some_ptr, .len = 10 };

标准库中的应用

从 Zig 0.14.0 开始,标准库中的许多类型都采用了声明字面量模式。

ArrayListUnmanaged

zig
const Container = struct {
    // 使用 .empty 而不是 .{}
    list: std.ArrayListUnmanaged(i32) = .empty,
};

test "ArrayListUnmanaged with decl literal" {
    var c: Container = .{};
    defer c.list.deinit(std.testing.allocator);

    try c.list.append(std.testing.allocator, 1);
    try c.list.append(std.testing.allocator, 2);

    try std.testing.expectEqual(2, c.list.items.len);
}

GeneralPurposeAllocator

zig
pub fn main() !void {
    // 使用 .init 进行初始化
    var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();
    const ptr = try allocator.alloc(u8, 100);
    defer allocator.free(ptr);

    std.debug.print("allocated {} bytes\n", .{ptr.len});
}

test "GPA with decl literal" {
    var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();
    const ptr = try allocator.alloc(u8, 100);
    defer allocator.free(ptr);

    try std.testing.expectEqual(100, ptr.len);
}

字段和声明不可重名

Zig 0.14.0 引入了一项限制:容器类型(structunionenumopaque)的字段和声明不能同名。

zig
// 错误:字段和声明同名(此代码无法编译)
// const Bad = struct {
//     Value: u32,           // 字段
//     const Value = 100;    // 声明 - 编译错误!
// };

// 正确:遵循命名约定
const Good = struct {
    value: u32, // 字段使用 snake_case
    const Value = 100; // 声明使用 PascalCase
};

这个限制是为了消除 MyType.foo 到底是访问字段还是声明的歧义。