D言語

2020年7月12日 (日) 03:03時点におけるすじにくシチュー (トーク | 投稿記録)による版 (契約: 単元『D言語/関数』で説明済みなので参照のこと。)

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

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


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 をつければ解決します。

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

クラスは複数の変数を一まとめにし、それらを扱う関数も内包することで、簡潔で他のプログラムに依存しない独立した処理を可能とします。また、クラスは雛形(ひながた)であり、同じ性質を持ったデータ(オブジェクト)を多数作る事ができます。

実体化

クラスは雛形に過ぎないため、実際に変数として使うためには定義、宣言した後に実体化(インスタンス化)という作業が必要になります。Cというクラスのオブジェクトc1は

C c1;    // 宣言
c1 = new C();

で実体化できます。これ以降c1をC型の変数として扱う事ができます。newが実体化する命令で、その対象はCというクラス、という事です。空白の()は後述するコンストラクタに何も指定しない事を意味します。

メンバ変数

classの中に定義された変数の事を特にメンバ変数と言います。 オブジェクトの持つデータそのもので、これを扱いやすくするための仕組みが以下になります。

メンバ関数

classの中に定義された関数を特にメンバ関数と言います。 メンバ変数に対する決まりきった処理を他のオブジェクトに依存すると、関わるオブジェクト数が増えるため複雑で読みにくく直しにくいコードになります。メンバ変数の事はメンバ関数に任せましょう。

コンストラクタ

this()という名前を持つ特殊なメンバ関数です。クラスをオブジェクトとして実体化する際に、this関数の中で定義された初期化処理を実行します。引数なしの場合はメンバ変数のデフォルト値を与えるのに使います。初期化するだけなので、返り値はありません。

this()
{
    i = 0;
    str = "Default string";
}

引数ありの場合も定義でき、何個でも定義できます(オーバーライド)

this(int x)
{
    i = x;
    str = "int override";
}
this(string strin)
{
    i = 0;
    str = strin.idup;
}

ここで定義した関数は、new実行時にC(引数)と表記する事で、引数に応じてthis(引数)関数を実行します。様々な初期状態を持った同じクラスのオブジェクトを作るのに便利です。

演算子オーバーロード

四則演算(+-*/)など多くの演算記号を、クラスに対してまるごと作用させる演算を定義できます。

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