我最近决定在"系统编程"领域试些小众语言.我已用了Java,Dart和Kotlin等高级语言编程多年了(并试了许多其他相同级别或更高级的语言),需要扩大视野,因为对某些类型应用,这些语言并不是最好的工具.
这篇博文中,我想重点介绍D语言这里,经过一些初步实验,它比其他语言更能引起注意.
我还尝试了Zig和Nim12,但觉得它们不适合我,至少现在是这样.
当然,我已试过Rust,但是Rust虽然在很多方面都是个天才语言,但并没有真正让我对编写代码感到兴奋.相反,一想到周末要花时间与借用检查器作斗争,我就充满了恐惧.
我绝对会在工作环境中使用Rust(且已这样了),因为它的安全保证(不仅是内存安全,还有资源和线程安全)和出色的性能(在低内存消耗和原始速度方面),但对业余爱好项目,谢谢.
在我看来,Nim(另一非常有趣语言),在另一端走得太远了,安全不如速度和快乐重要.因此,如果你喜欢它(速度非常快,创建微小二进制文件且使用很少的内存),它可能只是适合你的语言.
Zig有很多承诺,但目前还没有准备好.尽管它专注于简单性,但也非常冗长且难以正确使用.
D似乎是个很好的平衡.它很容易熟悉,同时有些非常有趣的功能.它已存在了足够长的时间,已足够稳定.
本文,我想分享我所学到的东西,重点是元编程及单元测试.
D简介
在2023年,D并不是一门新语言.它自2001年以来一直存在,但从那时起已有了很大的发展,特别是自2010年左右的D2版本稳定以来.
它有3个不同且维护良好的编译器,下载页:
1,DMD是用D自身编写的参考编译器.
2,GDC是D的GCC前端.
3,基于LLVM的LDC.
DMD一般用来更快编译(事实上,它可能是生产级语言中最快编译器之一),但其他两个一般更擅长优化运行时速度.
在介绍D的功能方面D语言旅游做得非常出色,而D的Gems部分特别有趣,因为它展示了D有的,而大多数其他语言所没有的东西,如(UFCS)统一函数调用语法,域保护,(CTFE)编译时函数求值,(如@safe,@nogc,@mustuse)属性等等.
另见包括消息传递和线本存储的多线程节,用它们来共同支持使用类似Actor模型来编写并发代码.
讨论更高级功能前,先展示一些D示例.
下面显示了D切片的实际效果:
cpp
import std.stdio : writeln;
void main()
{
int[] test = [ 3, 9, 11, 7, 2, 76, 90, 6 ];
test.writeln;
writeln("First element: ", test[0]);
writeln("Last element: ", test[$ - 1]);
writeln("Exclude the first two elements: ",
test[2 .. $]);
writeln("Slices are views on the memory:");
auto test2 = test;
auto subView = test[3 .. $];
test[] += 1; //将每个元素递增1
test.writeln;
test2.writeln;
subView.writeln;
//创建空切片
assert(test[2 .. 2].length == 0);
}
编译并运行它:
cpp
dmd -of=slices slices.d
./slices
[3, 9, 11, 7, 2, 76, 90, 6]
First element: 3
Last element: 6
Exclude the first two elements: [11, 7, 2, 76, 90, 6]
Slices are views on the memory:
[4, 10, 12, 8, 3, 77, 91, 7]
[4, 10, 12, 8, 3, 77, 91, 7]
[8, 3, 77, 91, 7]
还可直接dmd -runfile.d或rdmd(DMD自带),从源码运行D程序.甚至可按脚本运行:
cpp
#!/usr/bin/env rdmd
它显示了许多有趣的特征.
1,test.writeln与writeln(test)相同.这就是UFCS.
2,test[$-1],显示了如何在[]中按数组/切片长度使用$符号.
3,test[2..$],类似同样使用$的Go的典型切片.
4,test[]+=1,显示了可由编译器优化的向量运算.
5,assert(test[2 .. 2].length == 0);,D断定,稍后用来测试单元.
相当不错.
D元编程
D有许多元编程功能.元编程是针对程序自身编程的编程.
Lisp可能是使用宏元编程的先驱,但宏并不是元编程的唯一方法.
如,如下例所示,D有允许在编译时检查类型的模板,以特化函数:
cpp
@safe:
auto concat(T)(T lhs, T rhs) {
static if (is(T: double)) {
//T可转换为双精
return lhs + rhs;
} else {
//'~'一般是D中的`连接`符号
return lhs ~ rhs;
}
}
unittest {
assert(2.concat(3) == 5);
assert(4.2.concat(0.8) == 5.0);
assert("Hello".concat(" D") == "Hello D");
}
运行单元测试:
cpp
dmd -w -main -unittest -run tests.d
//1个模块通过单元测试
该示例有点傻,因为D支持重载符号这里,所以只能这样.
如果熟悉Java,concat类似通用静态方法,但与Java不同,D允许编译时检查类型,因此可专门针对某些类型特化函数.
static if是编译时执行的if块,运行时不存在.
注意,模板有两个参数列表:一个包含编译时参数,另一个包含运行时参数.如果D编译器可推导编译时参数,则可省略它.
可用!符号显式提供编译时参数.
如,std.conv标准模块中的to模板,把类型当参数,但因为一般无法推导,因此几乎总是显式传递:
cpp
unittest {
import std.conv: to;
assert(42.to!string == "42");
}
而这只是最基本的D模板.
还可用template关键字来执行更高级操作,如生成多个函数:
cpp
template BiDirectionalConverter(T1, T2) {
import std.conv: to;
T2 convert(T1 t) {
return t.to!T2();
}
T1 convert(T2 t) {
return t.to!T1();
}
}
unittest {
alias StringIntConv = BiDirectionalConverter!(string, int);
assert(StringIntConv.convert("20") == 20);
assert(StringIntConv.convert(20) == "20");
}
std.conv中的八进制(octal)模板,用来在D中声明编译时的八进制:
cpp
void main() {
import std.stdio: writeln;
import std.conv;
writeln(octal!"750");
}
运行:
cpp
dmd -run tests.d
488
强烈建议浏览D模板教程,以了解更多信息.
D中的另一个模板是插件模板.它是一个允许好像在周围域内编写它一样,直接在调用点粘贴代码的复制和粘贴模板.
cpp
mixin template Abcd(T) {
T a, b, c, d;
}
unittest {
mixin Abcd!int;
a = 10;
assert(a == 10);
assert(b == 0);
assert(c == 0);
assert(d == 0);
}
最后,还可用串插件这里生成代码串:
cpp
///用T类型的`a,b`和c字段构建`一个结构`.
string abcStruct(T)(string name) {
return "struct " ~ name
~ " { "
~ T.stringof ~ " a; "
~ T.stringof ~ " b; "
~ T.stringof ~ " c; "
~ " }\n";
}
unittest {
mixin(abcStruct!string("StringStruct"));
mixin(abcStruct!int("IntStruct"));
auto abcstr = StringStruct("hey", "ho", "let's go");
assert(abcstr.a == "hey");
assert(abcstr.b == "ho");
assert(abcstr.c == "let's go");
auto abcint = IntStruct(42);
assert(abcint.a == 42);
assert(abcint.b == 0);
assert(abcint.c == 0);
}
D可用-mixin标志创建包含编译过程中生成的所有插件的文件:
cpp
dmd -w -main -unittest -mixin=mixins.d -run tests.d
1个模块通过单元测试
现在,查看mixins.d文件,找到D编译器生成的结构:
cpp
//测试.d中扩展.d(67)
struct StringStruct { string a; string b; string c; }
//测试.d中扩展.d(68)
struct IntStruct { int a; int b; int c; }
或,用pragma编译指示,以便编译时D仅打印生成代码:
cpp
pragma(msg, abcStruct!double("DoubleStruct"));
//dmd -w -main -of=tests tests.d
//结果:
struct DoubleStruct { double a; double b; double c; }
更多的mixin技巧
官方D文档中的(Parser)代码生成示例,显示了编译时很容易解析串来生成常量配置数据.
单元测试
前例中,使用unittest块来演示D的一些功能.我想很明显,编译单元中一般不包含这些块中代码,因此编译器运行测试时,必须传递-unittest选项给编译器(要实际运行测试,或执行生成的二进制文件,加上-run选项).
回顾下,如下单元测试:
cpp
unittest {
assert(2 + 2 == 4);
}
把上面的4更改为5并运行代码:
cpp
dmd -w -main -of=tests -run tests.d
使用-main选项,以便编译器在没有函数时生成空main函数.
-w标志,按错误对待警告,-of来命名输出文件.用--help查看所有选项.
如果不打印内容,则所有测试都正常.即没有运行测试.
现在用-unittest重试:
cpp
dmd -w -main -unittest -run tests.d
`tests.d(18):[unittest]unittest`失败
`1/1`模块失败的单元测试
输出非常简单.它只是告诉你有多少模块的测试失败了,及断定失败的文件和行.
对快速测试来说不错,但最好告诉失败的真正原因论坛.
如,这是我想出的一个显示失败断定期望结果和实际结果的小模板,来使断定更强大:
cpp
auto assertThat(string desc, string op, T)(T lhs, T rhs) {
import std.conv: to;
const str = "assert(lhs " ~ op ~ " rhs, \"" ~
desc ~ ": \" ~ lhs.to!string() ~ \" " ~ op ~ " \" ~ rhs.to!string())";
return mixin(str);
}
现在,断定如下:
cpp
unittest {
assertThat!("adding two and two", "==")(2 + 2, 5);
}
运行它:
cpp
dmd -w -main -unittest -run tests.d
`tests.d-mixin-20(20):[unittest]`加二加二`:4==5`
`1/1`模块失败的单元测试
真酷!
顺便,D单元测试一般来验证函数属性是否符合期望(D编译器一般会推导它们,给每个函数手动注解大量属性非常麻烦).
如,在D中实现树时,我试测试:
cpp
@safe @nogc nothrow pure unittest {
auto tree = Tree([0,0,0,2], [10,11,12,13]);
assertThat!("children(2) basic case", "==")(tree.children(2), [3, -1]);
}
仅当按@safe @nogc nothrow pure注解,推导unittest中使用的函数时,才有效(编译器会传递性检查这些函数).
结果如下:
cpp
myd dmd -unittest -run source/app.d
...一堆错误略...
很有意思!
另一个常见用例是只运行单个测试,编译器不支持,但你可自己做,正如@jfondren在D论坛上所示:
cpp
module tester1;
unittest { assert(true); }
unittest { assert(!!true); }
unittest { assert(1 != 1); }
unittest { assert(1 > 0); }
version (unittest) {
bool tester() {
import std.meta : AliasSeq;
import std.stdio : writef, writeln;
alias tests = AliasSeq!(__traits(getUnitTests, tester1));
static foreach (i; 0 .. tests.length) {
writef!"Test %d/%d ..."(i + 1, tests.length);
try {
tests[i]();
writeln("ok");
} catch (Throwable t) {
writeln("failed");
}
}
return false;
}
shared static this() {
import core.runtime : Runtime;
Runtime.moduleUnitTester = &tester;
}
}
void main() {
assert(false); //这不会运行
}
运行它:
cpp
dmd -w -main -unittest -run tests.d
测试1/4...好
测试2/4...好
测试3/4...失败
测试4/4...好
非常整洁,但可能不是你想要的.
这使用了如getUnitTests特征等相当高级的东西(D特征是元编程,如果你来自Rust或Scala,概念可能不一样)和UDA(编译时注解)这里.
dub包管理
最后
IDE支持似乎还不错,但与Java,Kotlin,Typescript甚至Rust等主流语言相去甚远.
我首先试使用emacs(你需要获得d模式,然后安装serve-d这里,LSP服务器,也支持VSCode的D支持).
然后注意到D的IntelliJ插件非常强大,并且作为Jebrains产品的大用户,很好惊喜(一般,小众语言在IntelliJ中没有很好的支持)!
向IntelliJ插件的开发者致敬!它提供了非常好的开箱即用体验,来生成片段的漂亮模板,代码浏览(包括进入Dstdlib,非常适合学习),内置文档风格精美,有扫描器,因此在代码中显示警告,通过dfmt自动格式化,内置支持dub.
如果用d-unit作为依赖,甚至可运行测试.
D,还支持CPU和内存分析,及非常好的文档工具ddoc这里,与在Rust中一样,可在编译时执行D文档,确保文档示例总是有效!
我厌倦了像Java这样基于VM的语言,并且不太喜欢编写Rust,D可能会成为我下个最喜欢的语言.