Zig是码农们期待已久的C语言的替代品吗?

开发 前端
从很多方面来说,我整个编程生涯都像是在等待C的替代产品的漫长等待。20年前,尽管我用C ++。 随着时间的流逝,我了解到C ++是一个复杂的怪物,无论我读了多少本书,都永远无法驯服。

与以前的C竞争者(例如C ++,D,Java,C#,Go,Rust和Swift)的比较

从很多方面来说,我整个编程生涯都像是在等待C的替代产品的漫长等待。20年前,尽管我用C ++。 随着时间的流逝,我了解到C ++是一个复杂的怪物,无论我读了多少本书,都永远无法驯服。

我认为Yossi Kreinin和他的C ++常见问题解答(https://yosefk.com/c++fqa/)在总结我对C ++讨厌的各个方面都做得很好。

因此,在成为一名专业的C ++程序员时,我总是着眼于其他选择。 第一个有希望的替代品是D。D最初看起来很有前途,但经过仔细检查,我认为D实际上只是一个根本上不好的主意的清理版本。 C ++的主要问题之一是它的接收器语言设计方法。

当用C和Lua实现一个简单的游戏引擎时,我意识到与C ++相比,同时保留这两种语言的思路实际上更少了。 它给我带来了对C的重新热爱。尽管有其所有局限性,C是一种相当简单的语言,可以提供很多控制。

Java和C#在许多方面只是尝试重新实现C ++。他们可能使事情变得更简单,但最终却陷入了虚拟机和90年代面向对象的编程炒作中。不是说Java或C#不好,其中很多可能与那些支持臃肿的IDE和过度设计的语言的相关社区有关。

Zig是码农们期待已久的C语言的替代品吗?

C简单性的回归

从Google转到Go是对多余的C ++,D,Java和C#的欢送。Go将我们带回到了起点,回到了C。Go重新设想了如果不冒险走C ++道路,C可能会是什么。我们得到的是一种简单的语言,它修复了我在C语言中经常遇到的许多问题。

但是故事还没有结束。 紧随Go之后,我们得到了Rust。 最初,我认为Rust实际上是D应该一直以来的目标。 对C ++应该是什么的真正的重新思考。 Rust保持了低级控制,高级抽象机制和手动内存管理,但增加了无与伦比的类型安全性。 一切看起来都太好了,难以置信。

坦率地说,我认为是。我记得能够在两天内用Go编写一些不错的程序。朱莉娅,我目前的最爱也有些相似。另一方面,学习Rust就像学习Haskell。在做任何有用的事情之前,只需了解许多概念和理论即可。

如果C ++教会了我任何东西,那就是要重视简单性,而Rust不会这样做。

后来,我从互联网上许多Rust用户的评论中学到了,Rust重复了C ++的主要缺点之一。它的编译时间确实很慢。我认为没有什么比等待C ++编译破坏我编程的乐趣了。听起来好像Rust更糟了。那是一个破坏交易的因素。

Zig是码农们期待已久的C语言的替代品吗?

Swift-我想爱的语言

我20年来一直是苹果的忠实粉丝。 我很喜欢Cocoa GUI库,并在没有iPhone之前就对Objective-C进行了编程,突然,每个人和他们的宠物都在用Objective-C进行编程。

是的,Objective-C有点笨拙,但它的简单性具有一定的美感。 与C ++不同,它是对C语言的相当简单的补充。 根据经验,您实际上可以真正快速地教初级开发人员Objective-C。

因此,Swift的发布让我觉得我已经达到了编程的必杀技。最终,一种非常现代的语言与Objective-C很好地集成在一起,因此我们仍然可以使用很棒的Apple库,例如Cocoa。

Swift从Rust那里借来了很多想法,从很多方面来说,我认为我们终于为普通人获得了Rust。可以在相当短的时间内学会Swift。

但是我在Swift方面的经历好坏参半。 即使在今天,我也仍然无法正确表达语言的问题,因为它似乎可以解决很多问题。

我将iPhone应用程序从Objective-C移植到了Swift。 我最好的经历之一是Swift仅仅由于严格的类型系统就发现了一大堆错误,这些系统捕获了在Objective-C中不可见的问题,而这在编译时至少是关于类型的,这是众所周知的。

与C ++,C#和Java相比,我会说Swift是更好的语言。 Swift几乎解决了我所有有关C ++的特定问题。 但是我每次使用Go时都意识到,编写Go程序比Swift有趣得多。 但是Go的错误处理有点糟糕,它重复了具有空指针的百万美元错误。 Swift避免了这两个问题。

上次与Julia在一起很长时间后,我回到Swift时,看到Swift的一些问题变得更加清晰:

Swift语法不适合函数式编程

从Objective-C继承的Smalltalk启发式语法在面向对象的编程中很好地工作,但是对于函数式编程却非常糟糕。将函数用作一等公民时,您无需费心确保参数名称正确。

面向对象的编程和函数式编程之间的停战。Swift试图为两个不同的主人服务,并为此遭受痛苦。在进行非常实用的样式编程时,您希望您的函数主要是自由函数。这些更易于传递并在函数设置中使用。

但是Swift最终主要是面向OOP人群,将函数放在方法中。 一旦完成了许多函数编程,这就会变得很麻烦。

Zig适合编程领域的何处?

因此Swift从来没有真正成为我最终的通用编程语言。如果我想以更高的抽象级别进行编程,获得高性能并完成工作,我将选择Julia。

但这仍然为C之类的替代品留下了未填补的空间。朱莉娅(Julia)不能真正取代C。它吞噬了内存,无法产生小的二进制文件,不适合使其他语言可以使用的库。您不想使用它来创建OS内核或进行微控制器编程。

Go和Rust都真的接近于替换C。Go摆脱了使用C的简单性和使用感。但是它使用垃圾回收并不能完全替代C。与Java相比,在Go中对内存使用的更多控制仍然是毫无价值的,因为您可以获得指针,并且实际上可以创建自己的辅助分配器。

Rust降低了手动的内存分配,但是未能复制C的简单性和感觉。也许这两种语言之间是否有什么可以填补的空间?

确实有。 我认为这就是Zig。 Zig比Go更复杂,但比Rust更易于学习和使用。

但是,这样的Zig总结并不能使语言公正。Zig为表带来了很多新想法,这很有道理,并且使Zig编码的体验非常独特。但是在深入探讨之前,让我们先看一下基础知识。

正确掌握基础知识

如果我们要学习另一种类似C的语言,我们将无法重复C ++最糟糕的情况,例如糟糕的编译时间。 Zig如何解决这些问题?

我遇到了V编程语言的创建者Alexander Medvednikov进行的测试。这是编译具有400 K函数的文件的测试:

  • C 5.2秒 gcc测试
  • C ++ 1分25秒 g ++ test.cpp
  • Zig 10.1秒 Zig build-exe test.zig
  • Nim 45秒 nim c test.nim
  • Rust 30分钟 rustc test.rs 后Rust停止
  • Swift 在30分钟的swiftc测试后停止
  • D 6分钟后 segfault dmd test.d
  • V 0.6秒 v test v

Rust,Swift和D都失败了。Medvednikov用更少的行数对这些语言进行了进一步的测试,Rust再次表现出了最差的预期。

正如您在列表中所看到的,Zig是最杰出的演员。 尽管很难不注意到V语言会在不到一秒钟的时间内完成所有操作。 这使我想起更详细地探索V。 快速扫描表明它可以手动分配内存,泛型和可选(必须明确允许使用空指针)。

Zig内存分配

如果不进行手动内存管理,您将无法使用C语言。进行C风格编程的人都希望这样做。如果我不需要,那么我可以为Julia编程。

Zig没有提供Rust所提供的那种最高的安全性,但是如果不这样做,它所获得的是一个对于初学者来说更容易掌握和使用的模型。

需要在Zig中分配内存的任何内容都将分配器作为参数。 因此Zig非常明确地说明了何时需要内存管理。

这是我编写的一个简单函数,它使用32位无符号整数n并将其拆分为十进制数字:

  1. fn decimals(alloc: *Allocator, n: u32) !Array(u32) { 
  2. var x = n
  3. var digits = Array(u32).init(alloc); 
  4. errdefer digits.deinit(); 
  5. while (x >= 10) { 
  6. try digits.append(x % 10); 
  7. xx = x / 10; 
  8. try digits.append(x); 
  9. return digits; 

请注意,必须使用分配器分配用于保留各个十进制数字的数组数字,该分配器是十进制函数的参数。

这就是Zig真正的光芒所在。 确保您不会忘记分配内存在C语言中很难。而且很容易以错误的位置结束内存。 Zig从Go复制了延迟概念。 但是除了推迟它还有errdefer。 如果您不了解Go,那么从本质上讲,延迟是将行或代码块的执行推迟到函数退出之前的一种方法。

为什么这么好? 因为它使您可以确保某些代码得以运行,而不管退出该函数之前使用了什么复杂的if-else语句。

  1. errdefer digits.deinit(); 

上面的行与正常的Go延迟有所不同,因为只有在返回错误代码的情况下,它才会执行。因此,如果一切正常,那么它将永远不会运行。

在呼叫站点,我们将使用常规延迟来确保我们不会忘记释放分配给数字的内存。

  1. const digits = try decimals(allocator, 4123); 
  2. defer digits.deinit(); 
  3. for (digits.items) |digit| { 
  4. try print("{},", .{digit}); 

从我在Zig玩游戏方面的有限经验,我会说这是一个很好的系统。分配器和defer的结合使用使您非常清楚要分配和释放内存的位置,同时可以轻松正确地进行分配。

C兼容

许多类C语言的问题是它们无法与C配合使用。这意味着从该语言调用C函数应该很容易,而从C对该语言调用函数应该很容易。

此外,您编写程序的一般方式应该与C完全兼容,因此您不必创建较大的C抽象级别。例如。C ++对C语言不是很友好,因为没有大量包装就无法在C中使用典型的C ++库。

但是Zig非常C,因为它没有暴露C不会得到的奇怪的东西。 结构中没有vtable(C ++中的虚拟函数表)。 没有C知道如何调用的构造函数或析构函数。 也没有任何例外,C也会在捕获方面遇到困难。

从Zig使用C很简单。 实际上,Zig的创建者会声称Zig比C本身更擅长使用C库。

  1. const c = @cImport({ 
  2. @cDefine("_NO_CRT_STDIO_INLINE", "1"); 
  3. @cInclude("stdio.h"); 
  4. }); 
  5. pub fn main() void { 
  6. _ = c.printf("hello\n"); 

如您所见,Zig解析C头文件并包含来自C的类型和函数没有问题。实际上Zig是完全成熟的C编译器。您可以根据需要使用Zig编译C程序。

同样,将Zig函数暴露给C也很容易。这是一个Zig函数,采用32位整数并返回32位整数。

  1. export fn add(a: i32, b: i32) i32 { 
  2. return a + b; 

通过将export放在它的前面,可以使我们与程序链接的C代码可以访问它。实际上,我们的主要功能是在C代码部分中定义的,并且使用了Zig中定义的功能。

  1. #include <stdint.h>int32_t add(int32_t a, int32_t b); 
  2. int main(int argc, char **argv) { 
  3. assert(add(42, 1337) == 1379); 
  4. return 0; 

这意味着您可以轻松地开始将较大的C程序的某些部分转换为Zig并继续进行编译。 移植程序时,这是一项非常强大的功能。 过去让我很容易地从Objective-C移植到Swift的原因是,我可以一次用Swift版本替换一个Objective-C方法,进行编译并看到一切仍然有效。

实际上,通过允许您自动将C程序转换为Zig代码,Zig使其变得更加容易。 这是Zig编译器内置的:

  1. $ zig translate-c foobar.c 

当然,该代码不是最佳的,可能有点混乱。但这有点像使用Google翻译进行自然语言翻译。这是一个很好的起点,可以节省大量的体力劳动。您可以稍后自己手动修复细节。

极简主义

极简主义首先吸引了很多人使用C编程。 这就是Go正确的事情,并使编程变得很高兴。 您可以轻松地将整个程序放在脑子里。

现在,如果您开始阅读Zig并查看我在这里给您的源代码示例,它可能看起来很复杂。 有些语言结构可能看起来很奇怪。 可以很容易地感觉到它是一种复杂的语言。

因此,弄清Zig不支持的所有事物实际上非常有用:

  • 没有类继承,例如C ++,Java,Swift等。
  • 通过Go之类的接口没有运行时多态性。
  • 没有泛型。 您不能像在编译时检查的Swift中那样指定通用接口。
  • 没有函数重载。您不能多次使用不同的参数编写具有相同名称的函数。
  • 没有异常抛出。
  • 没有闭包。
  • 没有垃圾收集。
  • 没有用于资源获取的构造函数和析构函数是初始化(RAII)。

但是,通过巧妙地使用一些核心功能,Zig能够提供几乎相同的功能:

  • 类型可以在编译时像对象一样传递。
  • 标签工会。在其他编程语言中也称为求和类型或变体。
  • 函数指针。
  • 实数指针和指针算术。
  • Zig代码可以在编译时部分评估。您可以使用comptime关键字将代码标记为在编译时可执行。
  • 函数和类型可以与结构相关联,但不能物理存储在结构中,因此C代码不可见。

在Zig中模拟泛型

例如。通过利用在编译时运行代码的能力,在Julia中创建了类似于模板的内容。洛里斯·克罗(Loris Cro)有一篇很好的文章更详细地描述了这一点。我将仅使用该文章中的示例来快速了解该想法。

我们可以定义例如一个称为LinkedList的函数,该函数只能在编译时调用,该函数采用链表中元素的类型,然后返回包含以下元素的链表类型:

  1. fn LinkedList(comptime T: type) type { 
  2. return struct { 
  3. pub const Node = struct { 
  4. prev: ?*Node = null, next: ?*Node = null, data: T, 
  5. }; 
  6. first: ?*Node = null, last: ?*Node = null, len: usize = 0
  7. }; 

这利用了结构可以是匿名的事实。 您不需要给他们起个名字。 但是,此功能需要一点包装。 注意这一部分:

  1. pub const Node = struct { prev: ?*Node = null, next: ?*Node = null, data: T,}; 

这里有许多Zig特定功能在起作用,需要一些解释。 在Zig中,可以在定义结构时将值分配给结构成员。 成员可以是在编译时或运行时存在的字段。 上一个:?* Node = null是结构字段的一个示例,该字段在编译时存在,但其默认值为null。 那疯狂的*前缀呢?

在Zig中,* Node表示类似于C / C ++的指向Node类型对象的指针。但是,由于Zig除非明确允许,否则不允许指针为null,因此必须添加?。指示指针可以为空。

节点本身被设置为周围匿名结构的字段。 但是,由于将其定义为const,因此仅在编译时存在。 如果在运行时检查LinkedList结构的内存,则找不到与Node对应的区域。

另外请记住,虽然您可以在编译时将类型用作任何其他对象,但它们在运行时在Zig中并不存在。 因此,基本上我们在这里所做的就是创建带有嵌套类型的结构。

让我使用Loris Cro的示例之一进行更好的解释。首先,他创建一个包含点的链表,并将其分配给仅在编译时存在的名为PointList的变量:

  1. const PointList = LinkedList(Point); 

然后,我们可以使用此新创建的类型实例化一个空列表。

  1. var my_list = PointList{}; 

我们不需要为first,last和len指定任何初始值,因为它们具有默认值。

在这里,我们使用嵌套类型创建一个Node对象来保存我们的点数据:

  1. const p = Point.x = 0.y = 2.z = 8 }; 
  2. var node = PointList.Node{ .data = p }; 
  3. my_list.first = &node; 
  4. my_list.last = &node; 
  5. my_list.len = 1

在Zig中模拟接口

尽管Zig没有类或面向对象语言之类的接口的关键字,但我们仍然可以构建自己的运行时多态系统,类似于C程序员多年来所做的那样。

您只需使用函数指针定义结构即可。在Unix内核中,您看到了类似的操作,可以对任何文件描述符进行通用处理,无论它们是文件,套接字还是管道。

  1. typedef struct _File { 
  2. void (*write)(void *fd, char *data); 
  3. void (*read)(void *fd, char *buffer, int size); 
  4. void (*close)(void *fd); 
  5. File; 

这并不完全是它的定义方式。 我只是从内存中做到这一点。 这允许我们做的是为文件,套接字和管道提供不同的打开功能。 但是,由于它们都给了我们File结构,因此其他函数可以使用其包含的这些函数指针对此进行操作,从而抽象出底层结构的差异。

在Zig中,我们使用Nathan Michaels在此处更详细描述的类似方法。 Zig比C提供了更好的功能,因此您会看到Zig在创建通用迭代器,分配器,读取器,写入器以及在Zig中进行更多操作时使用了更多功能。

有人可能会问,为什么不将这类内容纳入语言呢?如果您曾经使用过Lua,那么您将了解一些优点,而不是给您构造块以创建面向对象的系统,而不是对其进行硬接线。

使用Zig,您可以构建C ++风格的面向对象的系统,类似于Go甚至是类似于诸如Objective-C之类的更动态语言的面向对象编程。

这种自行开发的方法可能非常有效。我们已经看到LISP程序员使用它来在LISP中构建面向对象的编程系统,甚至创建类似于Julia的多调度系统。

 

责任编辑:赵宁宁 来源: 今日头条
相关推荐

2021-11-09 09:00:00

Docker开发工具

2009-10-12 14:31:24

红帽虚拟化

2020-12-04 09:41:36

C编程语言替换C

2021-11-16 14:21:02

React 开发 Beta

2022-02-08 11:45:03

PiniaVuex前端

2012-07-20 09:37:00

Oracle LinuCentOS

2023-07-11 18:36:20

CloudReady开源

2011-04-12 09:13:51

OpenIndianaSolaris替代品

2020-08-23 12:15:38

技术研发指标

2021-07-16 22:49:50

PiniaVuex替代品

2013-10-30 16:40:45

2022-08-02 10:45:29

AppFlowyNotion开源

2021-07-19 22:31:39

微信微信客服移动应用

2022-09-26 09:39:30

LinuxsystemdWSL

2023-08-29 18:49:41

2020-12-09 11:10:58

GitHub代码开发者

2013-09-16 11:10:30

2021-10-19 09:00:00

KubeMQKubernetes工具

2016-09-13 15:50:24

TurtlEvernote开源

2020-02-17 21:35:21

JoplinEvernote开源
点赞
收藏

51CTO技术栈公众号