Ziglings 笔记 70: 编译期鸭子类型 (Duck Typing)

只要你会叫,就是好鸭子

在 Ziglings 的第 70 个练习中,我们探索了 Duck Typing (鸭子类型)

在 C++ 中,我们通常使用模板和 Concepts 来约束类型;在 Java 中,我们使用 Interface。 而在 Zig 中,我们有一种更直接的方法:直接在编译期询问类型“你有这个方法吗?”。

挑战:真鸭、假鸭和水管

我们有三个结构体:

  1. Duck:真鸭子(有 waddlequack)。
  2. RubberDuck:橡皮鸭(也有 waddlequack)。
  3. Duct:通风管道(只有 connect)。

我们需要编写一个通用的 isADuck 函数,它能识别出前两个是“鸭子”,而排除掉水管。

解决方案

使用 @hasDecl 内置函数来检查方法是否存在:

const print = @import("std").debug.print;

// ... 结构体定义 Duck, RubberDuck, Duct 省略 ...

pub fn main() void {
    const ducky1 = Duck{ .eggs = 0, .loudness = 3 };
    const ducky2 = RubberDuck{ .in_bath = false };
    const ducky3 = Duct{ .diameter = 17, .length = 165, .galvanized = true };

    // anytype 使得该函数可以接收任意类型的参数
    print("ducky1: {}, ", .{isADuck(ducky1)}); // true
    print("ducky2: {}, ", .{isADuck(ducky2)}); // true
    print("ducky3: {}\n", .{isADuck(ducky3)}); // false
}

fn isADuck(possible_duck: anytype) bool {
    // 1. 获取传入参数的类型
    const MyType = @TypeOf(possible_duck);

    // 2. 检查是否有 "waddle" 声明
    const walks_like_duck = @hasDecl(MyType, "waddle");
    
    // 3. 检查是否有 "quack" 声明
    const quacks_like_duck = @hasDecl(MyType, "quack");

    // 综合判断
    const is_duck = walks_like_duck and quacks_like_duck;

    // 这一步至关重要!
    // 如果 is_duck 为 false,编译器会直接丢弃这个 if 块。
    // 所以即使 Duct 没有 quack() 方法,这里也不会报错。
    if (is_duck) {
        possible_duck.quack();
    }

    return is_duck;
}

核心知识点总结

1. 什么是 anytype

它是 Zig 泛型系统的另一半(另一半是 comptime T: type)。

  • comptime T: type 用于显式传递类型。
  • anytype 用于隐式传递值的类型。它让函数变成了模板函数。

2. 编译期的 if

Zig 的编译器非常聪明。如果 if 的条件是编译期已知的常量(comptime_int, bool 等),编译器在生成机器码时会只保留正确的分支。 这使得我们可以写出这样的逻辑:“如果是鸭子,就调用 quack;如果不是,就什么都不做”。这在 C 语言中是很难实现的(通常需要宏魔法)。

3. 没有 Interface 关键字?

Zig 没有 interface 关键字。我们通过文档约定或者像这样编写 helper 函数(isADuck)来检查类型是否符合要求。这种隐式的接口实现方式更加灵活,减少了代码的耦合度。


后续预告:我们的 Zig 语法基础之旅即将结束!接下来的练习将把目光投向标准库和实际应用,比如文件操作、内存分配器等。但在此之前,还有一个关于包管理的小练习 072_comptime6.zig。准备好迎接最后的挑战了吗?