在 Zig 中实现接口

在 Zig 中实现接口

实现接口的关键是遵循特定的函数调用约定,包含接口通常由函数原型和定义组成,表示了一组要被实现的方法或行为。实现接口的关键在于确保函数的参数类型和返回值等方面与接口定义一致。

  1. 抽象:可以通过使用函数指针、结构体和指针、泛型等技术来实现。抽象的目的是将具体的实现细节与接口定义分离开来,以便于在不同的实现之间切换或扩展功能。
  2. 约束:可以通过定义一组接口函数或方法以及相应的参数和返回类型来实现。接口的实现需要满足一定的约束条件,以保证接口的一致性和可互换性。

现在有针对一个函数 foo() 有两个实现 Impl1Impl2,讨论它们的接口实现。(对于需要可变类型的接口情况,通常将 this 的类型改为 this: *This 就可以了)。

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

const Impl1 = struct {
    a: []const u8,
    const This = @This();
    fn foo(this: This, value: u8) u32 {
        return value + (std.fmt.parseInt(u32, this.a, 10) catch unreachable);
    }
};

const Impl2 = struct {
    b: u64,
    const This = @This();
    fn foo(this: This, value: u8) u32 {
        _ = value;
        return @intCast(this.b);
    }
};

使用泛型实现

鸭子类型

在 Zig 中,我们可以使用泛型函数和 anytype 类型来实现鸭子类型的接口。

fn foo(impl: anytype, value: u8) u32 {
    return impl.foo(value);
}

test "duck" {
    const impl1 = Impl1{ .a = "123" };
    const impl2 = Impl2{ .b = 456 };
    try testing.expectEqual(128, foo(impl1, 5));
    // 实例引用还是实例指针不影响,因为 Zig 会隐式解引用指针
    try testing.expectEqual(456, foo(&impl2, 5));
}

优点:

  • 简单易懂

缺点:

  • anytype 范围太大,对代码提示不友好
  • impl 的限制太少,对调用者不友好
  • 是函数级别的接口,不是类级别的接口

以下两种方法是对鸭子类型的改进。

转发

在转发方法中,我们可以创建一个包含实例指针的接口结构体,并将函数调用转发到具体实例。

fn Interface(comptime Impl: type) type {
    return struct {
        impl: Impl,
        const This = @This();
        fn foo(this: This, value: u8) u32 {
            // 为实例引用和实例指针提供统一转发
            return switch (@typeInfo(Impl)) {
                // Impl 为指针类型
                .Pointer => |p| p.child.foo(this.impl.*, value),
                inline else => Impl.foo(this.impl, value),
            };
        }
    };
}

fn interface(impl: anytype) Interface(@TypeOf(impl)) {
    return Interface(@TypeOf(impl)){ .impl = impl };
}

test "fn (type) type" {
    // 接口包含实例
    const interface1 = interface(Impl1{ .a = "123" });
    // 接口包含实例指针
    const impl2 = Impl2{ .b = 456 };
    const interface2 = interface(&impl2);
    try testing.expectEqual(128, interface1.foo(5));
    try testing.expectEqual(456, interface2.foo(5));
}

优点:

  • 通过反射判断类型,可以包含 Impl 实例,也可以包含 *Impl 指针
  • 提供更好的代码提示

缺点:

  • 对每个 Impl 都对应一个 Interface,即 Interface(Impl1)Interface(Impl2) 是不同类型。这是基于泛型的接口共有的缺点。

实例 std.io.SeekableStream

pub fn SeekableStream(
    comptime Context: type,
    comptime SeekErrorType: type,
    comptime GetSeekPosErrorType: type,
    comptime seekToFn: fn (context: Context, pos: u64) SeekErrorType!void,
    comptime seekByFn: fn (context: Context, pos: i64) SeekErrorType!void,
    comptime getPosFn: fn (context: Context) GetSeekPosErrorType!u64,
    comptime getEndPosFn: fn (context: Context) GetSeekPosErrorType!u64,
) type {...}

此处将需要转发的函数直接写在参数中,比较繁琐,但相对的可以使用不在类内定义的函数。

Trait

Trait 是一种在编译时通过反射将接口类型的函数等效于实际类型的函数的方法。其中 Trait 是只包含函数类型作为成员的结构体。

fn Trait(comptime Impl: type) type {
    return struct {
        fn foo(_: Impl, value: u8) u32 {
            return value;
        }
        foo: @TypeOf(foo),
        // 没有默认实现时,直接给出类型
        // foo: fn (impl: Impl, value: u8) u32,
    };
}

fn Interface(comptime trait: fn (type) type, comptime Impl: type) t: {
    const T = trait(Impl);
    var info = @typeInfo(T).Struct;

    // [0..].* 用于快速创造数组
    // "hello"[0..].* 等价于 [_:0]u8{ 'h', 'e', 'l', 'l', 'o' };
    var fields = info.fields[0..].*;
    for (&fields) |*f| {
        // 在 Impl, T 中搜索函数
        if (@hasDecl(Impl, f.name)) {
            f.default_value = @field(Impl, f.name);
        } else if (@hasDecl(T, f.name)) {
            f.default_value = @field(T, f.name);
        } else if (f.default_value == null) {
            @compileError("trait match failed");
        }
    }

    info.fields = &fields;
    break :t @Type(std.builtin.Type{ .Struct = info });
} {
    return .{};
}

