新智元报道
来源:theartofmachinery
编辑:大明
作为开发人员,换开发语言其实是一件需要很高成本的事,本文主要讲讲我是怎么从C语言转投D语言的。
其实我的经历和许多系统程序员的故事差不多。曾经有一段时间,C是大多数程序员的首选语言。有一天,我意识到我的大多数C程序都在重新实现C++中的东西:动态数组、更好的字符串、多态类等等。所以我尝试使用C++,起初我很喜欢它。RAII、类、泛型等新的组件和概念让编程再次变得有趣起来。
我曾经想象过,如果我把所有关于C ++的书籍都看一遍,并掌握了模板元编程之类的东西,我说不定会成为系统编程的全能之神,我写的代码会让人大吃一惊。但事后看来,学习也可能最终会产生更多相反的效果: 我写出的代码实际上变得更糟。
总之全能之神当不上了,我很伤心。
我记得我读过Scott Meyer著名的《Effective C ++》,这本书其实更多讲的是指出低效率的C ++编程的问题,我发现自己写的大多数C++代码都对上了号。让我们面对现实吧:C可能很难用,但它确实足够“优雅”,而提到C++,你很难跟“优雅”搭上边。
很多前C ++程序员最终都用回了C。就我而言,我发现了D语言。其实D也不完美,但是我使用它因为它让我感觉更像是C++应该有的样子(C+=1)。比如以下面这个简单的C程序为例(一加一等于几?):
int main()
{
printf("1 + 1 = %d!\n", 1 + 1);
return 0;
}
如果使用C++标准库,代码是这样的:
#
int main()
{
std::cout << "1 + 1 = " << 1 + 1 << "!" << std::endl;
return 0;
}
如果使用D语言,代码是这样的:
import std.stdio;
void main()
{
writef("1 + 1 = %d!\n", 1 + 1);
}
这个例子虽然浅显,但它体现出了C++和D之间背后理念的一些区别。
这篇关于C ++成员函数指针的文章也是对D的起源的一个很好的解释。如果你酷爱编程,这篇文章是一个很好的解读, 但我的解读是:C++成员函数指针应该是感觉像是一个低级功能(就像普通函数指针一样),但其实现的复杂性和多样性说明它们真的很“高级”。
这些指针的实现过程很复杂,因为关于它们能做什么/不能做什么的规则是很微妙的。作者解释了几个C ++编译器的实现,包括优雅而简单的Digital Mars C ++实现,即DMC。DMC编译器是由Walter Bright编写的,他是“D语言”的发明者。
D具有C ++的类和模板以及其他核心功能,但设计者花费了大量时间思考C ++规范,以及如何让设计和编程变得更简单。Walter曾经说过,他在部署C ++模板的痛苦经历,让他考虑过根本不把该功能纳入D,后来他意识到,这个过程本来不需要那么复杂。
下面对D语言的功能和特点进行一番大概的介绍,其实可以把D视作一个“改进版”的C语言。介绍中时刻少不了和C/C++的对比。
-betterC开关
D编译器支持-betterC开关,该开关可以启用/禁用D运行时以及依赖于它的所有高级功能。上面的C代码可以直接转换为betterC:
import core.stdc.stdio;
extern(C):
int main()
{
printf("1 + 1 = %d!\n", 1 + 1);
return 0;
}
$ dmd -betterC example.d
$ ./example
1 + 1 = 2!
生成的二进制文件看起来很像等效的C二进制文件。事实上,如果你在betterC中重写了一个C库,仍然可以链接到已经对C版本编译的代码,无需修改就可立即使用。
实际上,如果只是要在D语言中编写类似C的代码,并不需要-betterC开关。只有在没有D Runtime的特殊情况下才需要使用。
静态assert()
这个功能允许开发者在编译时验证一些假设。
static assert(kNumInducers<16);
系统代码通常对对齐或结构大小或其他事物做出假设。使用静态assert不仅可以记录这些假设,而且如果有人通过添加struct成员或其他东西来破坏假设,则会触发编译错误。
Slices
典型的C代码中存在大量的“指针/长度”参数对,一个常见bug就是二者的不同步。对于由指针和长度定义的一系列内存,Slice是一种简单且超级有用的抽象表示。现在不必使用这样的代码:
buffer_p += offset;
buffer_len -= offset; // Got to update both
而可以用下边这种更不容易出bug的代码:
buffer = buffer[offset..$];
Slice 其实就是具备优秀语法功能的指针/长度对。
编译时间函数估计 (CTFE)
许多函数都可以用编译时间来评估。
long factorial(int n) pure
{
assert (n >= 0 && n <= 20);
long ret = 1;
foreach (j; 2..n+1) ret *= j;
return ret;
}
// Statically allocated array
// Size is calculated at compile time
Permutation[factorial(kNumThings)]permutation_table;
scope Guards
函数的一部分中的代码通常会在后续部分带上一段清理代码。一个常见的错误来源是未能正确匹配该代码,(尤其是涉及多个控制流路径时)。D的scope guards设定使得这个问题变得不再困难:
p = malloc(128);
// free() will be called when thecurrent scope exits
scope (exit) free(p);
// Put whatever if statements, or loops,or early returns you like here
你甚至可以在作用域中使用多个scope,或嵌套使用scope。清理代码将在需要时以正确的顺序被调用。
D语言还利用结构析构函数支持RAII。
常量和不可变量
有一个流行的说法是,C和C++中的const对编译器优化很有用。不过D的作者表示,每当他想到一个新的基于const的C++优化时,最终都发现它在实际代码中并不起作用。所以他对D的const语义做了一些修改,并添加了不可变量。可以在D const FAQ中阅读更多内容。
函数纯度
可以实施函数纯度功能。我之前写过关于pure关键字的一些好处。
@Safe
SafeD是D的一个部分,禁止使用指针类型转换和内联汇编等高风险语言功能。标记为@safe的代码由编译器强制执行,不使用这些功能,因此高风险代码可以仅限需要这些功能的应用程序的一小部分。
元编程
如前所述,元编程在一些C ++程序员中名声不好。但是D中的元编程具备一些没那么有趣的优点,程序员一般倾向于只在必要时才用,而不是一个有趣的谜题。
需要将枚举类型的名称作为数组?容易!
enum State
{
stopped,
starting,
running,
stopping,
}
string[] state_names =[__traits(allMembers, State)];
没有预处理器
好吧,这其实是一个“非功能”,但D没有相当于C的预处理器的功能。所有理智的用例都被替换为本机语言功能,如清单常量和模板。这包括适当的模块支持,这意味着D可以摆脱旧#include黑客的限制。
关于D语言的更多内容,可查看D语言作者Walter Bright的更详细的介绍:
https://dlang.org/blog/2018/06/11/dasbetterc-converting-make-c-to-d/
参考链接:
https://theartofmachinery.com/2019/04/05/d_as_c_replacement.html