与以前的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和过度设计的语言的相关社区有关。
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更糟了。那是一个破坏交易的因素。
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并将其拆分为十进制数字:
- 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);
- xx = x / 10;
- }
- try digits.append(x);
- return digits;
- }
请注意,必须使用分配器分配用于保留各个十进制数字的数组数字,该分配器是十进制函数的参数。
这就是Zig真正的光芒所在。 确保您不会忘记分配内存在C语言中很难。而且很容易以错误的位置结束内存。 Zig从Go复制了延迟概念。 但是除了推迟它还有errdefer。 如果您不了解Go,那么从本质上讲,延迟是将行或代码块的执行推迟到函数退出之前的一种方法。
为什么这么好? 因为它使您可以确保某些代码得以运行,而不管退出该函数之前使用了什么复杂的if-else语句。
- errdefer digits.deinit();
上面的行与正常的Go延迟有所不同,因为只有在返回错误代码的情况下,它才会执行。因此,如果一切正常,那么它将永远不会运行。
在呼叫站点,我们将使用常规延迟来确保我们不会忘记释放分配给数字的内存。
- const digits = try decimals(allocator, 4123);
- defer digits.deinit();
- for (digits.items) |digit| {
- 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库。
- const c = @cImport({
- @cDefine("_NO_CRT_STDIO_INLINE", "1");
- @cInclude("stdio.h");
- });
- pub fn main() void {
- _ = c.printf("hello\n");
- }
如您所见,Zig解析C头文件并包含来自C的类型和函数没有问题。实际上Zig是完全成熟的C编译器。您可以根据需要使用Zig编译C程序。
同样,将Zig函数暴露给C也很容易。这是一个Zig函数,采用32位整数并返回32位整数。
- export fn add(a: i32, b: i32) i32 {
- return a + b;
- }
通过将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);
- return 0;
- }
这意味着您可以轻松地开始将较大的C程序的某些部分转换为Zig并继续进行编译。 移植程序时,这是一项非常强大的功能。 过去让我很容易地从Objective-C移植到Swift的原因是,我可以一次用Swift版本替换一个Objective-C方法,进行编译并看到一切仍然有效。
实际上,通过允许您自动将C程序转换为Zig代码,Zig使其变得更加容易。 这是Zig编译器内置的:
- $ 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的函数,该函数只能在编译时调用,该函数采用链表中元素的类型,然后返回包含以下元素的链表类型:
- 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中,可以在定义结构时将值分配给结构成员。 成员可以是在编译时或运行时存在的字段。 上一个:?* Node = null是结构字段的一个示例,该字段在编译时存在,但其默认值为null。 那疯狂的*前缀呢?
在Zig中,* Node表示类似于C / C ++的指向Node类型对象的指针。但是,由于Zig除非明确允许,否则不允许指针为null,因此必须添加?。指示指针可以为空。
节点本身被设置为周围匿名结构的字段。 但是,由于将其定义为const,因此仅在编译时存在。 如果在运行时检查LinkedList结构的内存,则找不到与Node对应的区域。
另外请记住,虽然您可以在编译时将类型用作任何其他对象,但它们在运行时在Zig中并不存在。 因此,基本上我们在这里所做的就是创建带有嵌套类型的结构。
让我使用Loris Cro的示例之一进行更好的解释。首先,他创建一个包含点的链表,并将其分配给仅在编译时存在的名为PointList的变量:
- const PointList = LinkedList(Point);
然后,我们可以使用此新创建的类型实例化一个空列表。
- var my_list = 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中模拟接口
尽管Zig没有类或面向对象语言之类的接口的关键字,但我们仍然可以构建自己的运行时多态系统,类似于C程序员多年来所做的那样。
您只需使用函数指针定义结构即可。在Unix内核中,您看到了类似的操作,可以对任何文件描述符进行通用处理,无论它们是文件,套接字还是管道。
- typedef struct _File {
- void (*write)(void *fd, char *data);
- void (*read)(void *fd, char *buffer, int size);
- void (*close)(void *fd);
- }
- File;
这并不完全是它的定义方式。 我只是从内存中做到这一点。 这允许我们做的是为文件,套接字和管道提供不同的打开功能。 但是,由于它们都给了我们File结构,因此其他函数可以使用其包含的这些函数指针对此进行操作,从而抽象出底层结构的差异。
在Zig中,我们使用Nathan Michaels在此处更详细描述的类似方法。 Zig比C提供了更好的功能,因此您会看到Zig在创建通用迭代器,分配器,读取器,写入器以及在Zig中进行更多操作时使用了更多功能。
有人可能会问,为什么不将这类内容纳入语言呢?如果您曾经使用过Lua,那么您将了解一些优点,而不是给您构造块以创建面向对象的系统,而不是对其进行硬接线。
使用Zig,您可以构建C ++风格的面向对象的系统,类似于Go甚至是类似于诸如Objective-C之类的更动态语言的面向对象编程。
这种自行开发的方法可能非常有效。我们已经看到LISP程序员使用它来在LISP中构建面向对象的编程系统,甚至创建类似于Julia的多调度系统。