我最近决定在"系统编程"
领域试些小众语言
.我已用了Java,Dart
和Kotlin
等高级语言编程
多年了(并试了许多其他相同级别
或更高级的语言),需要扩大视野,因为对某些类型
应用,这些语言
并不是最好的工具.
这篇博文中,我想重点介绍D语言
这里,经过一些
初步实验,它比其他语言
更能引起注意.
我还尝试了Zig
和Nim
12,但觉得它们不适合
我,至少现在
是这样.
当然,我已试过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
可能会成为我下个
最喜欢的语言
.