初探 Zig 编程:新手入门

1.1 语法概念

讲真,学习一门编程语言到底要花多少精力,我不知道。但是语法概念绝对是新手最需要了解掌握的,虽然内容不多,但是知识点全部都是新知识,需要各位读者朋友耐心一起看。

从之前的篇章我们知道 Zig 是更好的 C,内置 C 编译器,所以和 C 一样,语句都是用分号结尾,代码块都是以花括号{ } 分隔。

接下来我们要看一个完整的小程序代码来讲解主要的语法

const std = @import("std");

// 如果 `main` 不是 `pub` (public),此代码将无法编译
pub fn main() void {
    const user = User{
        .power = 9001,
        .name = "Goku",
    };

    std.debug.print("{s}'s power is {d}\n", .{user.name, user.power});
}

pub const User = struct {
    power: u64,
    name: []const u8,
};

你可以把这个代码块保存为 learning.zig 文件,直接跑  zig run learning.zig  就可以得到结果:

Goku's power is 9001

1.2 语法 1:模块引用

啥叫模块,就是编写好的函数代码库,方便你直接在程序代码中直接引用。从上面的范例代码中,我们可以看到:

// 以 @ 开头的函数是内置函数。它们是由编译器提供的,而不是标准库提供的。
const std = @import("std");

上面这行的意思就是Zig 的标准库以 std 作为模块名。简单明了。

当然既然能引用内置的标准库文件,外部的文件是否可以当成模块引用呢。这个肯定是可以的,触类旁通。例如,将 User 结构移动到它自己的文件中,比如 models/user.zig

// models/user.zig
pub const User = struct {
    power: u64,
    name: []const u8,
};

在主程序代码中可以这样引用:

// main.zig
const User = @import("models/user.zig").User;

models/user.zig 可以导出不止一项内容。例如,再导出一个常量:

// models/user.zig
pub const MAX_POWER = 100_000;

pub const User = struct {
    power: u64,
    name: []const u8,
};

这时,可以这样导入两个变量:

const user = @import("models/user.zig");
const User = user.User;
const MAX_POWER = user.MAX_POWER

1.3 语法 2:代码注释

Zig 的代码注释就一种,

// 如果 `main` 不是 `pub` (public),此代码将无法编译

没有 C 语言的代码注释 如/* ... */ 的多行注释语法。

如果你看过 Zig 的标准库文档,你就会看到它的实际应用。//! 被称为顶级文档注释,可以放在文件的顶部。三斜线注释 (///) 被称为文档注释,可以放在特定位置,如声明之前。如果在错误的地方使用这两种文档注释,编译器都会出错。注意,其实不管怎么变化,注释代码的语法就是双斜杠。

1.4  语法 3:函数

说到函数,我们最需要知道的就是入口函数 main:

pub fn main() void

忽略 main 作为程序入口的特殊作用,它只是一个非常基本的函数:不带参数,不返回任何东西(void)。

接下来我们看一个函数例子:

const std = @import("std");

pub fn main() void {
    const sum = add(8999, 2);
    std.debug.print("8999 + 2 = {d}\n", .{sum});
}

fn add(a: i64, b: i64) i64 {
    return a + b;
}

主函数里面运行的 add 函数可以在不申明的情况下直接运行,这个是有点颠覆 C/C++ 程序员的习惯的,需要注意。

为了提高可读性,Zig 中不支持函数重载(用不同的参数类型或参数个数定义的同名函数)。这也是 Zig 降低程序员心智的设计,让我们可以专注业务编程。

1.5  语法 4:结构体

结构体就是面向对象编程中的对象:

pub const User = struct {
    power: u64,
    name: []const u8,
};

结构字段以逗号终止,并且可以指定默认值:

pub const User = struct {
    power: u64 = 0,
    name: []const u8,
};

当我们创建一个结构体时,必须对每个字段赋值。比如如下赋值

const user = User{.name = "Goku"};

因为User 结构体字段 power 配置了默认值,所以上面的代码可以正常编译。

结构体可以有方法,也可以包含声明(包括其他结构),甚至可能包含零个字段,此时的作用更像是命名空间。

pub const User = struct {
    power: u64 = 0,
    name: []const u8,

    pub const SUPER_POWER = 9000;

    fn diagnose(user: User) void {
        if (user.power >= SUPER_POWER) {
            std.debug.print("it's over {d}!!!", .{SUPER_POWER});
        }
    }
};

上面 diagnose 在定义 User 类型中,接受 User 作为其第一个参数。因此,我们可以使用struct.method() 的语法来调用它。但结构内的函数不必遵循这种模式。一个常见的例子是用于结构体初始化的 init 函数:

pub const User = struct {
    power: u64 = 0,
    name: []const u8,

    pub fn init(name: []const u8, power: u64) User {
        return User{
            .name = name,
            .power = power,
        };
    }
}

init 的命名方式仅仅是一种约定,在某些情况下,open 或其他名称可能更有意义。如果你和我一样,不是 C++ 程序员,可能对 .$field = $value, 这种初始化字段的语法感到奇怪,但你很快就会习惯它。

当我们创建 "Goku" 时,我们将 user 变量声明为 const :

const user = User{
    .power = 9001,
    .name = "Goku",
};

// 第二种写法
const user: User = User{
    .power = 9001,
    .name = "Goku",
};

// 第三种写法
const user: User = .{
    .power = 9001,
    .name = "Goku",
};

// 第四种写法
pub fn init(name: []const u8, power: u64) User {
    // instead of return User{...}
    return .{
        .name = name,
        .power = power,
    };
}

1.6  语法5:数组和切片

数组的大小是固定的,其长度在编译时已知。长度是类型的一部分,因此 4 个有符号整数的数组 [4]i32 与 5 个有符号整数的数组 [5]i32 是不同的类型。

