D 编程语言

作者:Ameer Armaly

在过去的几十年里,编程语言已经取得了长足的进步。与 UNIX 和 C 语言的初期,编译型语言刚刚起步时相比,今天的世界充满了各种具有不同目标和特性的语言。在本文中,我将讨论这样一种语言,即 Digital Mars 公司的 D 语言。D 是一种原生编译、静态类型、多范式、类 C 语言。它的目标是通过结合 C 语言的性能和灵活性以及 Ruby 和 Python 等语言的生产力提升因素,来简化任何类型应用程序(从内核到游戏)的开发。它最初由 Walter Bright 构思,Walter Bright 是为 DOS 编写第一个 ANSI C++ 编译器的人。参考编译器可以为 Windows 和 Linux 免费下载,前端在双重 GPL 和 Artistic 许可证下获得许可。

GDC 是一个 D 编译器,它使用 GCC 后端,并根据 GPL 协议分发。D 的特性包括没有预处理器、垃圾回收器、灵活的一等数组、契约、内联汇编程序等等。尽管如此,它仍然保持与 C 的 ABI 兼容性,使您可以轻松地在 D 中使用所有旧的 C 库,而无需太多工作。D 标准库也包括所有标准 C 函数的绑定。

Hello World

在 D 语言中,Hello World 程序是这样的

import std.stdio;  // standard i/o module
int main(char[][] args)
{
      writefln("Hello world!");
      return 0;
}

writef 是 D 语言类型安全的 printf 版本;writefln 在末尾添加一个换行符。垃圾回收器 D 包含一个自动垃圾回收器,使程序员无需显式地管理内存。这使程序员可以更专注于手头的任务,而不是担心每个内存块的状态。此外,它还消除了一整类与悬空指针和无效内存引用相关的错误。在 GC 会降低应用程序速度的情况下,始终可以选择完全关闭它,或者使用 C 的 malloc 和 free 进行内存管理。

模块

在 D 语言中,模块使用 import 语句导入,并与源文件一一对应,句点作为路径分隔符。模块中的每个符号都有两个名称:符号的名称和以模块名称为前缀的符号名称,这称为完全限定名称或 FQN。例如,writefln 可以称为 writefln 或 std.stdio.writefln。对于首选 FQN 的情况,静态 import 语句导入模块的符号,但避免将它们放入全局命名空间。例如,std.string 和 std.regexp 模块都包含用于查找、替换和拆分的函数。因为我更可能使用纯字符串函数而不是正则表达式,所以我将静态导入 std.regexp,这样每当我想要使用它的任何函数时,我都必须显式地使用,而我可以简单地通过它们的常规名称调用字符串函数。

模块可以具有静态构造函数和析构函数。任何模块中的 static this() 函数都是静态构造函数,在 main() 之前调用;在 main 返回后,调用静态 ~this() 函数。因为模块是符号化导入的,这意味着没有头文件。所有内容都声明一次且仅声明一次,从而消除了预先声明函数或在两个地方声明类并尝试保持两个声明一致的需要。

别名和 typedef

在 D 语言中,别名和类型之间存在区别。typedef 向类型检查系统和函数重载引入了一个全新的类型,这将在稍后讨论。别名是类型的简单替换,或者可选地是符号的简单替换

alias int size_t;
typedef int myint; //can't implicitly convert to int
alias someReallyLongFunctionName func;
数组

在 D 语言中,数组在各个方面都是一等类型。D 包含三种类型的数组:静态数组、动态数组和关联数组。数组声明从右向左读取;char[][] 被解释为字符数组的数组

int[] intArray; // dynamic array of ints
int[2][4] matrix; // a 2x4 matrix

所有数组都具有 length、sort 和 reverse 属性。关联数组是指索引不是顺序整数的数组,可能是文本字符串、结构体或任意整数

import std.stdio;
int main(char[][] args)
{
        int[char[]] petNumber;
        petNumber["Dog"] = 212;
        petNumber["cat"] = 23149;
        int[] sortMe = [2, 9, 341, 23, 74, 112349];
        int[] sorted = sortMe.sort;
        int[] reversed = sorted.reverse;
        return 0;
}

