前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ziglang30分钟速成

ziglang30分钟速成

作者头像
zouyee
发布2024-01-19 15:24:53
3820
发布2024-01-19 15:24:53
举报
文章被收录于专栏:Kubernetes GOKubernetes GO

这份zig简明教程适合已经有编程基础知识的同学快速了解zig语言,同时也适合没有编程经验但是懂得善用搜索引擎的同学,该文章详细介绍Zig编程语言各种概念,主要包括基础知识、函数、结构体、枚举、数组、切片、控制结构、错误处理、指针、元编程和堆管理等内容。

基本用法

命令 zig run my_code.zig 将编译并立即运行你的 Zig 程序。每个单元格都包含一个 Zig 程序,你可以尝试运行它们(其中一些包含编译时错误,可以注释掉后再尝试)。

首先声明一个 main() 函数来运行代码。

下面的代码什么都不会做,只是简单的示例:

代码语言:javascript
复制
// comments look like this and go to the end of the line
pub fn main() void {}

可以使用内置函数@import 导入标准库,并将命名空间赋值给一个 const 值。Zig 中的几乎所有东西都必须明确地被赋予标识符。你也可以通过这种方式导入其他 Zig 文件,类似地,你可以使用 @cImport 导入 C 文件。

代码语言:javascript
复制
const std = @import("std");

pub fn main() void {
    std.debug.print("hello world!\n", .{});
}

注意:后续会在结构部分部分解释 print 语句中的第二个参数。

一般用var 来声明变量,同时在大多数情况下,需要带上声明变量类型。

代码语言:javascript
复制
const std = @import("std");

pub fn main() void {
    var x: i32 = 47; // declares "x" of type i32 to be 47.
    std.debug.print("x: {}\n", .{x});
}

const 声明一个变量的值是不可变的。

代码语言:javascript
复制
pub fn main() void {
    const x: i32 = 47;
    x = 42; // error: cannot assign to constant
}

Zig 非常严苛,不允许你从外部作用域屏蔽标识符,以防止混淆:

代码语言:javascript
复制
const x: i32 = 47;

pub fn main() void {
    var x: i32 = 42;  // error: redefinition of 'x'
}

全局作用域的常量默认为编译时的 “comptime” 值,如果省略了类型,它们就是编译时类型,并且可以在运行时转换为运行时类型。

代码语言:javascript
复制
const x: i32 = 47;
const y = -47;  // comptime integer.

pub fn main() void {
    var a: i32 = y; // comptime constant coerced into correct type
    var b: i64 = y; // comptime constant coerced into correct type
    var c: u32 = y; // error: cannot cast negative value -47 to unsigned integer
}

如果希望在后面设置它,也可以明确选择将其保留为未定义。如果你在调试模式下意外使用它引发错误,Zig 将使用 0XAA 字节填充一个虚拟值,以帮助检测错误。

代码语言:javascript
复制
const std = @import("std");

pub fn main() void {
  var x: i32 = undefined;
  std.debug.print("undefined: {}\n", .{x});
}

在某些情况下,如果 Zig 可以推断出类型信息,才允许你省略类型信息。

代码语言:javascript
复制
const std = @import("std");

pub fn main() void {
    var x: i32 = 47;
    var y: i32 = 47;
    var z = x + y; // declares z and sets it to 94.
    std.debug.print("z: {}\n", .{z});
}

但是需要注意,整数字面值是编译时类型,所以下面的示例是行不通的:

代码语言:javascript
复制
pub fn main() void {
    var x = 47; // error: variable of type 'comptime_int' must be const or comptime
}

函数与结构体

函数

函数可以带参数和返回值,使用fn关键字声明。pub关键字表示函数可以从当前作用域导出,使其它地方可以调用。下面示例是一个不返回任何值的函数(foo)。pub关键字表示该函数可以从当前作用域导出,这就是为什么main函数必须是pub的。你可以像大多数编程语言中一样调用函数:

代码语言:javascript
复制
const std = @import("std");

fn foo() void {
    std.debug.print("foo!\n", .{});

    //optional:
    return;
}

