惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
The Blog of Author Tim Ferriss
I
InfoQ
H
Hackread – Cybersecurity News, Data Breaches, AI and More
aimingoo的专栏
aimingoo的专栏
小众软件
小众软件
有赞技术团队
有赞技术团队
J
Java Code Geeks
Apple Machine Learning Research
Apple Machine Learning Research
大猫的无限游戏
大猫的无限游戏
Engineering at Meta
Engineering at Meta
B
Blog RSS Feed
博客园_首页
Y
Y Combinator Blog
V
Visual Studio Blog
Google DeepMind News
Google DeepMind News
M
MIT News - Artificial intelligence
雷峰网
雷峰网
博客园 - 司徒正美
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
H
Help Net Security
P
Proofpoint News Feed
B
Blog
云风的 BLOG
云风的 BLOG
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报

网上冲浪指南

成都双流区凤翔湖公园 四川雅安龙苍沟 在 Zig 中实现 TaskCompletionSource 自定义 zig test runner 如何配置 ZigBrains 用 Zig 实现一个简单的任务队列 密码保护:借助图生图模型创建电子桌游地图 用 Cloudflare 给 WordPress 减负 接二连三修电脑 修复小新 Pad Pro 2021 蓝牙耳机没有声音的问题 手动升级一下 OpenWRT 如何安全地面向公网提供本地 NAS 上的 Web 服务 徒步·金堂开照寺二道坪山脊环线 Hello, ActivityPub 迁移博客到 VPS 优化博客网站的性能 解决 Qsirch 无法搜索文件夹的问题 N100 小主机遭遇 NVMe 硬盘故障:一次系统的诊断与反思 外接显示器 EDID 损坏如何处理 How to switch GitHub CLI account automatically
Zig 中的类型替换
Zeeko · 2026-09-02 · via 网上冲浪指南

“派生类(子类)对象可以在程序中代替其基类(超类)对象。”

里氏替换原则

与 C# 这种面向对象的编程语言不同,Zig 提供了多种方式让我们可以在程序中应用里氏替换:

  • vtable 替换
  • anytype 替换
  • comptime type 替换
  • union 替换

基于我们的使用场景,我们需要像选择 Allocator 一样选择合适的替换方式。

vtable 及其适用场景

vtable 模式是 Zig 中最接近其他 OOP 语言的实现接口的方式,Zig 中的 Allocator、Writer 等内置接口的实现都采用了 vtable 模式。这个模式由以下部分组成:

  • 虚拟函数表:通常命名为 vtable,类型是一个仅包含函数指针成员的 struct
  • 函数调用对象:类型通常是 *anyopaque,表示指向任意类型的指针

std.mem.Allocator 接口为例,其定义如下:

/// The type erased pointer to the allocator implementation.
///
/// Any comparison of this field may result in illegal behavior, since it may
/// be set to `undefined` in cases where the allocator implementation does not
/// have any associated state.
ptr: *anyopaque,
vtable: *const VTable,

pub const VTable = struct {
    alloc: *const fn (*anyopaque, len: usize, alignment: Alignment, ret_addr: usize) ?[*]u8,
    resize: *const fn (*anyopaque, memory: []u8, alignment: Alignment, new_len: usize, ret_addr: usize) bool,
    remap: *const fn (*anyopaque, memory: []u8, alignment: Alignment, new_len: usize, ret_addr: usize) ?[*]u8,
    free: *const fn (*anyopaque, memory: []u8, alignment: Alignment, ret_addr: usize) void,
};

要实现新的 Allocator 接口,只要实现 VTable 中定义的 4 个函数即可。如果我们的新实现需要访问内部状态,则可以将内部状态数据结构作为 *anyopaque 保存到 ptr 字段。这样在 VTable 函数中就可以通过下面的方式来访问:

fn alloc(context: *anyopaque, len: usize, alignment: std.mem.Alignment, return_address: usize) ?[*]u8 {
    const self: *MyAllocator = @ptrCast(@alignCast(context));
    // bla bla bla
}

当我们需要构造 MyAllocator 的实例并将其作为 Allocator 接口传递给消费者时,为 MyAllocator 添加一个 allocator方法:

// In MyAllocator struct
pub fn allocator(self: *MyAllocator) std.mem.Allocator {
    return .{
        .ptr = self,
        .vtable = &.{
            .alloc = alloc,
            .resize = resize,
            .remap = remap,
            .free = free,
        },
    };
}

// when using MyAllocator
var my_allocator = MyAllocator.init();
doSomeStuff(my_allocator.allocator());

从代码上看,这要比很多编程语言的 class MyAllocator : IAllocator 要繁琐太多了,但这也给我们解锁了一些新的灵活性,比如我们可以为接口本身添加成员方法,Allocator 接口就是通过这种方式对外提供了 VTable 之外的丰富的函数。 这样的接口几乎就是一个没有成员字段的抽象类了