动态数组和静态数组可以使用 .. 运算符进行切片。起始参数是包含的,但结束参数是不包含的。因此,如果您从零切片到数组的长度,您将获得整个数组

int[] numbers = [1, 2, 3, 4, 5, 6, 7];
numbers = numbers[0..2] // 1-3 now

最后,D 使用 ~ 运算符进行字符串连接,因为加法和字符串连接在根本上是两个不同的概念

char[] string1 = "Hello ";
char[] string2 = "world!";
char[] string = string1 ~ string2; // Hello world!

这是一个 D 如何在更底层的例程之上实现大量语法糖的典型例子,以便使程序员更专注于任务本身的实现。字符串 D 将数组更进一步。因为字符串在逻辑上是字符数组,所以 D 没有内置的字符串类型;相反,我们只需声明一个字符数组。

此外,D 有三种类型的字符串:char,一个 UTF-8 代码点;wchar,一个 UTF-16 代码点;以及 dchar,一个 UTF-32 代码点。这些类型,以及用于操作 Unicode 字符的标准库例程,使 D 成为一种适合国际化编程的语言。与 C 相比,D 字符串知道它们的长度,从而消除了更多与查找难以捉摸的空终止符相关的错误和安全问题。

契约

D 实现了使契约式编程变得容易的技术,这可以提高程序的质量保证。将契约作为语言本身的一部分,使得它们更有可能被实际使用,因为程序员不必实现它们或使用外部库来实现它们。

最简单的契约类型是 assert 语句。它检查传递给它的值是否为真,如果不是,则抛出异常。Assert 语句可以传递可选的消息参数以提供更多信息。函数有两种类型的契约,pre 和 post,分别由函数之前的 in 和 out 块表示。in 契约必须在函数的其余部分执行之前满足;否则,将抛出 AssertError。post 契约传递函数的返回值,并检查以确保函数在将值传递给应用程序之前完成了它应该做的事情。当程序在启用 release 选项的情况下编译时,所有 assert 和契约都将被删除以提高速度。

int sqrt(int i)
in {
        assert(i > 0);
   }
out(result) { // the return value is
              // always assigned to result
assert((result * result) ==i);
}
body
{...}

另一种类型的契约是单元测试。其目的是确保特定的函数或一组函数在各种可能的参数下按照规范工作。假设我们有以下相当无用的加法函数

int add(int x, int y) { return x + y; }

单元测试将放在同一个模块中,如果启用了 unittest 选项,它将在模块导入并且执行其中的任何函数后立即运行。在这种情况下,它可能看起来像这样

unittest {
        assert(add(1, 2) == 3);
        assert(add( -1, -2) == -3);
}
条件编译

因为 D 没有预处理器,所以条件编译语句是语言本身的一部分。这消除了预处理器带来的无数麻烦以及它可以使用的无限方式,并且可以加快编译速度。version 语句很像 C 中的 #ifdef。如果定义了版本标识符,则会编译它下面的代码;否则,不会编译。

version(Linux)
import std.c.linux.linux;
else version(Win32)
import std.windows.windows;

debug 语句很像 version 语句,但它不一定需要标识符。调试代码可以放在全局 debug 条件或特定标识符中

