Zig是期待已久的C语言替代品吗

2020-11-08 09:29:42

与C++、D、Java、C#、Go、Rust和Swift等C语言竞争者相比。

在很多方面,我的整个编程生涯感觉就像是漫长的等待,等待C语言的替代品。20年前,我还以为那是C++。随着时间的推移,我认识到C++是一个复杂的怪物,无论我读了多少厚厚的最佳实践书籍,它永远不会被驯服。

我认为Yossi Krein用他的C++常见问题解答很好地总结了我讨厌C++的一切。

因此,当我是一名职业C++程序员时,我总是在寻找替代方案。第一个有希望的替代方案是D。D最初看起来很有前途,但仔细观察后,我发现D实际上只是一个根本不好的想法的一个清理版本。C++的关键问题之一是其语言设计的厨房水槽方法。这里面的东西实在太多了。

在用C和Lua实现一个简单的游戏引擎时,我开始意识到,与C++相比,同时理解这两种语言的脑力开销实际上更小。它让我重新爱上了C,尽管它有很多局限性,但C是一种相当简单的语言,它给了你很大的控制力。

Java和C#在很多方面都只是对C++进行重新散列的尝试。他们可能会让事情变得更简单,但最终有点过于沉迷于90年代的虚拟机和面向对象编程的炒作。我不是说Java或C#不好,但我真的不喜欢。这在很大程度上可能与那些支持臃肿的IDE和过度工程化的语言的社区有更多的关系。

与C++、D、Java和C#的过度使用相比,离开谷歌是值得欢迎的。它把我们带回了起点--C。Go重新想象,如果C没有走上C++的道路,它会是什么样子。我们得到的是一种简单的语言,它解决了我在C语言中一直存在的许多问题。

但故事还没有结束。紧跟着Go,我们找到了铁锈。起初,我认为铁锈真的是D应该一直扮演的角色。真正重新思考C++应该是什么样子。Ruust保持了低级控制、高级抽象机制和手动内存管理,但增加了无与伦比的类型安全性。这一切看起来都太好了,简直不像是真的。

坦率地说,我认为是这样的。我记得我在大约两天内就能在围棋中写出一些像样的程序。朱莉娅,我现在最喜欢的也有些相似。另一方面,学习Rust感觉很像学习Haskell。简单地说,在你做任何有用的事情之前,你需要理解很多概念和理论。

如果说C++教会了我什么的话,那就是重视简单性,而这不是Rust所做的。

后来,我从互联网上许多Rust用户的评论中了解到,Rust重复了C++的大罪之一。它的编译时间非常慢。我认为,没有什么比等待C++编译更能破坏我编程的乐趣了。而且听起来铁锈的情况似乎更糟。这是一笔交易的破坏者。

20多年来,我一直是苹果的铁杆粉丝。我喜欢Cocoa的图形用户界面库,在iPhone问世之前很久就开始编写Objective-C程序,突然间每个人和他们的狗都开始编写Objective-C程序了。

是的,Objective-C有点笨重,但它的简洁有一定的美感。与C++不同,它是对C语言的一个相当简约的补充。根据经验,您可以非常快速地教初级开发人员Objective-C。

因此,斯威夫特的亮相让我觉得我已经达到了编程涅槃的地步。最后,一种真正的现代语言,它与Objective-C很好地集成在一起,所以我们仍然可以使用像Cocoa这样令人敬畏的Apple库。

斯威夫特借鉴了拉斯特的许多想法,在很多方面,我认为我们终于有了一种凡人的锈。斯威夫特可以在相当长的时间内学会。

但我在斯威夫特的经历好坏参半。即使到了今天,我仍然很难准确地表达出这门语言的错误之处,因为它似乎把很多事情都做对了。

我把一个iPhone应用程序从Objective-C移植到了SWIFT。我的一个很好的经历是,Swift发现了一大堆漏洞,这仅仅是因为严格的类型系统捕捉到了Objective-C中看不见的问题,而Objective-C对类型的松懈是出了名的,至少在编译时是这样。

与C++、C#和Java相比,我认为Swift是更好的语言。我几乎所有关于C++的具体问题都是由SWIFT解决的。但我意识到,每次我花一些时间玩围棋,编程围棋都比斯威夫特有趣得多。然而,Go有一些糟糕的错误处理,它重复了拥有空指针的数百万美元的错误。斯威夫特避开了这两个问题。

上一次我和朱莉娅待了很长时间后回到斯威夫特,我更加清楚地看到了斯威夫特的一些问题:

继承自Objective-C的受Smalltalk启发的语法在面向对象编程中工作得很好,但对于函数式编程来说简直是糟糕透顶。在将函数用作第一类对象时,您不想费心确保获得正确的参数名称。

面向对象和函数式编程之间的不安休战。斯威夫特正试图为两个不同的主人服务,并为此付出了代价。在进行函数式编程时,您希望您的功能主要是自由函数。它们在功能设置中更容易传递和使用。

