IBM System/360,大约拍摄于1965年的宣传照片
在编程语言不断演变的领域中,出现了新的竞争者:Zig。
它正在引起一波热潮,尤其是在系统编程领域,凭借其独特的性能、安全和简洁性。作为现代 C 和 C++ 的替代品而设计,它提供了无缝的 C 互操作性,并且具备精确的内存控制,同时也非常适合新手,甚至可以取代 Rust 或 Go,成为强有力的选择。
咱们瞧一瞧!
Zig简史需要说明的是,我不是系统编程专家。
我喜欢 Rust 的功能,过去曾试用过它,并读了几本书。
Go 语言是我解决较小工具项目的首选(双关语:go-to 的意思和 Go 语言相关),这些项目超出了 shell 脚本所能处理的范围,我用它编写过多个工具。
这篇文章反映了我作为主要使用 Java 和 Swift 等托管语言的开发者的看法,对系统编程的理解。
Zig 是由 Andrew Kelly 创建的,其首次公开发布于 2016 年。
零号,齐瓦纳(CC BY 4.0 齐格基金会)
与其它系统语言相比,它比其他类似的目标语言领先几年时间。比如,Rust 于 2012 年首次亮相,而 Golang 则是在 2009 年首次亮相。
尽管 Zig 还很年轻且竞争激烈,它已经迅速找到自己的位置。目前,使用 Zig 的两个最知名的项目分别是 JavaScript 运行时 Bun(Bun),以及最近推出的跨平台终端模拟器 Ghostty(Ghostty)。
短短几年内,Zig 已经引起了广泛关注。它吸引了那些在软件开发中重视控制、安全和简洁性的开发者。
趣味事实:名字“Zig”似乎是通过一个随机生成“Z”开头的4字母单词的脚本选出来的。不过,我没有找到它如何变成3个字母的信息。可能“Zigg”这样的词引起了创作者的注意,但为了避免重复字母,他们选择了“Zig”。
Zig 是什么?它是一个开源项目。本质上,Zig 是一种通用的系统编程语言。
该语言诞生于希望打造一个更实用的语言,更像C语言的愿望。其设计思想侧重于在性能、安全性和健壮性与简易性及易用性之间找到一个平衡[更实用的语言,如C语言] (链接)。
- 实用主义:
确保新手和有经验的开发者都能轻松上手,开发者为中心的简洁性,没有不必要的复杂性,只有极简的语法。 - 最优性:
通过编写最“自然”和最易表达的代码,实现顶级的运行时表现。 - 健壮性:
最优性是首要的,但安全性紧随其后,同时保证高效率不受损失。开发者可以完全控制关键性能特性,比如手动内存管理。 - 透明性:
Zig 在其代码中提倡 明确性,避免遮蔽程序行为的隐藏控制流。例如,没有隐藏的分配,所有内存管理都是显式的,也没有隐式类型转换。
有几个特别突出的功能,例如它优秀的 C 互操作性和 comptime
,稍后我们会详细讨论这些特性,以及其他现代语言常见的特性,比如集成工具和简单的跨平台编译。
这里有个常见的‘Hello, World’:
const std = @import("std");
pub fn main() void {
const lang = "Zig";
std.debug.print("Hello, {s} World!\n", .{lang});
// 这段代码使用Zig语言打印 "Hello, Zig World!"
}
即使不了解这种语言的细微之处,如果你之前用过类似C语言的,语法也会感觉很亲切。
主要特点让我们深入讨论一下语言的关键特性,以便更好地展示Zig的那些好处。
没有隐藏的流程在Zig中,没有隐含的控制流是一个很重要的原则。
其他语言中有许多特性会使代码行为难以预测,或使实际的数据流动更难以看清,从而使代码更难分析:
- 隐式类型转换/强制类型转换:
自动的数据类型转换可能导致意外的行为,比如精度损失、转换错误或溢出,这些都是不允许的。只允许安全且无歧义的类型强制转换。 - 操作符重载:
重载标准操作符虽然可以提供很多功能,但会改变默认行为,并可能导致意外的函数调用或副作用。 - 异常和隐式错误处理:
异常是强大的工具,但它们会模糊控制流程,使得函数在代码中不明显的地方提前退出。 - 自动内存管理:
垃圾回收和“隐式”分配是将复杂性转移给运行时的安全特性。然而,这也引入了不确定性和性能成本。 - 隐式异步:
像异步/等待这样的特性可以隐藏实际的执行流程,使得在调试时很难推理操作序列。
正如你猜到的,Zig 通过避免隐藏的控制流来应对这些问题的担忧。
无隐式类型转换/强制类型转换将值转换为另一种类型叫做类型转换。很多语言会自动转换一些特定的类型(比如基本类型),不过 Zig 却不是这样。
相反,如果转换已知是安全且无歧义的,例如对于常量值,它会使用 类型强制。
数据类型得手动转换:
const std = @import("std");
pub fn main() void {
const a: f32 = 23.0;
const b: i32 = 42;
// 安全转换,无精度损失
const sum: u8 = a + b;
std.debug.print("结果为: {}\n", .{sum});
const x: f32 = 23.5;
const y: i32 = 42;
// 不安全转换,有精度损失
const result: u8 = x + y;
std.debug.print("结果为: {}\n", .{result});
}
// 编译出错
// src/main.zig:16:26: 错误:因浮点值 '65.5' 有分数部分而无法转换为类型 'u8'
// const result: u8 = x + y;
//
显式类型转换通过内置函数(如[@intFromFloat](https://ziglang.org/documentation/master/#intFromFloat)
等)来实现,当无法进行安全类型转换时,就会用到这些函数(更多内置函数信息可以参考此链接)。
为了提高透明度和使控制流程更明显,这里没有操作符重载.
我对此有点矛盾,因为重载运算符可以创建有趣的DSL,比如 Swift 中的自定义操作符(在Swift中)。但我完全理解为什么没有包含这一点,因为它会干扰代码的可读性,并且需要了解不同类型之间如何通过重载运算符相互作用。
清晰的错误处理机制尽量避免异常;错误作为函数签名的一部分。使用错误联合体和try
及catch
关键字来处理错误,使错误处理成为逻辑的一部分。
Zig 中的错误以一种特殊的带有 !
前缀的联合类型返回,这种类型明确表明一个函数可能返回一个正常值或错误。
// 定义可用的错误类型
const Errors = error {
DivideByZero,
// ...
};
fn 分割(a: u8, b: u8) !u8 {
// a 和 b 是两个8位无符号整数
if (b == 0) {
// 如果 b 为0
return Errors.DivideByZero;
// 返回除零错误
}
// 返回 a 除以 b 的结果
return a / b;
}
现在没有像 Java 或 Swift 中那样的 throw
,它会绕过正常的控制流程并提前退出当前函数。相反,通过同一个关键字 return
返回,要么是错误,要么是有效值。与 Golang 不同,这里不需要使用元组。
因为错误处理是显式的,所以有两种应对方式。
- 使用
try
调用divide
,并要求调用它的函数在签名中添加错误处理。 - 使用
catch
来处理可能发生的错误。
第一个选项承认错误并要求调用者在调用栈中的调用者处理错误。
const std = @import("std");
pub fn main() void {
// 使用 `try` 将错误向上抛出栈
const result = try divide(42, 23);
std.debug.print("Result: {}\n", .{result});
}
首先,这个代码编译不过
src/main.zig:6:20: 编译器错误:期望类型是 'void',但找到 '@typeInfo(@typeInfo(@TypeOf(main.divide)).@"fn".return_type.?).error_union.error_set'
const result = try divide(42, 23);
^~~~~~~~~~~~~~~~~~~~~~
src/main.zig:3:15: 提示:此函数不允许返回错误
pub fn main() void { // 这是Zig语言的函数定义
即使 main
函数必须使用 !void
返回类型来声明可能的错误,如果不处理这些错误。问题在于,没有比 main
更上层的函数来处理这些错误,所以我们的应用程序会崩溃。
那,咱们就搞定这个!
const std = @import("std");
pub fn main() void {
// 使用 `catch` 来捕捉错误
const result = divide(23, 0) catch |err| {
std.debug.print("捕获错误: {}\n", .{err});
return;
};
std.debug.print("结果是: {}\n", .{result});
}
由于错误处理直接进行,不需要用 try
;所以也就不用 !void
来声明可能抛出的错误。
catch
子句要么捕获有效结果,要么并通过 |<变量名>|
赋值将错误传递给处理程序块。处理程序块需要根据签名返回一个值,在这种情况下,处理程序块因为是 void
类型,所以不需要返回任何值。
switch
也可以更优雅地处理多个错误,例如在 HTTP 处理程序中。
self.handle(action, req, res) catch |err| switch (err) {
HttpError.NotFound => {
res.status = 404;
res.body = "未找到该资源";
res.write() catch return false;
},
_ => self.handleOtherErrors(req, res, err),
}
Zig 错误处理的方式很符合它的整体目标。
- 透明性:
在函数类型签名中明确列出所有可能的错误路径。 - 可预测性:
不会有隐藏的控制流程;每个潜在的错误都必须明确处理。 - 安全性:
提倡全面的错误处理,减少未处理错误导致程序崩溃的风险。
对于像我这样来自使用“传统”异常语言的开发者来说,Zig的错误联合体类型这种设计一开始可能会觉得有点奇怪。然而,它们的冗长性和强制性带来了更大的清晰度,使得系统更加健壮、易于维护且高度可靠。
手动内存管理Zig 提供了对堆内存分配和释放的完全掌控,以确保内存管理既高效又可预测。
与 C 和 C++ 不同的是,Zig 引入了分配器(allocators),这些分配器被明确传递给函数以避免隐式的内存操作。分配操作与 defer 模式配合使用,以确保内存清理工作。
const std = @import("std");
pub fn main() !void {
// 获取分配内存
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
// 传递分配器并在完成后清理
const array = try createData(allocator, 10);
defer allocator.free(array);
// 打印数组数据
for (0.., array) |idx, value| {
std.debug.print("arry[nhkv1sx5v02o]=nhkv1sx5v02o\n", .{ idx, value });
}
}
fn createData(allocator: std.mem.Allocator, count: usize) ![]u32 {
// 使用给定的分配器分配足够的空间
var array = try allocator.alloc(u32, count);
// 初始化数据
for (0.., array) |idx, _| {
array[idx] = @intCast(idx * idx); // 将索引平方转换为整数
}
返回数组
}
传递分配器是一个关键原则,您可以在 stdlib
中看到,并且您也应该在您的代码中采用这个传递分配器。
为什么不直接在需要时再创建一个分配器对象呢?
和 Zig 中的其他事物一样,这也是因为同样的原因,在大多数情况下。
- 透明性:
无隐藏的内存分配,使得资源使用更加高效、可预测且更易于理解。 - 灵活性:
调用者可以根据需求选择最合适的分配器。有多种分配器可选,甚至可以自行创建。
通过重视显式的内存管理,Zig 消除了其他不太严格的语言中常见的隐式内存操作陷阱。这可能一开始会让人觉得有些繁琐,但提供了完全的控制和灵活性的好处。这虽然一开始可能会让人觉得繁琐,但最终能提供完全的控制和灵活性。
可选库另一个让Zig成为嵌入式或资源有限的环境优秀选择的突出特性是,标准库完全可以根据需要选择是否使用,这意味着用户可以根据需求选择是否使用它。
与大多数现代语言不同,如果你不想使用stdlib
,可以完全不用它,所以它也不会被打包进二进制文件里。
可选的部分还不止这些,libc
也是 Zig 的一个可选项。这在一些环境中特别有用,比如低级系统内核、自定义运行时环境或直接运行在硬件上的程序。
这也是 Zig 明确性做法的又一个好例子。没有隐式的依赖,即使是 stdlib
中的代码,只要被用到,也会被包含在最终的二进制文件中,除此之外不再包含其他内容。这比其他语言,例如 Go 或 Rust,来说是一个很大的优势,因为它们的标准库与语言紧密集成,常常会附带你不使用或不需要的功能,导致默认生成的二进制文件更大。
Zig 把包含什么的细粒度控制权交给开发者,从而使开发者能够灵活且有意识地做出决定。
这也使得Zig能够为WebAssembly提供小巧且加载快速的模块。
集成的构建系统探索一门语言能让我们觉得既有趣又高效。
例如,JavaScript 不断变化甚至经常出现问题的工具,或者 Java 令人头疼的 Gradle 经历,都可能成为这些语言的包袱。
许多更新的语言试图通过包含自己的工具和依赖管理来简化这种混乱,并提供一个默认的选择,从而使整体体验更加流畅。
Rust 有 [cargo](https://doc.rust-lang.org/cargo/)
,Go 有其内置的模块、格式化和测试功能在 [go](https://go.dev/doc/cmd)
命令 中,甚至是现代的 JavaScript/TypeScript 方法如 Deno 也提供了处理项目即时需求的所有工具。所以任何现代语言都需要提供让开发者更容易操作的功能,而 Zig 也没有让人失望。
Zig 自带一个包管理器和构建系统,旨在简化依赖管理和跨平台构建过程。这些工具与语言高度集成,为构建、测试和编译提供了一流的支持,让开发更加便捷。
构建脚本是用 Zig 语言本身编写的,默认存放在名为 build.zig 的文件里。
const std = @import("std");
pub fn build(b: *std.Build) void {
const exe = b.addExecutable(.{
.name = "my-app",
.root_source_file = b.path("src/main.zig"),
.target = b.host,
});
b.installArtifact(exe);
}
使用编程语言本身作为工具语言是一个巨大的优势,因为开发人员只需要学习一种语言就可以完成这两项工作。此外,这还提供了强类型支持,并使构建脚本易于验证。
运行构建脚本命令: zig build
一个最小化的配置其实并不需要构建命令,不过zig build-exe src/main.zig
。
也可以直接进行交叉编译,直接指定目标架构和操作类型。
`` zig 构建可执行文件 --target x86_64-linux-gnu src/main.zig ```
总体来说,构建系统确实很复杂,就像所有构建系统一样,但我们一开始就可以轻松上手。它的文档和语言参考是分开的。
流畅的C互操作性作为系统编程语言,C语言兼容性是必不可少的,大多数语言在互操作性上表现不一。Zig在这一方面则表现得尤为突出,其无缝性尤为显著。
使用 C 库很简单,只需包含头文件即可。
考虑以下 C 程序:
const stdio = @cImport({
@cInclude("stdio.h");
});
pub fn main() void {
// 注意:必须确认返回值
_ = stdio.printf("Hello from C and Zig!\n");
}
Zig 与 C 链接器集成在一起,确保编译时正确解析必要符号。
libc
或类似的库必须在构建过程中被链接,这样才能找到头文件。
C 互操作性双向进行,通过使用 export
关键字生成可以从 C 调用的函数非常简单。
export fn add(a: i32, b: i32): i32 {
return a + b;
}
// 在C中:
// extern int add(int x, int y); // C中的等价代码
编译成共享库: zig build-lib -dynamic add.zig
无缝的 C 互操作性使 Zig 成为一个出色工具,用于工作和现代化 C 项目,无论是通过 C 来使用这些项目,还是用 Zig 编写新代码,并让 C 来使用这些新代码。
- 无需使用额外的工具或绑定
- Zig 自动处理链接过程和符号解析过程,以及类型转换的复杂情况
- 可以逐步用 Zig 代码替换大型 C 项目的部分代码,用更安全、更易维护且更可预测的 Zig 代码
我们已经见过许多出色的功能,但 _comptime
是一个独特而强大的特性。它可以在编译时运行。
与传统的语言不同,Zig 使用了一个两级编译过程,允许生成数据、执行计算或根据条件包含代码。
与许多大型系统不同,它不仅仅是文本替换,而是运行代码并将结果插入到二进制文件中。这种方法将计算从运行时转移到编译阶段,减少了运行时的开销,并提高了程序设计的灵活性。
首先,我们来创建一个查询表吧。
const std = @import("std");
// 生成编译时表。
const table = comptime 生成表();
fn 生成表() []u8 {
return [_]u8{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
}
pub fn 主函数() void {
std.debug.print("编译时生成的表:{any}\n", .{table});
}
结果内置到库中,消除了任何运行时计算过程。
comptime
不限于简单的用途,它可以用于任何所有变量都已确定的情况,例如存储递归计算的阶乘结果。
const std = @import("std");
fn 阶乘(n: u32) u32 {
return if (n == 0) 1 else n * 阶乘(n - 1);
}
pub fn main() void {
const result = comptime 阶乘(10); // 计算10的阶乘
std.debug.print("阶乘结果为: nhkv1sx5v02o\n", .{result});
}
添加类似 std.time.sleep
的代码会导致失败,因为 comptime
函数在编译时不再确定,编译器会告诉我们哪里出了问题以及为什么。
/home/ben/.zig/lib/std/os/linux.zig:1528:9: 错误:无法评估编译时常量表达式 @intFromPtr(request)
@intFromPtr(request),
^~~~~~~~~~~~~~~~~~~~
/home/ben/.zig/lib/std/os/linux.zig:1528:21: 注释:此操作数导致操作在运行时执行
@intFromPtr(request),
^~~~~~~
/home/ben/.zig/lib/std/Thread.zig:80:55: 注释:此处调用时在编译时常量位置
switch (linux.E.init(linux.clock_nanosleep(.MONOTONIC, .{ .ABSTIME = false }, &req, &rem))) {
~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/main.zig:4:19: 注释:此处调用时在编译时常量位置
std.time.sleep(@as(u64, 5) * std.time.ns_per_s); // 安全,但需要显式转换
~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/main.zig:10:38: 注释:此处调用时在编译时常量位置
const result = comptime factorial(10);
~~~~~~~~~^~~~
src/main.zig:10:20: 注释:'comptime' 关键字强制此处进行编译时常量评估
const result = comptime factorial(10);
^~~~~~~~~~~~~~~~~~~~~~
// ...SNIP...
另一个 comptime
的应用场景是根据条件决定的代码分支。
const std = @import("std");
const builtin = @import("builtin");
pub fn main() void {
if (comptime builtin.os.tag == .macos) {
// 此代码仅在目标操作系统为 macOS 时才会编译进去。
return;
}
std.debug.print("我肯定不在 macOS 上。\n", .{});
}
这优化了二进制代码,在编译之后,用于计算的代码段或未使用的条件分支代码不再包含在内。当然,不再需要运行时的计算。
其他语言也会这样做,比如,要么用文本宏,要么用多个文件。但直接将其包含在文件中则是一个不错的功能。
Zig、Rust 和 Go 对比Zig、Rust 和 Go 都是为了解决类似 C/C++ 这样的旧编程工具的局限性和不足的现代编程语言。虽然每种语言都有自己独特的理念、优势和劣势,但它们在性能、安全性、简洁性以及适用场景方面有着显著的不同。
不过,与其他人相比,Zig 在某些方面显得特别突出,因为它以全新或至少是新奇的方式前进。
在关键领域,情况如下:
性能Zig 强调显式的内存管理,具有完全控制内存的分配和释放。没有垃圾回收,这保证了性能的可预测性和低延迟。可以排除标准库甚至 libc
,使其非常适合最小的和性能关键的应用程序,特别是在资源受限的环境中。
Rust 通过零成本抽象提供高性能的同时,通过细粒度的内存安全(这要感谢借用检查器)确保内存安全。它的内存模型消除了未定义行为,而不会牺牲运行时性能,使其在安全关键系统中成为一个强有力的选择,同时仍然是一款高性能的选择。然而,借用检查器是一个复杂且学习曲线陡峭的机制。
Go 通过采用垃圾收集来简化程序并牺牲一些原始性能。自动管理内存可以极大地简化开发过程,特别是在对性能要求不是特别高的场景下。然而,它引入了不可预测的延迟,导致响应时间变得不可预知,这使得它在实时或对性能要求极高的系统中不如其他语言。
安全须知Zig 通过明确地控制错误处理和内存管理来促进安全性。所有控制流和内存分配都是显式的。许多隐性特性并未被使用。所有操作都需要明确知晓和同意。
Rust 因其严格的内存安全保证而著称,这归功于所有权和借用规则。这样,编译器和借用检查器可以在编译阶段阻止许多问题,比如数据竞争、释放后使用以及缓冲区溢出。然而,这些安全特性给初学者带来了不少复杂性,可能会让初学者感到望而却步。
Go 的主要特点在于其简洁性。它通过垃圾回收来保证基本的内存安全。与其他语言相比,仍然可能导致运行时故障并且更可能忽略一些问题。
简单Zig 更倾向于极简设计,避免了许多复杂的特性,比如宏、异步/等待、操作符重载等。其语法简洁明了,便于阅读和维护,同时也不牺牲性能。这使得它既适合新手也适合有经验的专业人士。
Rust 提供了丰富的功能集,包括生命周期、特征等特性,使其成为一种“强大而复杂”的语言。借用检查器和高级抽象为初学者尤其是新手创造了一条陡峭的学习曲线。
_Go_\ 语言是为简单和易用性设计的,重点在于提升开发者的生产力。为此牺牲了一些高级功能,比如手动内存管理、宏指令或更强的安全保证。不过,它仍然是这些语言中最容易上手的。不过,这使得它在底层编程方面稍微欠缺一些灵活度。
为何用Zig?那么,你为什么不选择 Zig 在你的下一个项目中呢?
说起来,Zig 在现代编程领域中通过平衡明确性、简洁性和系统编程所需的强大功能而脱颖而出。Zig 的整体理念是尽可能透明和可预测,这使得开发者能够完全掌控其代码,同时避免了未定义行为、隐藏的控制流以及隐式的运行时影响。
Zig 的多功能性让使用各种常见系统编程语言(如 C、C++、Rust 等)的开发者都心动不已。
- C 开发者可以避免未定义行为的风险,同时保持完全控制。甚至可以采用增量的方法。
- 厌倦了与借用检查器斗争的 Rust 开发者,他们希望找到一个更合理的学习曲线,同时仍然能够发挥最大的威力和安全性。
- 希望对内存管理有更多的控制,并减少运行时成本以提高关键性能应用程序性能的 Go 开发者。
在这个世界里,性能、安全性和简易性通常只能取其二,但Zig却实现了极为出色的平衡。
但是我也需要谈谈选择不使用Zig的理由。
为什么不 Zig最明显的一点是它的成熟度。尽管有像 Ghostty 或 Bun 这样很棒的项目,Zig 还是没有发布 1.0 版本,甚至推荐使用夜版(如在此处所示)。这表示 API 会有大的改动,不同版本间也可能存在不兼容的问题,类似那些绑定特定版本的项目。
例如,我无法从源代码构建 Ghostty,因为它需要的是 0.13 版本,而当时可用的 nightly 版本是 0.14 版本。接下来遇到的问题是构建 LSP 失败了,它首先说错误的 nightly 版本,然后在更新编译器后,编译过程中出现了错误。
这种情况在正式发布1.0版本之前是预料之中的,不过这使得它很难说服那些没有专门资源来定期适应更新的公司进行购买,或者会让这些公司卡在某个特定版本上。相比之下,Rust 在十年前就已经发布了它的1.0版本。
接下来的问题是与Zig的年轻特性及其生态系统有关。
每一种语言都需要随着时间的推移来构建其可用的工具和库,而且有很多事情在进行中,并且正在稳步增长。无缝的 C 语言互操作性也填补了你可能遇到的许多空白。然而,我们必须承认 Rust 和 Golang 拥有更大的生态系统,特别是在商业环境中,拥有更大的人才库。
Zig 的另一个很棒的地方是它追求简单。为了简洁而省略的每一点复杂性都省略了一些我们在其他语言中可能认为理所当然的功能。很赞(具体条件适用),Zig 这样做是为了减少不必要的复杂性。
比如说,与 Go 的 goroutines 或 Rust 的 无畏的并发 相比,没有内置的并发模型可能对你来说是无法接受的缺点。
我们仍然可以自己实现很多“缺失”的功能,而生态系统也会随着时间提供很多东西。但对于涉及大量并行处理或并发的项目来说,这可能会让事情变得更复杂,因此我们可能会选择另一种语言。
不过还是,我建议你试试 Zig 呀!
为什么要关注Zig,即使你不使用它在我看来,即使不直接用于工作,学习一门新的编程语言也是非常值得的。
每一种新语言不仅让我们接触到独特的设计哲学、解决问题的范式和方法,它还提供了一种新的视角来挑战并拓展我们的思维。
我们了解到一些技术和概念的见解,这些技术和概念可能在我们主要使用的语言中并不常见。但更广泛的了解有助于我们在长期中编写更好的代码,避免固步自封。
资源 Zig 项目介绍- Bun (JavaScript 运行时,)
- Ghostty (终端仿真器)
-
Awesome Zig (一个关于 Zig 语言的资源集合)
[面向对象的编程]: Object-Oriented Programming
[增删查改操作]: Create Read Update Delete
[Java虚拟机 (JVM)]: Java Virtual Machine
[测试系统]: System Under Test
共同學(xué)習(xí),寫下你的評論
評論加載中...
作者其他優(yōu)質(zhì)文章