pub fn main() void {
    foo();
}

下面示例是一个返回整数值的函数:

代码语言:javascript
复制
const std = @import("std");

fn foo() i32 {
    return 47;
}

pub fn main() void {
    var result = foo();
    std.debug.print("foo: {}\n", .{result});
}

Zig不允许你忽略函数的返回值:

代码语言:javascript
复制
fn foo() i32 {
    return 47;
}

pub fn main() void {
    foo(); // error: expression value is ignored
}

但是你可以将其赋值给丢弃变量 _

代码语言:javascript
复制
fn foo() i32 {
    return 47;
}

pub fn main() void {
  _ = foo();
}

也可以声明函数时带上参数的类型,这样函数调用时可以传入参数:

代码语言:javascript
复制
const std = @import("std");

fn foo(x: i32) void {
    std.debug.print("foo param: {}\n", .{x});
}

pub fn main() void {
    foo(47);
}

结构体

结构体通过使用const关键字分配一个名称来声明,它们的赋值顺序可以是任意的,并且可以使用常规的点语法进行解引用。

代码语言:javascript
复制
const std = @import("std");

const Vec2 = struct {
    x: f64,
    y: f64
};

pub fn main() void {
    var v = Vec2{.y = 1.0, .x = 2.0};
    std.debug.print("v: {}\n", .{v});
}

结构体可以有默认值;结构体也可以是匿名的,并且可以强制转换为另一个结构体,只要所有的值都能确定:

代码语言:javascript
复制
const std = @import("std");

const Vec3 = struct{
    x: f64 = 0.0,
    y: f64,
    z: f64
};

pub fn main() void {
    var v: Vec3 = .{.y = 0.1, .z = 0.2};  // ok
    var w: Vec3 = .{.y = 0.1}; // error: missing field: 'z'
    std.debug.print("v: {}\n", .{v});
}

可以将函数放入结构体中,使其像面向对象编程中的对象一样工作。这里有一个语法糖,如果你定义的函数的第一个参数为对象的指针,我们称之为”面向对象编程”,类似于Python带self参数的函数。一般约定是通过将变量命名为self来表示。

代码语言:javascript
复制
const std = @import("std");

const LikeAnObject = struct{
    value: i32,

    fn print(self: *LikeAnObject) void {
        std.debug.print("value: {}\n", .{self.value});
    }
};

pub fn main() void {
    var obj = LikeAnObject{.value = 47};
    obj.print();
}

我们一直传递给std.debug.print的第二个参数是一个元组,它是一个带有数字字段的匿名结构体。在编译时,std.debug.print会找出元组中参数的类型,并生成一个针对你提供的参数字符串的版本,这就是为何Zig知道如何将打印的内容变得漂亮的原因。

代码语言:javascript
复制
const std = @import("std");

pub fn main() void {
    std.debug.print("{}\n", .{1, 2}); #  error: Unused arguments
}

枚举、数组与切片

枚举

枚举通过使用const关键字将枚举组以类型方式来声明。

注意:在某些情况下,可以简化枚举的名称。其可以将枚举的值设置为整数,但它不会自动强制转换,你必须使用@enumToInt或@intToEnum来进行转换。

代码语言:javascript
复制
const std = @import("std");

const EnumType = enum{
    EnumOne,
    EnumTwo,
    EnumThree = 3
};

pub fn main() void {
    std.debug.print("One: {}\n", .{EnumType.EnumOne});
    std.debug.print("Two?: {}\n", .{EnumType.EnumTwo == .EnumTwo});
    std.debug.print("Three?: {}\n", .{@enumToInt(EnumType.EnumThree) == 3});
}

数组

Zig有数组概念,它们是具有在编译时已知长度的连续内存。你可以通过在前面声明类型并提供值列表来初始化它们,同时可以通过数组的len字段访问它们的长度。

注意:Zig中的数组也是从零开始索引的。

代码语言:javascript
复制
const std = @import("std");

