コンテンツにスキップ

D言語

出典: フリー教科書『ウィキブックス(Wikibooks)』

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

D言語は、2001年にウォルター・ブライト(Walter Bright)によって設計されたプログラミング言語です。D言語は、C言語やC++言語から多くのアイデアを借用しており、それらを現代的な言語機能で拡張しています。D言語は、コンパイル時に静的に型付けされる静的型付け言語であり、メモリ管理は手動でも自動でも行うことが可能です。 D言語の最も重要な特徴の1つは、高速であることです。D言語は、C言語と同じように、ネイティブコードを生成することができます。D言語のコンパイラは、C言語と同じくらい高速であり、実行速度が非常に速いプログラムを生成することができます。 また、D言語は、C++言語よりもシンプルな文法を持っています。これにより、開発者はより素早く開発することができます。D言語には、オブジェクト指向プログラミング、ジェネリックプログラミング、関数型プログラミングなどの多くのプログラミングパラダイムが含まれています。 D言語は、Linux、Windows、macOS、FreeBSDなどの多くのプラットフォームでサポートされています。D言語は、Webアプリケーション、ゲーム、システムプログラミング、データベースアプリケーションなど、さまざまな用途に使用されています。

この教科書では、D言語の基礎から始めて、より高度なトピックに進んでいきます。D言語を学ぶことで、より高速かつ効率的なプログラミングができるようになります。

目次

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

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というモジュールをインポートする」という意味です[1]
さらに意味のわからない単語が出てきましたね。一つずつ解説していきましょう。
std というディレクトリーや stdio.d というファイルを作った覚えはないと思います。これらはphobos[2]という標準ライブラリと呼ばれるもので、コンパイラーのインストール時に一緒にインストールされたものです。おそらく探せば 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言語の構文はC言語やJavaに似ており、テキストに書かれたプログラムを1行ずつ処理していく手続き型言語の側面を持ち合わせています。基本的な流れや、一つ一つの文、コメント、演算子などもC言語を元にしています。詳しくはC言語の該当項目を参照してください。

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

文字列の表示の関数

[編集]

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

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

write および writeln の例
import std.stdio;

int a = 7;
int b = 4;

void main() {
  write("ABC"); 
  writeln(" a = ", a); // a を表示
  writeln(" b = ", b); // b を表示
}
実行結果
ABC a = 7 
b = 4

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

writeflnの例
import std.stdio;

int a = 7;

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

コメント

[編集]

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

コメントの例
// 一行コメント

/* 
 複数行コメント
 */

/+ ネスト
  /+ コメント +/ 
 +/

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

shebang

[編集]

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

埋込みドキュメント

[編集]

コメントをドキュメント化することもできます。

埋込みドキュメントの例
/// 一行コ埋込みドキュメント

/**
 複数行埋込みドキュメント
 */

/++ ネスト
  /+ 埋込みドキュメント +/ 
 +/

変数

[編集]

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

コード例
import std.stdio;

void main() {
    auto a = 2460;                // 変数 a を宣言し初期値2460で初期化
    auto b = 48;                  // 変数 b を宣言し初期値48で初期化
    writeln("a: ", a, " b: ", b); // 変数 a と 変数 b の値を参照
    auto c = a - b;               // 変数 c を宣言し初期値 a - b で初期化
    writeln("c: ", c);            // 変数 c を参照
    
    b = c - 12;                   // 変数 b に c - 12 を代入する
    writeln("b: ", b);            // 変数 b を参照
    a = a - b;                    // 変数 a に a - b を代入する
    writeln("a: ", a);            // 変数 a を参照
}
実行結果
a: 2460 b: 48
c: 2412
b: 2400
a: 60
変数を使うには宣言が必要で、宣言と同時に初期値を与えることができます。
auto で型宣言した場合は、初期値が必要で初期値の型が変数の型になります(型推論)
変数に値を「代入する」という操作によって、変数が格納する値が変化します。
代入という操作は、等号記号 "=" を用いて表します。これは数学の等号とは意味が異なるので注意が必要です。
変数の値は、変数名を使って参照します。

関数

[編集]

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

コード例
import std.stdio;

/// 関数呼び出しと型推論
void main()
{
    show_next(twice(8));
    int m = 20; // int 型の変数 m を宣言し 20 で初期化
    show_next(twice(m));
    writeln(m);
    writeln(add(m, 32));
    writeln(m);
}

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

/// int 型の値を受け取る、関数 'twice' を宣言。戻値の型は型推論により int となる
auto twice(int n)
{
    return 2 * n;
}

/// グローバル変数 counter は、関数 'show_next' によって参照されます
int counter = 0;

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

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

宣言した関数を呼び出すには、f(a, b, c) のように書きます。このとき、カッコの中にある a, b, c といったものを「実引数(argument)」と呼びます。上のコード例で定義した関数は、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;
}
実行結果
3
3

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

successor(successor(m));

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

ref int successor(ref int n)

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

UFCS

[編集]

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

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

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

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

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

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

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

UFCS版successor
import std.stdio;

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

ref int successor(ref int n) @property
{
    n = n + 1;
    return n;
}
実行結果
3
4
7 
7

制御文

[編集]

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

コード例 (再帰を使った例)
import std.stdio;

auto fibonacci(ulong 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言語」は、まだ書きかけです。加筆・訂正など、協力いただける皆様の編集を心からお待ちしております。また、ご意見などがありましたら、お気軽にトークページへどうぞ。
  1. ^ std.stdio は、core.stdc.stdioを拡張した標準的なI/O関数です。core.stdc.stdioはstd.stdioをインポートする際にパブリッシュされます。
  2. ^ Phobos Runtime Library
  3. ^ OSが、shebangに対応している必要があります。