情報技術 > プログラミング > D言語


Wikipedia
ウィキペディアD言語の記事があります。

D言語はWalter Bright氏によって開発が始められた言語です。


2010年代にFacebook社がD言語を支援したので有名になりましたが、しかしFacebook開発ではなくWalter Bridge氏の開発する言語であり、2001年からD言語は公開されています。

なお、Walter Bright氏は、低価格/高速コンパイラで名を馳せた Datalight C、世界初のnative-C++コンパイラ Zortech C++、そしてその後継であるSymentec C++、Java開発環境 Cafeなどで知られています。

公式名称は「D language」(D言語)または「D programming language」ですが、英語ではネット検索などの利便性のため "dlang" と表記される場合もあります。

目次編集

※ 現状ではサブページのリンク集です。

D言語の特徴編集

D言語はC++とは違い、C言語とのソースの互換性はありません(C言語のライブラリとは互換性あり)。もし互換してしまうと古い時代遅れのノウハウ(バッドノウハウ)を引きずりかねない、と考えていたためです。

D言語はシステムプログラミング言語を目指しています。システムプログラミング言語とは、オペレーティングシステムそのものの開発もでき、高速で動く言語のことです。(たとえばC言語はシステムプログラミング言語です。LinuxはC言語で書かれています。)

実際にD言語で開発されたOSが市販や公開されているかはともかく、D言語がOS開発言語も目指していることは、利用上、とても重要です。なぜなら、ユーザーにとって手間が掛かるかもしれないが、とりあえず、今までC言語で出来たハードウェア制御は、D言語でもひととおり出来るようになる予定・目標だからです。


D言語の特徴をいくつか上げておきましょう。

  • ネイティブな形式にコンパイルされるため動作が速い - JavaやRubyなどのインタプリタや仮想機械上で動作する言語に比べて非常に高速です。
  • C言語・Javaと似ているため、学習コストが低い
  • 自動メモリ管理 - ガベージコレクタを搭載しているため、メモリの取得・開放に関してプログラマが気にする必要はありません。もちろん手動でメモリを管理する方法も用意されています。
  • 保守性 - assert、unittest、invariant、debug, version による条件コンパイルなど、プログラムの保守性を高めるための機能があります。また、関数に定められる@safeなどのセキュリティレベルも保守性に貢献するかもしれません。
  • 可読性の高いテンプレート - C++と比べてテンプレートの構文がすっきりしていて仕様を把握するのも簡単です。
  • 強力なCTFE(コンパイル時関数実行) - D言語はかなりの範囲の関数の結果等をコンパイル時に実行することができます。
  • 豊富な表現力 - 関数型言語やオブジェクト指向言語といったパラダイムを非常にうまい具合に言語に組み込み、高い水準で両立させています。今後、借用(@live)に関連する機能も充実化される予定です。
  • コンパイルの速さ、コンパイラの開発しやすさ - D言語はそのコンパイラの開発が簡単になるように設計されており、特にパースはC++とは比べ物にならない速さです。

上記のうち、C++ との共通点は、ネイティブコンパイルされることくらいでしょう。文法からもわかりますが、D言語はC言語とのメモリ構造が同じになるように設計されており、C言語のライブラリを使えるようになっています。一方で、C言語の printf のような古い関数は D言語では writef を使ったり、ファイル入出力のためのクラス File があったりなど、D言語のより型安全で抽象的にラップされた機能があります。

C++ と違って cout の後ろの << の方向が >> だったかそれとも << だったかで悩む必要もD言語では無く、直感的にD言語は扱いやすく設計されています。

C++ や他言語との重要な違いはテンプレートです。テンプレート引数を < > で表すと、構文解析の際にそれがテンプレートの引数なのかそれとも比較式なのかを、記号表をいちいちチェックしなければならず、パースの時間が非常に長くなってしまいます。D言語は ! が二項演算子として使われていないことに着目して、これをきれいに解決しました。

Hello World!編集

まずは環境がちゃんと動くか試してみましょう。例として、"Hello World!"という文字列を表示させるプログラムを作ってみることにします。テキストファイルを新しく作り、hello.dという名前にしたらテキストエディタで以下のように編集します。

hello.d
import std.stdio;

void main()
{
    writeln("Hello World!");
}

さぁ、これを動かしてみましょう。

$ rdmd hello.d
Hello World!

動きましたか?