pub fn main() void {
    var array: [3]u32 = [3]u32{47, 47, 47};

    // also valid:
    // var array = [_]u32{47, 47, 47};

    var invalid = array[4]; // error: index 4 outside array of size 3.
    std.debug.print("array[0]: {}\n", .{array[0]});
    std.debug.print("length: {}\n", .{array.len});
}

切片

跟golang类似,Zig也有切片(slices),它们的长度在运行时已知。你可以使用切片操作从数组或其他切片构造切片。与数组类似,切片有一个len字段,告诉它的长度。

注意:切片操作中的间隔参数是开口的(不包含在内)。尝试访问超出切片范围的元素会引发运行时panic。

代码语言:javascript
复制
const std = @import("std");

pub fn main() void {
    var array: [3]u32 = [_]u32{47, 47, 47};
    var slice: []u32 = array[0..2];

    // also valid:
    // var slice = array[0..2];

    var invalid = slice[3]; // panic: index out of bounds

    std.debug.print("slice[0]: {}\n", .{slice[0]});
    std.debug.print("length: {}\n", .{slice.len});
}

字符串文字是以null结尾的utf-8编码的const u8字节数组。Unicode字符只允许在字符串文字和注释中使用。

注意:长度不包括null终止符(官方称为”sentinel termination”)。访问null终止符是安全的。索引是按字节而不是Unicode字符。

代码语言:javascript
复制
const std = @import("std");
const string = "hello 世界";
const world = "world";

pub fn main() void {
    var slice: []const u8 = string[0..5];

    std.debug.print("string {}\n", .{string});
    std.debug.print("length {}\n", .{world.len});
    std.debug.print("null {}\n", .{world[5]});
    std.debug.print("slice {}\n", .{slice});
    std.debug.print("huh? {}\n", .{string[0..7]});
}

const数组可以强制转换为const切片。

代码语言:javascript
复制
const std = @import("std");

fn foo() []const u8 {  // note function returns a slice
    return "foo";      // but this is a const array.
}

pub fn main() void {
    std.debug.print("foo: {}\n", .{foo()});
}

流程控制与错误处理

流程控制

Zig提供了与其他语言类似的if语句、switch语句、for循环和while循环。示例:

代码语言:javascript
复制
const std = @import("std");

fn foo(v: i32) []const u8 {
    if (v < 0) {
        return "negative";
    }
    else {
        return "non-negative";
    }
}

pub fn main() void {
    std.debug.print("positive {}\n", .{foo(47)});
    std.debug.print("negative {}\n", .{foo(-47)});
}

switch

代码语言:javascript
复制
const std = @import("std");

fn foo(v: i32) []const u8 {
    switch (v) {
        0 => return "zero",
        else => return "nonzero"
    }
}

pub fn main() void {
    std.debug.print("47 {}\n", .{foo(47)});
    std.debug.print("0 {}\n", .{foo(0)});
}

for-loop

代码语言:javascript
复制
const std = @import("std");

pub fn main() void {
    var array = [_]i32{47, 48, 49};

    for (array) | value | {
        std.debug.print("array {}\n", .{value});
    }
    for (array) | value, index | {
        std.debug.print("array {}:{}\n", .{index, value});
    }

    var slice = array[0..2];

    for (slice) | value | {
        std.debug.print("slice {}\n", .{value});
    }
    for (slice) | value, index | {
        std.debug.print("slice {}:{}\n", .{index, value});
    }
}

while loop

代码语言:javascript
复制
const std = @import("std");

pub fn main() void {
    var array = [_]i32{47, 48, 49};
    var index: u32 = 0;

    while (index < 2) {
        std.debug.print("value: {}\n", .{array[index]});
        index += 1;
    }
}

错误处理

错误是特殊的联合类型,你可以在函数前面加上 ! 来表示该函数可能返回错误。你可以通过简单地将错误作为正常返回值返回来抛出错误。

代码语言:javascript
复制
const MyError = error{
    GenericError,
    OtherError
};

pub fn main() !void {
    return MyError.GenericError;
}

fn wrap_foo(v: i32) void {
    if (foo(v)) |value| {
        std.debug.print("value: {}\n", .{value});
    } else |err| {
        std.debug.print("error: {}\n", .{err});
    }
}

