2312d,D语言单元测试等

原文

我最近决定在"系统编程"领域试些小众语言.我已用了Java,DartKotlin高级语言编程多年了(并试了许多其他相同级别或更高级的语言),需要扩大视野,因为对某些类型应用,这些语言并不是最好的工具.

这篇博文中,我想重点介绍D语言这里,经过一些初步实验,它比其他语言更能引起注意.

我还尝试了ZigNim12,但觉得它们不适合我,至少现在是这样.

当然,我已试过Rust,但是Rust虽然在很多方面都是个天才语言,但并没有真正让我对编写代码感到兴奋.相反,一想到周末要花时间与借用检查器作斗争,我就充满了恐惧.

我绝对会在工作环境中使用Rust(且已这样了),因为它的安全保证(不仅是内存安全,还有资源和线程安全)和出色的性能(在低内存消耗原始速度方面),但对业余爱好项目,谢谢.

在我看来,Nim(另一非常有趣语言),在另一端走得太远了,安全不如速度和快乐重要.因此,如果你喜欢它(速度非常快,创建微小二进制文件且使用很少的内存),它可能只是适合你的语言.

Zig有很多承诺,但目前还没有准备好.尽管它专注于简单性,但也非常冗长且难以正确使用.

D似乎是个很好的平衡.它很容易熟悉,同时有些非常有趣的功能.它已存在了足够长的时间,已足够稳定.

本文,我想分享我所学到的东西,重点元编程单元测试.

D简介

2023年,D并不是一门新语言.它自2001年以来一直存在,但从那时起已有了很大的发展,特别是自2010年左右的D2版本稳定以来.

它有3个不同且维护良好的编译器,下载页:

1,DMD是用D自身编写的参考编译器.

2,GDC是D的GCC前端.

3,基于LLVMLDC.

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.drdmd(DMD自带),从源码运行D程序.甚至可按脚本运行:

cpp 复制代码
#!/usr/bin/env rdmd

它显示了许多有趣的特征.

1,test.writelnwriteln(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 
...一堆错误略...

很有意思!

另一个常见用例是只运行单个测试,编译器不支持,但你可自己做,正如@jfondrenD论坛所示:

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特征是元编程,如果你来自RustScala,概念可能不一样)和UDA(编译时注解)这里.
dub包管理

最后

IDE支持似乎还不错,但与Java,Kotlin,Typescript甚至Rust等主流语言相去甚远.

我首先试使用emacs(你需要获得d模式,然后安装serve-d这里,LSP服务器,也支持VSCode的D支持).

然后注意到DIntelliJ插件非常强大,并且作为Jebrains产品的大用户,很好惊喜(一般,小众语言在IntelliJ中没有很好的支持)!

IntelliJ插件的开发者致敬!它提供了非常好的开箱即用体验,来生成片段漂亮模板,代码浏览(包括进入Dstdlib,非常适合学习),内置文档风格精美,有扫描器,因此在代码中显示警告,通过dfmt自动格式化,内置支持dub.

如果用d-unit作为依赖,甚至可运行测试.

D,还支持CPU内存分析,及非常好的文档工具ddoc这里,与在Rust中一样,可在编译时执行D文档,确保文档示例总是有效!

我厌倦了像Java这样基于VM的语言,并且不太喜欢编写Rust,D可能会成为我下个最喜欢的语言.

相关推荐
fqbqrr10 天前
2411d,右值与移动
d
fqbqrr5 个月前
2407d,D2024三月会议
d
fqbqrr8 个月前
2403d,d的com哪里错了
d
fqbqrr10 个月前
2402d,d的变参
d
fqbqrr10 个月前
2401d,ddip1027如何支持sql
d
fqbqrr10 个月前
2401d,讨论d串滑动参数
d
fqbqrr1 年前
2312d,d语言作为胶水,用C++调用rust
c++·rust·d
fqbqrr1 年前
2312d,把alloca注入调用者域
d
fqbqrr1 年前
2312d,d语言来绑定C++和rust
c++·rust·d