除了接口方法之外,我们还可以在接口 struct 上定义一些所有实现都会用到的数据字段,这样该接口的实现就不用重复声明这些数据结构了。std.Io.Writer 就是这么一个典型的例子,因为现实的代码中几乎只用有缓冲的 Io ,所以 Writer 接口内部就提供了用于存储缓冲数据的字段,具体的 Writer 实现就不需要各自实现缓冲特性了。

anytype 替换

总的来说,VTable 模式是非常符合 OOP 直觉的类型替换方式,但它的语法确实有些过于复杂以至于看起来不那么优雅了。如果我们只是需要在一个函数中根据场景的调用不同的实现,那么可以试试用 anytype 来替换。

想象这么一个场景,我们需要测试一个业务函数在半夜的行为,那么在编写测试的时候我们当然不能等到半夜在去执行,通常的做法是提供一个 TimeProvider 抽象,让具体的业务函数依赖这个 TimeProvider 类型获取当前时间:

pub fn doSomething(time_provider: TimeProvider, bla, bla, bla) void {
  // do some time aware stuff
  if (isNight(time_provider.now())) {
    // do some thing at night.
  }
}

借助 anytype, 我们可以省掉 TimeProvider定义:

pub const RealTimeProvider = struct {
  pub fn now(self: @This()) Timestamp {
    return self.realtime();
  }
  // other fields and methods omitted.
}
pub const MockTimeProvider = struct {
  timestamp: Timestamp,
  
  pub fn now(self: @This()) Timestampe {
    return self.timestamp;
  }
}

pub fn doSomething(time_provider: anytype, bla, bla, bla) void {
  // do some time aware stuff
  if (isNight(time_provider.now())) {
    // do some thing at night.
  }
}

test {
  var mock_time = MockTimeProvider{ .timestamp = 42 };
  doSomething(mock_time);
}

pub fn doOtherThing() {
  var time_provider = RealTimeProvider.init();
  doSomething(time_provider);
}

anytype 是一个 comptime 特性,这意味着调用 doSomething 时的参数类型必须在编译期确定,而不是像 VTable 一样可以在运行时切换。在编译时,Zig 编译器会检查传给 doSomething 的值是否提供了一个 now 函数满足 doSomething 对其函数签名的要求。

另外,anytype 只能用作函数的参数类型,而不能用于 struct 字段类型:

const MyStruct = {
  time_provider: anytype, // won't compile
}

comptime type 替换

作为 anytype 的替代,我们可以使用 comptime type 参数让我们可以在 struct 中存储编译期确定的任意类型:

pub fn MyStruct (comptime T: type) type {
  return struct {
    time_provider: T,
    
    pub fn printNow(self: @This()) void {
      std.debug.print("{a}", self.time_provider.now());
    }
  };
}

const MyStructWithRealTime = MyStruct(RealTimeProvider);
const MyStructWithMockTime = MyStruct(MockTimeProvider);

使用 comptime type 会把我们的类型变成一个高阶类型,只有在传入特定的实现 T 之后才能用来创建实例。

跟其他 comptime 特性类似,Zig 只有在编译过程中才能确定 T 的类型信息,这也就意味着目前的 LSP 工具没法提供关于 T 的任何信息。当我们敲下 self.time_provider. 的时候,IDE 无法给出任何补全提示。

除此之外,使用高阶类型还会带来另一个限制:引用了 MyStruct 的类型要么其本身是高阶类型要么必须决定 T 的实现。

fn A (comptime T: type) type {
  return struct {
    my_struct: MyStruct(T),
  };
}
// or
const A = struct {
  my_struct: MyStruct(RealTimeProvider),
}

对于数据容器来说,这个特性不算什么特别明显的限制。但是在使用依赖注入模式的业务代码中, 高阶类型的参数 T 可替换性就像传染病一样——所有要保持 T 可被替代的类型其本身也必须是高阶类型。

传染在 union(enum) 处终止

如果我们可以在某一处确定 T 的全部实现,那么我们就可以借助 union(enum) 类型来终止高阶类型的传染:

pub const TimeProvider = union(enum) {
  real: RealTimeProvider,
  mock: MockTimeProvider,
  
  pub fn now(self: @This()) Timestamp {
    return switch (self) {
      inline else => |impl| impl.now()
    };
  }
}

这里我们就像使用 vtable 一样构造了引用具体实现的 Wrapper (TimeProvider),跟 vtable 不同的是我们会用 union(enum) 列举全部的可用实现,并且虚函数表也变成 union 类型的一个个成员方法。

使用这个模式的好处显而易见,TimeProvider 不再是高阶类型,他可以被简单地使用。我们还避免了 *anyopaqueanytypecomptime T: type 这些让 LSP 难以分析特性,开发体验也好上不少。但这样做的代价也很明显,如果我们要添加新的 TimeProvider 实现,我们就必须要修改 TimeProvider 本身。

选择一个类型替换模式

  • 如果需要由第三方库实现你的接口,请使用 vtable 模式。
  • 如果需要比较好的开发体验,可以考虑 union(eunm)模式。
  • 如果只在很小范围的函数参数中需要使用类型替换,将依赖声明为 anytype 就足够了。