如果你编写一个可能出错的函数,当它返回时你必须决定如何处理错误。两个常见的选择是 try 和 catch。try 方式很摆烂,它只是简单地将错误转发为函数的错误。而 catch 需要处理错误。

try 其实就是 catch | err | {return err} 的语法糖。

代码语言:javascript
复制
const std = @import("std");
const MyError = error{
    GenericError
};

fn foo(v: i32) !i32 {
    if (v == 42) return MyError.GenericError;
    return v;
}

pub fn main() !void {
    // catch traps and handles errors bubbling up
    _ = foo(42) catch |err| {
        std.debug.print("error: {}\n", .{err});
    };

    // try won't get activated here.
    std.debug.print("foo: {}\n", .{try foo(47)});

    // this will ultimately cause main to print an error trace and return nonzero
    _ = try foo(42);
}

我们也可以使用 if 来检查错误。

代码语言:javascript
复制
const std = @import("std");
const MyError = error{
    GenericError
};

fn foo(v: i32) !i32 {
    if (v == 42) return MyError.GenericError;
    return v;
}

// note that it is safe for wrap_foo to not have an error ! because
// we handle ALL cases and don't return errors.
fn wrap_foo(v: i32) void {    
    if (foo(v)) | value | {
        std.debug.print("value: {}\n", .{value});
    } else | err | {
        std.debug.print("error: {}\n", .{err});
    }
}

pub fn main() void {
    wrap_foo(42);
    wrap_foo(47);
}

指针

Zig使用*表示指针类型,可以通过.*语法访问指针指向的值。示例:

代码语言:javascript
复制
const std = @import("std");

pub fn printer(value: *i32) void {
    std.debug.print("pointer: {}\n", .{value});
    std.debug.print("value: {}\n", .{value.*});
}

pub fn main() void {
    var value: i32 = 47;
    printer(&value);
}

注意:在Zig中,指针需要正确对齐到它所指向的值的对齐方式。 对于结构体,类似于Java,您可以解引用指针并一次获取字段,使用 . 运算符。需要注意的是,这仅适用于一层间接引用,因此如果您有指向指针的指针,您必须首先解引用外部指针。

代码语言:javascript
复制
const std = @import("std");

const MyStruct = struct {
    value: i32
};

pub fn printer(s: *MyStruct) void {
    std.debug.print("value: {}\n", .{s.value});
}

pub fn main() void {
    var value = MyStruct{.value = 47};
    printer(&value);
}

Zig允许任何类型(不仅仅是指针)可为空,但请注意它们是基本类型和特殊值 null 的联合体。要访问未包装的可选类型,请使用 .? 字段:

代码语言:javascript
复制
const std = @import("std");

pub fn main() void {
    var value: i32 = 47;
    var vptr: ?*i32 = &value;
    var throwaway1: ?*i32 = null;
    var throwaway2: *i32 = null; // error: expected type '*i32', found '(null)'

    std.debug.print("value: {}\n", .{vptr.*}); // error: attempt to dereference non-pointer type
    std.debug.print("value: {}\n", .{vptr.?.*});
}

注意:当我们使用来自C ABI函数的指针时,它们会自动转换为可为空指针。 获得未包装的可选指针的另一种方法是使用 if 语句:

代码语言:javascript
复制
const std = @import("std");

fn nullChoice(value: ?*i32) void {
    if (value) | v | {
        std.debug.print("value: {}\n", .{v.*});
    } else {
        std.debug.print("null!\n", .{});
    }
}

pub fn main() void {
    var value: i32 = 47;
    var vptr1: ?*i32 = &value;
    var vptr2: ?*i32 = null;

    nullChoice(vptr1);
    nullChoice(vptr2);
}

元编程

Zig的元编程受几个基本概念驱动:

  • 类型在编译时是有效的值。
  • 大多数运行时代码在编译时也能工作。
  • 结构体字段的评估是编译时的鸭子类型(duck-typed)。
  • Zig标准库提供了执行编译时反射的工具。

下面是元编程的一个示例:

代码语言:javascript
复制
const std = @import("std");