正常に動いた場合、上にも示してある通り、コンソール画面に

Hello World!

と文字列が表示されます。

動かなかったら、環境変数などをもう一度確認してみてください。

ソースコードの解説編集

C言語を知っている読者には馴染みの深い見た目ではあると思うが、初心者向けにコードを一行ずつ解説していくことにしましょう。

import std.stdio;
C言語のコード冒頭によくある#include <stdio.h>に相当する部分ですが、しかしD言語では仕様の細部が違い、D言語にはプリプロセッサはありません。このコードの意味は「stdというフォルダにあるstdioというモジュールをインポートする」という意味です。
さらに意味のわからない単語が出てきましたね。一つずつ解説していきましょう。
std というフォルダや stdio.d というファイルを作った覚えはないと思います。これらはphobosという標準ライブラリと呼ばれるもので、コンパイラのインストール時に一緒にインストールされたものです。おそらく探せば phobos というフォルダが見つかるのではないでしょうか。
stdio.d にどのような内容が書かれているのか気になる方は、直接覗いてみるのも良いでしょうが、ここに定義の一覧が載っていますので、こちらを見るほうが良いでしょう。
「モジュール」という言葉について。D言語では「ソースコードが書かれた一つのファイル」のことを指します。
「インポート」とは、そのモジュール(この場合はstd.stdio)に書かれているシンボルの定義全て(シンボル表)を今のモジュール(この場合はhello.d)から使えるようにする、という意味です。5行目に "writeln" というのがありますね。"writeln" は std.stdio で定義されているのです。
void main()
関数 main を定義するぞ、という宣言です。詳しくは D言語/関数 を参照してください。関数というのは手続きをまとめたもので、値を返したり返さなかったりするもの、という理解で良いでしょう。void は「何も返さない」という意味です。--- 細かいことを言えば、main 関数だけは若干違っており、void main は、int main に内部的に書き換えられ、最後に必ず return 0; をするような仕様になっています。 ---
D言語のプログラムのエントリーポイントは必ず main 関数です。D言語ではC言語と同じように、地の文に直接処理を書き込むことはできません。しかしそうするとプログラムの実行を指定できないため、プログラムが起動されるときは main 関数を呼ぶ、と決まっているのです。
{
D言語では、{ } で囲まれた文のことを「ブロック」、「スコープ」と呼び、そこには文を書き並べることができます。このブロックは関数mainに属し、この関数の処理を表しています。
writeln ( "Hello World!" );
"Hello World!" については説明の必要がないでしょう。ここにある内容が表示されているのです。D言語では文字列を表す方法はたくさんありますが、とりあえずダブルクオーテーション " ... " で囲むことを覚えておけば良いでしょう。
writeln は関数です。std.stdio というモジュールに定義されているのでしたね。不正確になることを恐れずに言えば、これはターミナルに文字を表示するための関数です。
関数を呼び出すには、関数名の後にカッコで引数をくくる必要があります。この一文は、writeln という関数に引数として "Hello World!" という文字列を与えて、関数を呼び出しているのです。
; は、必ず文末につけなければなりません。
}
4行目の { に対応する閉じカッコです。

D言語の基礎編集

この節では、D言語のプログラムがどういう見た目をしているか、その全体像をまとめたものが書かれています。それぞれについてもう少し詳しく書かれたものはもっと下の節にありますので、そちらを参照してください。

ここ以外にも、D言語ツアーを読むと良いかもしれません。D言語ツアーは初心者向けの公式ガイドの和訳です。

概観編集

D言語の構文はC言語やJavaに似ており、テキストに書かれたプログラムを1行ずつ処理していく手続き型言語の側面を持ち合わせています。基本的な流れや、一つ一つの文、コメント、演算子などもC言語を元にしています。詳しくはC言語の該当項目を参照してください。

ここでは、D言語の文法について事細かに説明することはしません。ただしC言語と異なる部分もあるため、その部分については必ず明記しています。

文字列の表示の関数編集

write, writef, writeln, writefln などの関数で、文字列の表示が可能です。

ln がついた関数は、「自動的に末尾に改行を付ける」という意味であり、f がついた関数はC言語と同じように、書式を使って表示するという意味です。例を見てみましょう。

コード例
// write および writeln の例

import std.stdio;

int a = 7;
int b = 4;

void main()
{
  write("あああ"); 
  writeln("1行目。aは", a); // a を表示
  writeln("2行目。bは", b); // b を表示
}
実行結果
あああ1行目。aは7
2行目。bは4

基本的には、write, writeln は任意の型を任意の個数取ることができるので、いろいろなものを表示してみるのも面白いでしょう。

コード例
// writefln
import std.stdio;

int a = 7;

void main()
{
  writefln("変数は%d", a); 
}
実行結果
変数は7

コメント編集

D言語のコメントは3種類あります。

  • // 一行コメント
  • /* 複数行コメント */
  • /+ ネスト /+ コメント +/ +/

コメントとは、プログラムの中に残しておくメモのようなものです。コメントに何を書いてもコンパイラには影響を及ぼしません。このページにあるプログラムにもコメントが書かれていますね。

特別なコメントとしては、行頭に #! という一行コメントを書くことができます。これは他のインタプリタ言語のソースコードを直接実行するためのコメントとして有名ですね。D言語も #!/usr/bin/rdmd と書くことで、まるでインタプリタ言語のように使うことができるのです。

また、コメントをドキュメント化することもできます。/// /** */ と書かれたコメントはドキュメント化の対象となります。詳しくはD言語/ddocをご覧ください。

変数編集

変数とは、値を格納するものです。

コード例
import std.stdio;

void main() {
    auto a = 2460;    // a という変数を初期値2460で初期化
    auto b = 48;      // b という変数を初期値48で初期化
    writeln("a: ", a, " b: ", b);    // a: 2460 b: 48
    auto c = a - b;   // c という変数を初期値 a - b で初期化
    writeln("c: ", c); // c: 2412
    
    b = c - 12;   // 変数 b に c - 12 を代入する
    writeln("b: ", b); // b: 2400
    a = a - b;    // 変数 a に a - b を代入する
    writeln("a: ", a); // a: 60
}

変数に値を「代入する」という操作によって、変数が格納する値が変化します。代入という操作は、等号 "=" を用いて表します。これは数学の等号とは意味が異なるので、注意が必要です。

関数編集

プログラミングをする際に、決まった同じ処理をしたくなる場合があるはずです。その際にコードを再利用することができるよう、関数というものが備わっています。これも数学の意味での関数とは違って、副作用を持つことができるので、「手続き」としばしば呼ばれます。

コード例
import std.stdio;

void main() {
    show_next(twice(8));    // 1 : 17
    int m = 20; // int 型変数を宣言
    show_next(twice(m));    // 2 : 41
    writeln(m);             // 20
    writeln(add(m, 32));    // 52
    writeln(m);             // 20
}

// int 型の値を2つ受け取り、int 型の値を返す関数 'add' を宣言する
int add(int n, int m) {
   auto result = n + m;
   return n+m;
}

// int 型の値を受け取り、int 型の値を返す関数 'twice' を宣言する
auto twice(int n) {
    return 2 * n;
}

int counter = 0;
// int 型の値を受け取り、何も返さない関数 'show_next' を宣言する
void show_next(int n) {
    counter = counter + 1;
    writeln(counter, " : ", n+1);
}

関数の宣言方法は、上にある通りです。型をまだ説明していませんが、D言語では、値には常に「型」がついて回るのです。そのため、int (32bitの符号付き整数型) といったものが必要になってきます。変数宣言のときには単に "auto" と書くだけで良かったのは、コンパイラが初期値から型を推論して自動で型をつけてくれたからなのです。実際、関数 twice の宣言は auto と書くことができます。

宣言した関数を呼び出すには、f(a, b, c) のように書きます。このとき、カッコの中にある a, b, c といったものを「引数」と呼びます。上のコード例で定義した関数は、main が引数なし、twice, show_next が引数一個、add が引数二個、となっていますね。


ところで、writeln(m); // 20 とありますね。なぜ、これが "20" と表示されるか疑問に思いましたか? 関数に値を渡すとき、特に指定しなければそれは値渡しになります。値渡しとは、「変数の指す値」を渡し、変数自体の情報は渡さないということです。したがって、上のコード例では、m の値は、twice, add に渡されても変わらなかったのです。

では逆に、変数の値を書き換えるようにするためには、どうすれば良いのでしょうか。そのような関数の引数への渡し方を参照渡しと言います。D言語で参照渡しをする方法は簡単です。例を見てみましょう。

コード例
import std.stdio;

void main() {
    int m = 2;
    writeln(successor(m));   // 3
    writeln(m);              // 3
}

// int 型の参照渡し
int successor(ref int n) {
    n = n + 1;
    return n;
}

これを実行すると、m の値が変わっていることがわかります。しかし、同じコードでも

successor(successor(m));

はエラーになってしまいます。それは、successor の返値が参照渡しできないからです。このコードを意図したように動かすには、

ref int successor(ref int n)

と、返値の型にも ref をつければ解決します。

UFCS編集

関数呼び出しがいくつも重なると、読みにくくなることがあります。例えば、

writeln(twice(add(add(n, 10), 20)));

というコードは非常に読みにくいです。そこで活躍のするのが統一関数呼び出し構文(UFCS)です。これはおおざっぱに言ってしまえば、a.f(b, c, ...)f(a, b, c, ...) と(コンパイラが内部的に)書き換えることです。オブジェクト指向がわかる方は、メソッドの呼び出しのように書くことができるものだと理解できるでしょう。これを使うと先程のコードは以下のようになります。

n.add(10).add(20).twice().writeln();

これでだいぶスッキリしましたが、今度は twice writeln のあとにつくカッコすらも煩わしく感じてしまいます。これを省略するには、関数定義の際に @property 属性をつければよいです。つまり、

auto twice(int n) @property { ... }

と定義すれば良いです。なお、writeln についても同様に省略可能です。

制御文編集

プログラムを書く際には条件分岐やループといった機能が活躍します。フィボナッチ数列を計算する関数を定義してみましょう。

コード例 (悪い例)
import std.stdio;

long fibonacci(long n) {
    if (n < 0) {
        return 0;
    }
    else if (n == 1) {
        return 1;
    }
    else {
        return fibonacci(n-1) + fibonacci(n-2);
    }
}

void main() {
    writeln(fibonacci(10));
}
実行結果
55

早速出てきました。if-else 文です。

if (条件式) {
    条件式が真だったときに実行される文
}
else {
    条件式が偽だったときに実行される文
}

上のコードでは、引数が 0 以下であるときは 0 を返し、そうではないときは、1 ならば 1 を返し、そうでないときはフィボナッチ数列の漸化式にしたがった値を返しています。なお、else 節は省略できます。そのときは、条件が偽であるとき何もしません。

このフィボナッチ数の計算は一見シンプルで完璧であるように見えますが、問題があります。それは計算量です。fibonacci(10) はすぐに計算できますが、fibonacci(45) はなかなか計算が終わりません。なぜなら、この関数が呼び出しは自身を2度呼び出すので、2の累乗のオーダーの計算量になってしまうからです。
しかし、実際にフィボナッチ数列を手計算するときは線形時間のはずです。一つ前と今のフィボナッチ数がわかれば次のフィボナッチ数がわかるからです。この考えをコードに起こすには、同じ処理を繰り返さなければならないことがわかると思います。

コード例
import std.stdio;

long fibonacci(long n) {
    long previous = 0; // F_0
    long current  = 1; // F_1
    long counter = 1;  // いま current は F_{counter}
    while (counter < n) {
        long new_f = previous + current;    // F_{n+1} = F_n + F_{n-1}
        // 更新
        ++counter;    // counter を 1 増やす
        previous = current; // 次のフィボナッチ数に進める
        current = new_f;    // 次のフィボナッチ数に進める
    }
    return current;
}

void main() {
    writeln(fibonacci(10));
}
実行結果
55

while 文です。

while (条件式) {
    条件式が真だったときに実行される文
}

while 文は、「条件式が真であるなら文を実行し、偽であるならば実行しない」を繰り返す制御文です。

編集

D言語は強い静的型付け言語です。型とはデータの種類を表すものであり、典型的なものとして、int型, float型、string型、配列型、ポインタ型、関数型、といったものがあります。D言語は静的型付け、つまりコンパイル時に変数の型が必ず決まるのです。このように聞くとなんだか難しい気もしますが、D言語には強力な型推論の機能があり、厳格に気にしなければならないわけではないことは、変数宣言のセクションを見ればわかるでしょう。

C言語を知っている人へ : C言語での型のコードがD言語でエラーなくコンパイルされるなら、双方で同じ解釈をされます。

プリミティブな型編集

プリミティブな型、つまりそれ以上分解できない型を以下に挙げます。

整数型
  • byte 符号付き8bit整数
  • ubyte 符号無し8bit整数
  • short 符号付き16bit整数
  • ushort 符号無し16bit整数
  • int 符号付き32bit整数
  • uint 符号無し32bit整数
  • long 符号付き64bit整数
  • ulong 符号無し64bit整数
  • cent 符号付き128bit整数 (現在は使用不可)
  • ucent 符号無し128bit整数 (現在は使用不可)
浮動小数点数型
  • float 32bit
  • double 64bit
  • real x86 CPUでは80bit、それ外は double と同じ
文字型
  • char 符号無し8bit UTF-8 コードユニット
  • wchar 符号無し16bit UTF-16 コードユニット
  • dchar 符号無し32bit UTF-32 コードユニット
その他の型
  • void 「値が無い」ことを示す型。
  • bool 真理値 true false を表す型。
  • size_t 符号付き整数型。最大値まででプログラムのあらゆるメモリにアクセスできるように保証されている。通常は ulong を指す。
  • ptrdiff_t 符号無しの size_t

型を表示するには編集

typeid を使って型を表示することができます。以下の例1を見てください。

例1編集

import std.stdio;

void main()
{
    int   a = 42;
    float b = 32.5f;
    writeln(a, " ", b);     // 42 32.5
    long  c = 16;
    ulong d = 26;
    bool  e = a == c+d;
    writeln(e);             // true
    writeln(typeid(a-c-d)); // ulong
}

異なる型の間の演算、型キャスト等についてはD言語/型を参照してください。

配列型・ポインタ型編集

T に対して T[] という型が存在します。これは「Tの動的配列型」であり、参照型です。配列型の変数は、内部的にはヒープ領域にあるデータの先頭を指すポインタと配列の長さを持っています。これはC言語の配列のバッファオーバランを実行時エラーとするという改善です。

配列の長さは、length というプロパティで取得することができ、length プロパティにsize_tの値代入するとその長さの配列をメモリに確保してくれます。なお、確保されたメモリの各要素は、その型の初期値で初期化されます。

C言語経験者向けの注意 : Cスタイルの宣言 int array[]; は許可されていません。

例2編集

import std.stdio;

void main()
{
    int[] a = [2, 3, 5, 7, 11, 13];
    writeln(a, " ", a.length);    // [2, 3, 5, 7, 11, 13] 6
    
    auto b = a[0 .. $-2];    // $ は [ ] 内で使われると、その配列の長さ length を指す
    b[0] = 20;
    writeln(b, " ", b.length);    // [20, 3, 5, 7] 4
    writeln(a);    // [20, 3, 5, 7, 11, 13] (a, b は同じメモリ領域を指すため、b の要素を書き換えると a も変化する
    
    auto c = a ~ [17, 19];    // 連結 ~ によってヒープ領域に新しい動的配列が確保され、c はそれを指すようになる
    wirteln(c);    // [20, 3, 5, 7, 11, 13, 17, 19]
    c[1] = 30;
    writeln(c);    // [20, 30, 5, 7, 11, 13, 17, 19]
    writeln(a);    // [20, 3, 5, 7, 11, 13] (c は a のコピーを連結したので、a と同じ場所は指さない
}

例3編集

import std.stdio;

void main()
{
    int[] a;
    a.length = 10;
    writeln(a);    // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    
    auto b = new int[10];    // これでも長さ10の配列を確保できる
}

連想配列型編集

function / delegate 型編集

制御文編集

if 文編集

while 文編集

do-while 文編集

for 文編集

goto 文編集

switch 文編集

foreach / foreach_reverse 文編集

その他の文編集

ブロック文編集

with 文編集

asm 文編集

mixin 文編集

assert 文編集

編集

ユーザー定義型編集

enum編集

struct編集

class編集

D言語/オブジェクト指向を参照。

union編集

例外処理編集

契約編集

単元『D言語/関数』で説明済みなので参照のこと。

参考文献編集

  • プログラミング言語D Andrei Alexandrescu著 長尾高弘訳 株式会社翔泳社出版 ISBN 978-4-7981-3110-8
  • Programming in D Ali Çehreli著 ISBNs 978-0-692-59943-3 978-0-692-52957-7 978-1-515-07460-1 978-1-515-07460-1

外部リンク編集

このページ「D言語」は、まだ書きかけです。加筆・訂正など、協力いただける皆様の編集を心からお待ちしております。また、ご意見などがありましたら、お気軽にトークページへどうぞ。