首页 > 其他分享 >在 Zig 中实现接口

在 Zig 中实现接口

时间:2024-05-19 13:51:44浏览次数:26  
标签:const u8 实现 接口 Zig value Impl foo fn

在 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,
        },
    };
}

标签:const,u8,实现,接口,Zig,value,Impl,foo,fn
From: https://www.cnblogs.com/violeshnv/p/18200280

相关文章

  • Python 实现任意多边形的最大内切圆算法_任意多边形最大内切圆算法
    CSDN搬家失败,手动导出markdown后再导入博客园参考Matlab计算轮廓内切圆初衷是为了求裂缝的最大宽度![[output/attachments/5ecf17abcb54aaa4fb35b00c3f243f32_MD5.png]]直接上代码importrandomimportcv2importmathimportnumpyasnpfromnumpy.maimportcos,......
  • Obsidian 使用 ddnsto 穿透 nas 的 webdav 功能实现跨平台同步_obsidian nas 同步
    CSDN搬家失败,手动导出markdown后再导入博客园之前一直用坚果云的webdav功能做obsidian的跨平台同步(Windows,Ubuntu,iOS),但是今天在新的工作机上部署obsidian时,发现一次同步的文件数量超过了坚果云的限制(付费用户好像是500次),因此想换个平台来考虑。备选方案一个是阿里云的......
  • 手写Word2vec算法实现
    1.语料下载:https://dumps.wikimedia.org/zhwiki/latest/zhwiki-latest-pages-articles.xml.bz2【中文维基百科语料】2.语料处理(1)提取数据集的文本下载的数据集无法直接使用,需要提取出文本信息。安装python库:pipinstallnumpypipinstallscipypipinstallgensimp......
  • 《user-agent(UA)识别 Api 接口助力智能应用开发》
     在现代智能应用的开发中,往往需要对用户的设备和浏览器进行识别,以便适配不同的操作系统和浏览器。而user-agent是一种非常重要的信息,它包含了用户设备、操作系统和浏览器的相关信息。在本文中,我们将介绍一个强大的user-agent识别API接口,它可以帮助开发者轻松实现用户设备和浏......
  • 在 ASP.NET Core 中使用托管服务实现后台任务
    在ASP.NETCore中,后台任务作为托管服务实现。托管服务是一个类,具有实现 IHostedService 接口的后台任务逻辑。本文提供了三个托管服务示例:在计时器上运行的后台任务。激活有作用域的服务的托管服务。有作用域的服务可使用依赖项注入(DI)。按顺序运行的已排队后台任务......
  • CERIO-DT系列路由器Save.cgi接口存在命令执行漏洞
    漏洞描述:由于未经过过滤和适当限制的情况下,传入的参数直接用于构建并执行系统命令,攻击者通过将恶意命令注入到"Save.cgi"接口的请求参数中可以执行任意命令。Fofa:title="DT-100G-N"||title="DT-300N"||title="DT-100G"||title="AMR-3204G"||title="WMR-200N"POC:PO......
  • 利用Burpsuite爆破带有验证码web登录接口
    工具下载地址https://github.com/f0ng/captcha-killer-modified该工具下的验证码识别python脚本要求python环境小于3.10.0安装验证码识别python脚本引用的库pipinstall-ihttp://mirrors.aliyun.com/pypi/simple/--trusted-hostmirrors.aliyun.comddddocraiohttp加载工......
  • CERIO-DT系列路由器Save.cgi接口存在命令执行漏洞
    漏洞描述:由于未经过过滤和适当限制的情况下,传入的参数直接用于构建并执行系统命令,攻击者通过将恶意命令注入到"Save.cgi"接口的请求参数中可以执行任意命令。Fofa:title="DT-100G-N"||title="DT-300N"||title="DT-100G"||title="AMR-3204G"||title="WMR-200N"POC:PO......
  • 贝叶斯推断架构实现
    贝叶斯推断基础贝叶斯方法提出了一个概率框架来描述如何将新的观察结果整合到决策过程中。传统的贝叶斯推断的二进制算术结构中,后验概率的计算需要大量的乘、除、加。先验概率(由历史求因):根据以往经验和分析得到的概率,观测数据前某一不确定量的先验概率分布,通常指模型的参数\(\th......
  • 雷电接口史诗级强化!一根线完成2台电脑协同应用
    作为目前性能最强、功能最为齐全的PC接口,Thunderbolt也就是雷电接口有着非常出色的使用体验。不过,因为相关设备价格略贵,且大多数用户还是习惯于使用传统USBType-A接口的设备,所以雷电接口最近几年虽然一直是笔记本电脑的标配接口,但并不像USB-A那么为人所熟知。而现在,英特尔针对......