fn foo(x : anytype) @TypeOf(x) {
    // note that this if statement happens at compile-time, not runtime.
    if (@TypeOf(x) == i64) {
        return x + 2;
    } else {
        return 2 * x;
    }
}

pub fn main() void {
    var x: i64 = 47;
    var y: i32 =  47;

    std.debug.print("i64-foo: {}\n", .{foo(x)});
    std.debug.print("i32-foo: {}\n", .{foo(y)});
}

以下是泛型类型的一个示例:

代码语言:javascript
复制
const std = @import("std");

fn Vec2Of(comptime T: type) type {
    return struct{
        x: T,
        y: T
    };
}

const V2i64 = Vec2Of(i64);
const V2f64 = Vec2Of(f64);

pub fn main() void {
    var vi = V2i64{.x = 47, .y = 47};
    var vf = V2f64{.x = 47.0, .y = 47.0};
    
    std.debug.print("i64 vector: {}\n", .{vi});
    std.debug.print("f64 vector: {}\n", .{vf});
}

通过这些概念,我们可以构建非常强大的泛型类型!

堆管理

Zig为我们提供了与堆交互的多种方式,通常要求您明确选择使用哪种方式。它们都遵循下述相同的模式:

  1. 创建一个分配器工厂结构体。
  2. 检索由分配器工厂创建的std.mem.Allocator结构体。
  3. 使用alloc/free和create/destroy函数来操作堆。
  4. (可选)销毁分配器工厂。

这么处理的目的是:

  • 为了阻止您过度使用堆。
  • 这使得调用堆的任何东西(基本上是可失败的操作)都是显式的。
  • 您可以仔细调整权衡,并使用标准数据结构而无需重写标准库。
  • 您可以在测试中运行非常安全的分配器,并在发布/生产环境中切换到不同的分配器。

好的,但是你也可以偷点懒。你是不是想一直使用jemalloc? 只需选择一个全局分配器,并在所有地方使用它(请注意,某些分配器是线程安全的,而某些则不是)。

在这个示例中,我们将使用std.heap.GeneralPurposeAllocator工厂创建一个具有多种特性(包括泄漏检测)的分配器,并看看它是如何组合在一起的。

最后一件事,这里使用了defer关键字,它非常类似于Go语言中的defer关键字!还有一个errdefer关键字,如果需要了解更多信息,请查阅Zig文档。

代码语言:javascript
复制
const std = @import("std");

// factory type
const Gpa = std.heap.GeneralPurposeAllocator(.{});

pub fn main() !void {
    // instantiates the factory
    var gpa = Gpa{};
    
    // retrieves the created allocator.
    var galloc = &gpa.allocator;
    
    // scopes the lifetime of the allocator to this function and
    // performs cleanup; 
    defer _ = gpa.deinit();

    var slice = try galloc.alloc(i32, 2);
    // uncomment to remove memory leak warning
    // defer galloc.free(slice);
    
    var single = try galloc.create(i32);
    // defer gallo.destroy(single);

    slice[0] = 47;
    slice[1] = 48;
    single.* = 49;

    std.debug.print("slice: [{}, {}]\n", .{slice[0], slice[1]});
    std.debug.print("single: {}\n", .{single.*});
}

总结

现在我们已经掌握了相当大的Zig基础知识。没有覆盖的一些(非常重要的)内容包括:

  • 测试(Zig使得编写测试非常容易)
  • 标准库
  • 内存模型(Zig在分配器方面没有倾向性)
  • 异步编程(Zig 的异步特性在编译器中出现了性能退化,在 0.11 版本的 Zig 中已经不存在了,并且在 Zig 0.12 版本中也可能不会出现。)
  • 交叉编译
  • build.zig 文件

如果想要了解更多细节,请查阅最新的文档。

由于笔者时间、视野、认知有限,本文难免出现错误、疏漏等问题,期待各位读者朋友、业界专家指正交流。

参考文献

1.https://ziglang.org/documentation/master/.

2.https://gist.github.com/ityonemo/769532c2017ed9143f3571e5ac104e50

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-01-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DCOS 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档