数组长度可以从初始化中推断出来。在以下代码中,所有三个变量的类型均为 [5]i32 :

const a = [5]i32{1, 2, 3, 4, 5};

// 我们已经在结构体中使用过 .{...} 语法,
// 它也适用于数组

const b: [5]i32 = .{1, 2, 3, 4, 5};

// 使用 _ 让编译器推导长度
const c = [_]i32{1, 2, 3, 4, 5};

切片是指向数组的指针,外加一个在运行时确定的长度。你可以将切片视为数组的视图。切片例子如下:

const a = [_]i32{1, 2, 3, 4, 5};
const b = a[1..4];

在上述代码中, b 是一个长度为 3 的切片,并且是一个指向 a 的指针。
 

在实际代码中,切片的使用可能会多于数组。无论好坏,程序的运行时信息往往多于编译时信息。不过,在下面这个例子中,我们必须欺骗 Zig 编译器才能得到我们想要的示例:

const a = [_]i32{1, 2, 3, 4, 5};
var end: usize = 4;
const b = a[1..end];

b 现在是一个切片了。具体来说,它的类型是 []const i32

在创建切片时,我们可以省略上界,创建一个到要切分的对象(数组或切片)末尾的切片,例如 const c = b[2..]

当我们想通过切片修改数组的值的时候,一定需要使用 var  赋值。

const std = @import("std");

pub fn main() void {
    const a = [_]i32{1, 2, 3, 4, 5};
    var end: usize = 4;
    const b = a[1..end];
    std.debug.print("{any}", .{@TypeOf(b)});
}

因为 b 类型是 const ,执行操作 b[2] = 5 ,我们会收到编译时错误:cannot assign to constant.。如果我们想写入 b,就需要将 a 从 const 变为 var

const std = @import("std");

pub fn main() void {
    var a = [_]i32{1, 2, 3, 4, 5};
    var end: usize = 4;
    const b = a[1..end];
    b[2] = 99;
}

1.7 语法 6:字符串

Zig 里字符串是字节(u8)的序列(即数组或切片)。我们可以从 name 字段的定义中看到这一点:name: []const u8。

按照惯例,这类字符串大多数都是用 UTF-8 编码,因为 Zig 源代码本身就是 UTF-8 编码的。但这并不是强制的,而且代表 ASCII 或 UTF-8 字符串的 []const u8 与代表任意二进制数据的 []const u8 实际上并没有什么区别。

"Goku" 的类型更接近于 *const [4]u8,是一个指向 4 常量字节数组的指针。字符串字面量以空值结束。也就是说,它们的末尾总是有一个 \0。在内存中,"Goku" 实际上是这样的:{'G', 'o', 'k', 'u', 0},Zig 有一种独特的语法来表示以空结尾的数组。"Goku"的类型是 *const[4:0]u8,即 4 字节以空结尾的数组指针。

const std = @import("std");

pub fn main() void {
    //  建立 3 个布尔值的数值,并且以 false 值结尾
    const a = [3:false]bool{false, true, false};

    // 这句使用了 mem 的高级函数,先不用试图去理解
    std.debug.print("{any}\n", .{std.mem.asBytes(&a).*});
}

上面代码会输出:{ 0, 1, 0, 0} 。

1.8  语法 7:comptime 和 anytype

先看代码

std.debug.print("{s}'s power is {d}\n", .{user.name, user.power});

print 函数的定义要求我们的第一个参数(字符串格式)是编译时已知的:

// 注意变量"fmt"前的"comptime"
pub fn print(comptime fmt: []const u8, args: anytype) void {

因为 print 会进行额外的编译时检查,什么样的检查呢?假设你把格式改为 it's over {d}/n,但保留了两个参数。你会得到一个编译时错误:unused argument in 'it's over {d}'。它还会进行类型检查:将格式字符串改为{s}'s power is {s}\n,你会这个错误invalid format string 's' for type 'u64'。如果在编译时不知道字符串的格式,就不可能在编译时进行这些检查。

comptime 会对编码产生直接影响的地方是整数和浮点字面的默认类型,即特殊的 comptime_int 和 comptime_float。这行代码是无效的:var i = 0comptime代码只能使用编译时已知的数据,对于整数和浮点数,这类数据由特殊的 comptime_int 和 comptime_float 类型标识。这种类型的值可以在编译时执行。你需要做的是给变量一个显式类型:

var i: usize = 0;
var j: f64 = 0;

开头print 函数中特殊代码片段 .{user.name, user.power} 是什么呢?根据上述 print 的定义,我们知道它映射到 anytype 类型的变量。我们打印看一下:

pub fn main() void {
    std.debug.print("{any}\n", .{@TypeOf(.{.year = 2023, .month = 8})});
}

会输出:

struct{comptime year: comptime_int = 2023, comptime month: comptime_int = 8}

在这里,我们给匿名结构的字段取名为 year 和 month。在原始代码中,我们没有这样做。在这种情况下,字段名会自动生成 0、1、2 等。print 函数希望结构中包含此类字段,并使用字符串格式中的序号位置来获取适当的参数。

Zig 没有函数重载,也没有可变函数(vardiadic,具有任意数量参数的函数)。但它的编译器能根据传入的类型创建专门的函数,包括编译器自己推导和创建的类型。

1.9  总结

Zig 的基本语法虽然已经精简了,但是还是有一些知识量的,所以本节我们就到这里,我把官方的内容做了一次梳理重组,让知识点能简明扼要,保持 Zig 语法的简洁风格。方便大家学习。请稍安勿躁,下一节继续把基本语法讲完。

  • 21
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xiaodeshi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值