// 没有定义 foo()
const Impl3 = struct {};

test "trait" {
    const impl1 = Impl1{ .a = "123" };
    const impl2 = Impl2{ .b = 456 };
    const Interface1 = Interface(Trait, Impl1);
    const Interface2 = Interface(Trait, Impl2);
    const Interface3 = Interface(Trait, Impl3);
    try testing.expectEqual(128, Interface1.foo(impl1, 5));
    try testing.expectEqual(456, Interface2.foo(impl2, 5));
    try testing.expectEqual(5, Interface3.foo(undefined, 5));
}

优点:

  • 不包含实例指针
  • 接口创建时会检查 Trait 的所有函数是否被满足
  • 可以提供默认实现

缺点:

  • 使用 @Type 创建接口类型,影响代码提示
  • 调用函数时要将实例也作为参数(因为没有实例指针)

以上方法之间的界限并不分明,比如 Trait 可以认为是编译期的转发;转发也可以删去实例指针 impl,然后在调用时加入实例参数,或者修改转发函数提供默认实现。

使用指针实现

使用指针实现接口,接口的类型是固定的,可以嵌入到其他结构体中。而将泛型的接口作为成员可能需要再加上一层泛型,将泛型的影响扩大。

const PointerBased = struct { interface: PointerBasedInterface };

fn GenericBased(comptime GenericBasedInterface: type) type {
    return struct { interface: GenericBasedInterface };
}

union(enum)

const Interface = union(enum) {
    impl1: *Impl1,
    impl2: *Impl2,
    const This = @This();
    fn foo(this: This, value: u8) u32 {
        return switch (this) {
            // 对于每个实例 t 是不同类型的,为了捕获它,需要 inline else
            // 并且添加更多 Impl 时不需要修改函数
            inline else => |t| t.foo(value),
            // 等价于
            // .impl1 => |t1| t1.foo(value),
            // .impl2 => |t2| t2.foo(value),
            // // ...
        };
    }
};

test "union(enum)" {
    var impl1 = Impl1{ .a = "123" };
    var impl2 = Impl2{ .b = 456 };
    const interface1 = Interface{ .impl1 = &impl1 };
    const interface2 = Interface{ .impl2 = &impl2 };
    try testing.expectEqual(128, interface1.foo(5));
    try testing.expectEqual(456, interface2.foo(5));
}

优点:

  • 简单
  • 这是比较符合 Zig 的写法,没有特定需求的情况下应该优先选择这种方法

缺点:

  • 必须知道所有可能的实现才能构建 union(enum)

虚函数表

自拟虚拟函数表和实例指针。

const Impl1 = struct {
    a: []const u8,
    const This = @This();
    fn foo(ctx: *anyopaque, value: u8) u32 {
        const this: *This = @alignCast(@ptrCast(ctx));
        return value + (std.fmt.parseInt(u32, this.a, 10) catch unreachable);
    }
};

const Impl2 = struct {
    b: u64,
    const This = @This();
    fn foo(ctx: *anyopaque, value: u8) u32 {
        const this: *This = @ptrCast(@alignCast(ctx));
        _ = value;
        return @intCast(this.b);
    }
};

const Interface = struct {
    ptr: *anyopaque,
    vtable: *const struct {
        foo: *const fn (ctx: *anyopaque, value: u8) u32,
    },

    const This = @This();
    fn foo(this: This, value: u8) u32 {
        return this.vtable.foo(this.ptr, value);
    }
};

fn interface(comptime T: type, ctx: *T) Interface {
    return .{
        .ptr = ctx,
        .vtable = &.{
            .foo = T.foo,
        },
    };
}

test "VTABLE" {
    var impl1 = Impl1{ .a = "123" };
    var impl2 = Impl2{ .b = 456 };
    const interface1 = interface(Impl1, &impl1);
    const interface2 = interface(Impl2, &impl2);
    try testing.expectEqual(128, interface1.foo(5));
    try testing.expectEqual(456, interface2.foo(5));
}

优点:

  • 自拟虚函数表,也就是实现了虚函数,最灵活

缺点:

  • 每个 Impl 需要将接口的实现的第一个参数修改为任意类型 *anyopaque(其他方法对实现没有要求)。

实例 std.mem.Allocatorstd.heap.GeneralPurposeAllocator

ptr: *anyopaque,
vtable: *const VTable,

pub const VTable = struct {
    alloc: *const fn (ctx: *anyopaque,
                      len: usize, ptr_align: u8, ret_addr: usize) ?[*]u8,
    resize: *const fn (ctx: *anyopaque,
                       buf: []u8, buf_align: u8, new_len: usize, ret_addr: usize) bool,
    free: *const fn (ctx: *anyopaque,
                     buf: []u8, buf_align: u8, ret_addr: usize) void,
};
pub fn allocator(self: *Self) Allocator {
    return .{
        .ptr = self,
        .vtable = &.{
            .alloc = alloc,
            .resize = resize,
            .free = free,
        },
    };
}
posted @ 2024-05-19 13:51  Violeshnv  阅读(104)  评论(0编辑  收藏  举报