但SWIFT最终主要迎合了OOP人群,将功能主要放在了方法上。一旦您完成了大量的函数式编程,这就变得不和谐了。

因此,Swift从未真正成为我的终极通用编程语言。如果我想在更高的抽象层次上编程,获得高性能并完成任务,我会选择Julia。

但这仍为类似C的替代方案留下了空白。Julia不能真正取代C语言。它占用内存,不能生成小的二进制文件,不适合用来制作其他语言可以使用的库。你不会想用它来创建操作系统内核或进行微控制器编程。

Go和Rust都非常接近成为C的替代品。Go获得了使用C的简单性和很多感觉,但它使用了垃圾回收,这并不能完全取代C。在Go中仍然可以比在Java中更多地控制内存使用,这一点都不值得,因为您有指针,并且实际上可以创建自己的二级分配器。

Rust减少了手动内存分配,但未能复制C语言的简单性和感觉。是否有什么东西填补了这两种语言之间的空白?

确实有。这正是我所认为的Zig。Zig比Go复杂,但学习和使用起来比Rust简单得多。

但这样对Zig的概括并不公正。Zig为桌面带来了很多新的想法,这很有意义,这使得编码Zig的体验相当独特。但在深入讨论这一问题之前,让我们先来看看基础知识。

如果我们要学习另一种类似C的语言,我们就不能重复C++最糟糕的情况,比如糟糕的编译时间。Zig的表现如何?

我遇到了亚历山大·梅德韦德尼科夫(Alexander Medvednikov)的测试,他是V编程语言的创造者。这是一个用400K函数编译文件的测试:

在这一点上,铁锈、斯威夫特和D都失败了。梅德韦德尼科夫用更少的行数对这些语言做了进一步的测试,而拉斯特的测试结果也像预期的那样最差。

正如你在榜单上看到的,齐格是明星表演者之一。尽管很难不注意到,V语言在不到一秒的时间内就完成了所有这些工作。这提醒我要更详细地探索V。快速浏览一下就会发现,它在某种程度上与手动内存分配、泛型和可选项(必须显式允许空指针)配合使用。

如果不执行手动内存管理,您就不能成为C争用的对象。做C风格编程的人想要这样。如果我不需要,我会给朱莉娅编程。

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

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

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

Fn decimals(alloc:*Allocator,n:u32)!array(U32){var x=n;var digits=Array(U32).init(Alloc);errdefer digits.deinit();While(x&>;=10){try digits.append(x%10);x=x/10;}try digits.append(X);return digits;}。

请注意,用于保存单个小数位的数组数字必须使用分配器进行分配,该分配器作为DECIMALS函数的参数提供。

这才是Zig真正闪耀的地方。在C语言中,确保不会忘记释放内存是很困难的,而且很容易在错误的位置进行释放。Zig复制了围棋中的延迟概念。但除了推迟之外,它还出现了错误。如果您不知道GO,那么DEFER基本上就是一种将代码行或代码块的执行推迟到函数退出的方式。

这有什么了不起的?因为它允许您确保运行一些代码,而不管在退出函数之前使用了什么复杂的if-Else-语句。

上面的代码行是对正常GO延迟的一种改进,因为它只在返回错误代码的情况下才会执行。因此,如果一切正常,那么它将永远不会运行。

在调用点,我们将使用正常的延迟来确保我们不会忘记释放为我们的数字分配的内存。

从我玩Zig的有限经验来看,我想说这是一个相当好的系统。通过结合使用分配器和延迟,您可以非常清楚地知道要在何处分配和释放内存,同时也可以轻松地正确地进行分配和释放。

破坏了许多类C语言的是它们不能很好地与C打交道。我的意思是,从C语言调用C函数应该很容易,从C语言调用语言上的函数也应该很容易。

此外,通常的编程方式应该与C语言完全兼容,这样就不必创建大型的C抽象层。例如,C++对C语言不是很友好,因为典型的C++库不能在没有广泛包装的情况下从C中使用。

然而,Zig型如果对C非常友好,因为没有任何奇怪的东西暴露在C中不能得到的地方。结构中没有vtable(C++中的表到虚函数)。没有C不知道如何调用的构造函数或析构函数。也没有任何例外,C语言也很难捕捉到。

使用Zig中的C是微不足道的。事实上,Zig的创建者会声称Zig比C本身更擅长使用C库。

正如您所看到的,Zig在解析C头文件以及包含C中的类型和函数方面没有任何问题。事实上,Zig是一个完全成熟的C编译器。如果你愿意,你可以用Zig编译你的C程序。

同样,将Zig函数暴露给C也很容易。下面是一个Zig函数,它接受32位整数并返回32位整数。