debug writefln("Debug: something is happening.");
debug (socket) writefln("Debug: something is
happening concerning sockets.");

static if 语句允许在编译时检查常量

const int CONFIGSOMETHING = 1;

void doSomething()
{
        static if(CONFIGSOMETHING == 1)
        { ... }
}
作用域语句

作用域语句旨在通过允许作用域的清理、成功和失败代码进行逻辑分组,从而使代码的组织更加自然

void doSomething()
{
    scope(exit) writefln("We exited.");
    scope(success) writefln("We exited normally.");
    scope(failure) writefln("We exited due to an exception.");
        ..
}

作用域语句以相反的顺序执行。脚本语法 DMD,参考 D 编译器,支持 -run 选项,该选项运行从标准输入获取的程序。这允许您拥有自编译 D 脚本,只要适当的行位于顶部,就像这样

#!/usr/bin/dmd -run
类型推断

D 允许使用 auto 声明自动推断变量的最佳类型

auto i = 1; // int
auto s = "hi"; // char[4]

这允许编译器在需要该功能时选择最佳类型。

foreach

你们中的一些人可能熟悉 foreach 结构;它本质上是说,“对这个数组的每个元素执行此操作”,而不是“执行设定的次数,这恰好是数组的长度”。foreach 循环极大地简化了数组的迭代,因为程序员甚至不再需要关心计数器变量。编译器会处理这个问题,同时使数组的每个元素都可用

char[] str = "abcdefghijklmnop";
foreach(char c; str)
writefln(c);

您还可以通过在循环中声明元素来获取元素的索引

int [] y = [5, 4, 3, 2, 1];
foreach(int i, int x; y)
writefln("number %d is %d", i, x);

最后,您可以避免担心变量的类型,而是使用类型推断

foreach(i, c; str)

这为可以执行的众多编译器优化开辟了领域——所有这些都是因为编译器在尽可能多地处理事情,同时仍然为程序员提供完成任何给定任务的灵活性。

异常

作为规则,D 使用异常进行错误处理,而不是错误代码。D 使用 try-catch-finally 模型来处理异常,这允许将清理代码方便地插入到 finally 块中。对于 finally 块不足以处理的情况,作用域语句非常方便。

像任何面向对象的语言一样,D 具有创建对象类的能力。与 C++ 不同,一个主要的区别是缺少 virtual 关键字。这由编译器自动处理。D 使用单继承范式,依靠接口和 mixin(将在稍后讨论)来填补空白。类通过引用而不是值传递,因此程序员不必担心像处理指针一样处理它。此外,没有 -> 或 :: 运算符;. 在所有情况下都用于访问结构体和类的成员。所有类都派生自 Object,继承层次结构的根

class MyClass {
        int i;
        char[] str;
        void doSomething() { ... };
}

类可以通过具有多个同名函数来定义属性

class Person {
        private char[] PName;
        char[] name() {return PName;}
        void name(char[] str)
        {
        // do whatever's necessary to update any
        // other places where the name is stored
        PName = name;
        }
}

类可以具有构造函数和析构函数,即 this 和 ~this

class MyClass {
        this() { writefln("Constructor called");}
        this(int i) {
          writefln("Constructor called with %d", i);
        }
        ~this() { writefln("Goodbye");}

类可以访问其基类的构造函数

this(int i) {
        super(1, 32, i); // super is the name of the
                         // base class constructor
}

类可以在其他类或函数内部声明,并且它们可以访问该作用域中的变量。它们还可以重载运算符,例如比较运算符,以使对它们的操作更清晰,就像在 C++ 中一样。

类可以具有不变量,这些不变量是在构造函数末尾、析构函数之前和公共成员之前检查的契约,但在为发布编译时会删除

class Date
{
int day;
int hour;
invariant
    {
        assert(1 <= day && day <= 31);
        assert(0 <= hour && hour < 24);
    }
}

要检查两个类引用是否指向同一个类,请使用 is 运算符

MyClass c1 = new MyClass;
MyClass c2;
if(c1 is c2)
        writefln("These point to the same thing.");
接口

接口是任何从它派生的类都必须实现的一组函数

interface Animal {
        void eat(Food what);
        void walk(int direction);
        void makeSound();
}
函数

在 D 语言中,没有 inline 关键字——编译器决定哪些函数需要内联,因此程序员甚至不必担心它。函数可以被重载——也就是说,两个同名的函数可以接受不同的参数,但编译器足够聪明,知道您在谈论哪一个

void func(int i) // can implicitly take
                 // longs and shorts too
{...}

void func(char[] str)
{...}

void main()
{
        func(23);
        func("hello");
}

函数参数可以是 in、out、inout 或 lazy,其中 in 是默认行为。Out 参数是简单的输出

void func(out int i)
{
        I += 4;
}

void main()
{
        int n = 5;
        writefln(n);
        func(n);
        writefln(n);
}

inout 参数是读/写参数,但不会创建新副本

void func(inout int i)
{
        if(i >= 0)
                ..
        else
                ..
}

Lazy 参数仅在需要时才计算。例如,假设您像这样调用了一个函数

log("Log: error at "~toString(i)~" file not found.");

请注意,每次调用它时,字符串都会被连接并传递给函数。lazy 存储类意味着字符串只有在被调用时才会被组合在一起,从而提高了性能和效率。D 中的嵌套函数允许在其他函数中嵌套函数

void main()
{
        void func()
        {
                ..
        }
}

嵌套函数具有对封闭函数变量的读/写访问权限

void main()
{
int i;
        void func()
        {
                writefln(i + 1);
        }
}
模板

D 具有完全重新设计且高度灵活的模板系统。首先,! 运算符用于模板实例化。这消除了由 <> 实例化引起的众多歧义,并且更容易识别。这是一个简单的复制器模板

template TCopy(t) {
        void copy(T from, out T to)
        {
                to = from;
        }
}

void main()
{
        int from = 7;
        int to;
        TCopy!(int).copy(from, to);
}

模板声明可以被别名化

alias TFoo!(int) temp;

模板可以针对不同的类型进行特化,并且编译器会推断出您指的是哪种类型

template TFoo(T)        { ... } // #1
template TFoo(T : T[])  { ... } // #2
template TFoo(T : char) { ... } // #3
template TFoo(T,U,V)    { ... } // #4
alias TFoo!(int) foo1;          // instantiates #1
alias TFoo!(double[]) foo2;     // instantiates #2
                                // with T being double
alias TFoo!(char) foo3;        // instantiates #3
alias TFoo!(char, int) fooe;   // error, number of
                               // arguments mismatch
alias TFoo!(char, int, int) foo4; // instantiates #4
函数模板

如果模板只有一个成员函数而没有其他内容,则可以像这样声明它

void TFunk(T) (T i)
{
..
}
隐式函数模板实例化

函数模板可以隐式实例化,并且可以推断出参数的类型

TFoo!(int, char[]) (2,"foo");
TFoo(2, "foo");
类模板

在那些需要声明模板并且其唯一成员是类的情况下,请使用以下简化的语法

class MyTemplateClass (T)
{
..
}
Mixins

Mixin 就像将模板的代码剪切并粘贴到类中一样;它不会创建自己的作用域

template TFoo(t)
{
        t i;
}

class test
{
        mixin TFoo!(int);
        this()
        {
                i = 5;
        }
}

void main()
{
        Test t = new Test;
        writefln(t.i);
}
结论

D 是一种很有前途的语言,能够满足编程社区中的许多不同需求。数组、SH 语法和类型推断等特性使 D 在这些方面可以与 Ruby 和 Python 等语言相媲美,同时仍然为具有内联汇编程序和其他特性的底层系统程序员敞开大门。D 可以应用于许多编程范式——无论是使用类和接口的面向对象编程、使用模板和 mixin 的泛型编程、使用函数和数组的过程式编程,还是任何其他范式。垃圾回收器使程序员无需手动管理所有内存块,同时仍然使手动内存管理成为首选的情况成为可能。它引入了命令式语言(如 Lisp)的特性,即 lazy 存储类,这大大提高了效率。该语言相对稳定,偶尔会添加新的特性或更改。

简而言之,D 是一种为实际部署做好准备的语言。

资源

D 规范和参考编译器: www.digitalmars.com/d

GDC: dgcc.sf.net

众多开源项目和教程: www.dsource.org

Ameer Armaly 是一位失明的 18 岁高中生。他的兴趣包括弹吉他、编程和科幻小说。

加载 Disqus 评论