通过将EXPORT放在它前面,我们使它可以被我们与我们的程序链接的C代码访问。事实上,我们的主函数是在C代码部分定义的,它使用了在Zig中定义的函数。

#include<;stdint.h>;int32_t add(int32_t a,int32_t b);int main(int argc,char**argv){assert(add(42,1337)==1379);返回0;}。

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

事实上,Zig让它变得更容易,它允许你自动将C程序翻译成Zig代码。这内置于Zig编译器中:

当然,这段代码不会是最优的,而且可能有点乱。但这有点像使用谷歌翻译进行自然语言翻译。这是一个很好的起点,为你节省了大量体力劳动。您可以稍后手动修改细节。

吸引很多人使用C编程的首先是极简主义。这就是围棋的正确之处,并使编程成为一种乐趣。你可以很容易地把整个程序记在脑子里。

现在,如果你开始阅读Zig,看看我在这里给你的源代码例子,它可能看起来很复杂。有些语言结构可能看起来很奇怪。人们很容易就会觉得这是一门复杂的语言。

因此,澄清Zig不需要展示它有多小的所有事情实际上是很有用的:

没有函数重载。不能多次使用不同的参数编写同名函数。

不过,Zig巧妙地使用了几个核心功能,可以提供大致相同的功能:

ZIG代码可以在编译时进行部分求值。您可以使用Comptime关键字将代码标记为在编译时可执行。

函数和类型可以与结构相关联,但不是物理存储在结构中,因此对C代码是不可见的。

例如,通过利用在编译时运行代码的能力,在Julia中创建了类似于模板的东西。洛里斯·克罗有一篇很好的文章更详细地描述了这一点。我只会用那篇文章中的一个例子来快速说明这个想法。

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

Fn LinkedList(comptime T:type)type{return struct{pub const Node=struct{prev:?*Node=NULL,Next:?*Node=NULL,Data:T,};First:?*Node=NULL,Last:?*Node=NULL,len:usize=0,};}

这利用了结构可以是匿名的这一事实。你不需要给他们起名字。不过,这个函数需要稍微解包一下。请注意这一部分:

Pub Const Node=struct{prev:?*Node=NULL,Next:?*Node=NULL,Data:T,};

这里有一些Zig特有的特征需要解释一下。在Zig中,您可以在定义结构时为结构的成员赋值。成员可以是编译时或运行时存在的字段。Prev:?*Node=NULL是一个结构字段的例子,该字段在编译时存在,但其缺省值为NULL。那疯狂呢?*前缀呢?

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

节点本身被设置为周围匿名结构的一个字段。但是,因为它被定义为常量,所以它只在编译时存在。如果在运行时检查LinkedList结构的内存,则不会发现与Node对应的区域。

还要记住,虽然您可以在编译时将类型用作任何其他对象,但在运行时Zig中并不存在类型。所以基本上我们在这里做的是创建一个嵌套类型的结构。

让我用洛里斯·克罗(Loris Cro)的一个例子来更好地解释。首先,他创建了一个包含点的链表,并将其赋给一个仅在编译时存在的变量PointList:

我们不需要为first、last和len指定任何初始值,因为它们都有缺省值。

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

Const p=Point{.x=0,.y=2,.z=8};var node=PointList.Node{.Data=p};my_list.first=&;node;my_list.last=&;node;my_list.len=1;

虽然Zig没有类的关键字,也没有像面向对象语言那样的接口,但我们仍然可以构建自己的运行时多态性系统,类似于C程序员多年来一直在做的事情。

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

Tyfinf struct_File{void(*WRITE)(void*fd,char*data);void(*read)(void*fd,char*Buffer,int Size);void(*Close)(void*fd);}文件;

这并不完全是它被定义的方式。我只是凭记忆这么做的。这使得我们可以为文件、套接字和管道提供不同的打开功能。但由于它们都给我们返回了一个文件结构,其他函数可以使用它包含的这些函数指针对其进行操作,这就消除了底层结构中的差异。

在Zig中,我们使用了内森·迈克尔斯(Nathan Michaels)在这里更详细地描述的类似方法。Zig提供了比C更好的特性,所以你可以看到它更多地用于创建泛型迭代器、分配器、读取器、写入器以及Zig中的更多东西。

有人可能会问,为什么不把这些东西融入到语言中呢?如果您曾经使用过Lua,您就会知道为您创建面向对象的系统而不是硬连接它的构建块的一些好处。

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

这种自行其是的方法可能非常强大。我们已经看到LISP程序员使用它在LISP中构建面向对象的编程系统,甚至创建了类似Julia的多分派系统。

老实说,这个故事有点不成熟,因为我意识到Zig是一个太大的话题,不能在一些老奶奶的广泛报道中涵盖。我可以谈论的话题还有很多,但这只会让这个故事变得过于冗长。相反,我将尝试在未来写几篇较小的文章,重点关注Zig的不同方面。