モジュールとライブラリ

Dプログラム(およびライブラリ)の構成要素はモジュールだ。

Dのモジュールはシンプルな概念に基づいている:すべてのソースファイルがモジュールだ。したがって、これまでプログラムを書いてきた単一のファイルはすべて個別のモジュールだった。

デフォルトでは、モジュールの名前はファイル名から.d拡張子を削除したものと同一だ。明示的に指定する場合、モジュールの名前はmoduleキーワードで定義され、ソースファイルの最初の非コメント行に記述する必要がある。

例えば、ソースファイルの名前が"cat.d"である場合、モジュールの名前はmoduleキーワードで指定する。

module cat;

class Cat {
    // ...
}
D

module行は、モジュールがどのパッケージにも属していない場合(以下を参照)は省略可能だ。指定しない場合、.d拡張子を除いたファイル名と同じになる。

static this() static ~this()

static this() static ~this()は、モジュールスコープでは、structclassの対応するキーワードと同様に機能する:

module cat;

static this() {
    // ... モジュールの初期操作 ...
}

static ~this() {
    // ... モジュールの最終操作 ...
}
D

これらのスコープにあるコードは、スレッドごとに1回ずつ実行される。(ほとんどのプログラムは、main()関数の実行を開始する単一のスレッドで構成されていることに注意。)プログラム全体で1回だけ実行すべきコード (sharedおよびimmutable変数の初期化など)は、shared static this()およびshared static ~this()ブロックで定義する必要がある。これについては、データ共有の並行処理の章で説明する。

ファイル名とモジュール名

Dは、ソースコードおよびモジュール名においてUnicodeをサポートしている。ただし、ファイルシステムのUnicodeサポートはさまざまだ。例えば、ほとんどのLinuxファイルシステムはUnicodeをサポートしているが、Windowsファイルシステムではファイル名の大文字と小文字が区別されない場合がある。さらに、ほとんどのファイルシステムでは、ファイル名およびディレクトリ名に使用できる文字が制限されている。

移植性を考慮して、ファイル名には小文字のASCII文字のみを使用することをお勧めする。例えば、Résuméというクラスには、"resume.d"というファイル名が適している。

したがって、モジュールの名前もASCII文字で構成される:

module resume;  // ASCII文字で構成されるモジュール名

class Résumé {  // Unicode文字で構成されるプログラムコード
    // ...
}
D
パッケージ

関連するモジュールの組み合わせをパッケージと呼ぶ。Dパッケージもシンプルな概念だ:同じディレクトリ内に存在するソースファイルは、同じパッケージに属するとみなされる。ディレクトリ名はパッケージ名となり、モジュール名の最初の部分としても指定する必要がある。

例えば、"cat.d"と"dog.d"が"animal"ディレクトリ内にある場合、モジュール名とともにディレクトリ名を指定すると、それらは同じパッケージの一部になる。

module animal.cat;

class Cat {
    // ...
}
D

同様に、dogモジュールの場合:

module animal.dog;

class Dog {
    // ...
}
D

パッケージの一部であるモジュールの場合、module行は省略できず、パッケージ名を含むモジュール名全体を指定する必要がある。

パッケージ名はディレクトリ名に対応しているため、1 ディレクトリレベルより深いモジュールは、その階層構造をパッケージ名に反映させる必要がある。例えば、"animal"ディレクトリに"vertebrate"ディレクトリが含まれている場合、そのディレクトリ内のモジュールの名前には"vertebrate"も含まれる。

module animal.vertebrate.cat;
D

ディレクトリ階層は、プログラムの必要性に応じて任意に複雑にすることができる。比較的短いプログラムでは、通常、すべてのソースファイルが1つのディレクトリに格納される。

モジュールのインポート

これまでほとんどのプログラムで使用してきたimportキーワードは、現在のモジュールにモジュールを導入するために使用される:

import std.stdio;
D

モジュール名には、パッケージ名も含めることができる。例えば、上記のstd.は、stdiostdパッケージの一部であるモジュールであることを示す。

animal.catモジュールとanimal.dogモジュールは、同様にインポートされる。次のコードが"deneme.d"というファイル内にあると仮定しよう。

module deneme;        // このモジュールの名前

import animal.cat;    // 使用するモジュール
import animal.dog;    // 使用する別のモジュール

void main() {
    auto cat = new Cat();
    auto dog = new Dog();
}
D

注釈:下記で説明するように、プログラムを正しく構築するには、これらのモジュールファイルもリンカに提供する必要がある。

複数のモジュールを同時にインポートすることもできる。

import animal.cat, animal.dog;
D
選択的なインポート

モジュールをその名前すべてでインポートする代わりに、そのモジュールから特定の名前だけをインポートすることもできる。

import std.stdio : writeln;

// ...

    writefln("Hello %s.", name);    // ← コンパイルエラー
D

上記のコードは、writelnだけがインポートされており、writeflnはインポートされていないため、コンパイルできない。

選択的なインポートは、名前衝突の可能性を減らすことができるため、モジュール全体をインポートするよりも望ましいと考えられている。以下の例でわかるように、同じ名前が複数のインポートされたモジュールに存在する場合、名前衝突が発生する可能性がある。

選択的インポートでは、コンパイラは実際にインポートされるモジュールの一部だけをコンパイルすればよいため、コンパイル時間も短縮される。一方、選択的インポートでは、インポートする名前をすべてimport行で個別に指定する必要があるため、作業量が増える。

この本では、主に簡潔にするため、選択的インポートは使用していない。

ローカルインポート

これまで、必要なモジュールは常にプログラムの先頭でインポートしてきた。

import std.stdio;     // ← 上部
import std.string;    // ← 上部

// ... モジュールの残りの部分 ...
D

しかし、モジュールはソースコードの他の行でもインポートすることができる。例えば、次のプログラムの2つの関数は、それぞれ必要なモジュールを自分のスコープにインポートしている。

string makeGreeting(string name) {
    import std.string;

    string greeting = format("Hello %s", name);
    return greeting;
}

void interactWithUser() {
    import std.stdio;

    write("Please enter your name: ");
    string name = readln();
    writeln(makeGreeting(name));
}

void main() {
    interactWithUser();
}

ローカルインポートは、すべてのモジュールを無条件に先頭にインポートするよりも、実際に使用されるスコープにあるモジュールだけをインポートできるため、グローバルインポートよりも推奨される。コンパイラが、プログラムが関数を呼び出さないことを認識している場合、その関数内のインポート指令を無視することができる。

さらに、ローカルにインポートされたモジュールは、そのローカルスコープ内でのみアクセス可能なので、名前の衝突のリスクがさらに低くなる。

後でミックスインの章で、テンプレートミックスインにはローカルインポートが実際に必要であることを説明する。

この本全体の例では、ローカルインポートを利用していない。これは、主に、ローカルインポートがこの本の執筆開始後にDに追加されたためだ。

モジュールの位置

コンパイラは、パッケージ名とモジュール名を直接ディレクトリ名とファイル名に変換して、モジュールファイルを探す。

例えば、前の2つのモジュールは、それぞれ"animal/cat.d"および"animal/dog.d"という場所にある (ファイルシステムによっては"animal\cat.d"および"animal\dog.d"になる)。メインのソースファイルも考慮すると、上記のプログラムは3つのファイルで構成されている。

長いモジュール名と短いモジュール名

プログラム内で使用される名前は、モジュール名とパッケージ名を含めて表記されることがある:

auto cat0 = Cat();
auto cat1 = animal.cat.Cat();   // 上記と同じ
D

通常、長い名前は必要ないが、名前の衝突が発生する場合がある。例えば、複数のモジュールに存在する名前を参照する場合、コンパイラはどちらの名前を指すのか判断できない。

次のプログラムでは、2つの別々のモジュールで定義されている2つのJaguar構造体を区別するために、長い名前をすべて表記している。animalcar:

import animal.jaguar;
import car.jaguar;

// ...

    auto conflicted =  Jaguar();            // ← コンパイルエラー

    auto myAnimal = animal.jaguar.Jaguar(); // ← コンパイルできる
    auto myCar    =    car.jaguar.Jaguar(); // ← コンパイルできる
D
インポートの名前変更

便宜上、または名前の競合を解決するために、インポートされたモジュールの名前を変更することができる。

import carnivore = animal.jaguar;
import vehicle = car.jaguar;

// ...

    auto myAnimal = carnivore.Jaguar();       // ← コンパイルできる
    auto myCar    = vehicle.Jaguar();         // ← コンパイルできる
D

インポート全体を名前変更する代わりに、インポートされた個々のシンボルを名前変更することもできる。

例えば、次のコードを-wコンパイラスイッチでコンパイルすると、コンパイラは、.sortプロパティの代わりにsort()関数を使用すべきだと警告する。

import std.stdio;
import std.algorithm;

// ...

    auto arr = [ 2, 10, 1, 5 ];
    arr.sort;    // ← コンパイルの警告
    writeln(arr);
D
警告: .sortプロパティの代わりにstd.algorithm.sortを使用してください
Undefined

注釈:上記のarr.sort式は、sort(arr)と同等だが、後で説明するUFCS構文で記述されている。

この場合の解決策の1つは、std.algorithm.sortを名前変更してインポートすることだ。以下の新しい名前algSortは、 sort()関数を意味し、コンパイラの警告は表示されなくなる。

import std.stdio;
import std.algorithm : algSort = sort;

void main() {
    auto arr = [ 2, 10, 1, 5 ];
    arr.algSort;
    writeln(arr);
}
パッケージをモジュールとしてインポートする

パッケージの複数のモジュールを一緒にインポートする必要がある場合がある。例えば、animalパッケージの1つのモジュールをインポートするたびに、他のすべてのモジュールもインポートする必要がある場合がある。animal.catanimal.doganimal.horseなどだ。

このような場合、パッケージをモジュールとしてインポートすることで、パッケージのモジュールの一部またはすべてをインポートすることができる。

import animal;    // ← パッケージ全体がモジュールとしてインポートされる
D

これは、パッケージディレクトリにある特別な設定ファイルによって実現される。このファイルは、必ずpackage.dという名前で保存。この特別なファイルには、パッケージのmoduleディレクティブが含まれており、パッケージのモジュールを公にインポートする。

// ファイルanimal/package.dの内容:
module animal;

public import animal.cat;
public import animal.dog;
public import animal.horse;
// ... 他のモジュールも同様 ...
D

モジュールを公開してインポートすると、そのモジュールはインポートしたモジュールのユーザーも使用できるようになる。その結果、ユーザーはanimalモジュール (実際にはパッケージ)だけをインポートしても、animal.catおよびその他のすべてのモジュールにアクセスできるようになる。

機能の廃止

モジュールは時間とともに進化し、新しいバージョン番号でリリースされる。特定のバージョン以降、モジュールの作者は一部の機能を非推奨にする場合がある。機能を非推奨にするとは、新規に作成されるプログラムがその機能に依存しないようにすることである。非推奨の機能を使用することは推奨されない。非推奨の機能は、将来的にモジュールから削除される可能性がある。

機能が廃止される理由はたくさんある。例えば、モジュールの新しいバージョンにより、より優れた代替機能が追加された、機能が別のモジュールに移動された、モジュール内の他の部分との整合性を保つために機能名が変更された、などである。

機能の廃止は、deprecated属性で定義することで正式になる。必要に応じて、カスタムメッセージも指定できる。例えば、次の廃止メッセージは、関数の名前が変更されたことをユーザーに伝える。

deprecated("Please use doSomething() instead.")
void do_something() {
    // ...
}
D

モジュールのユーザーは、次のコンパイラスイッチのいずれかを指定することで、廃止された機能が使用された際にコンパイラがどのように反応するかを決定できる:

例えば、プログラムで廃止された機能を呼び出し、-deでコンパイルすると、コンパイルは失敗する。

do_something();
D
dmd deneme.d -de
deneme.d: 非推奨: 関数deneme.do_somethingは
廃止されました - 代わりにdoSomething()を使用してください。
Bash

廃止予定の機能の名前は、通常、新しい名前のaliasとして定義される:

deprecated("Please use doSomething() instead.")
alias do_something = doSomething;

void doSomething() {
    // ...
}
D

aliasキーワードについては、後の章で説明する。

プログラムにモジュール定義を追加する

importキーワードだけでは、モジュールをプログラムの一部として機能させることはできない。これは単に、現在のモジュール内でモジュールの機能を有効にするだけだ。これだけの機能は、コードをコンパイルするだけで十分だ。

メインソースファイル"deneme.d"のみでは、以前のプログラムをビルドすることはできない。

dmd deneme.d -w -de
deneme.o: 関数`_Dmain'で:
deneme.d: `_D6animal3cat3Cat7__ClassZ'を参照する未定義の参照
deneme.d: `_D6animal3dog3Dog7__ClassZ'を参照する未定義の参照
collect2: ld は1の終了ステータスを返した
Bash

これらのエラーメッセージはリンカーによって生成される。ユーザーフレンドリーなメッセージではないが、プログラムに必要な定義が欠落していることを示している。

プログラムの実際のビルドはリンカーの責任で、コンパイラが裏で自動的に呼び出す。コンパイラは、先ほどコンパイルしたモジュールをリンカーに渡し、リンカーはそれらのモジュール(とライブラリ)を結合して実行可能プログラムを生成する。

そのため、プログラムを構成するすべてのモジュールをリンカーに提供する必要がある。上記のプログラムをビルドするには、コンパイルラインに"animal/cat.d"と"animal/dog.d"も指定する必要がある:

dmd deneme.d animal/cat.d animal/dog.d -w -de
Bash

コマンドラインで毎回モジュールを個別に指定する代わりに、それらをライブラリとして結合することができる。

ライブラリ

コンパイルされたモジュールの集まりは、ライブラリと呼ばれる。ライブラリはそれ自体ではプログラムではなく、main()関数を持っていない。ライブラリには、関数、構造体、クラス、およびモジュールのその他の機能のコンパイル済み定義が含まれており、これらは後でリンカによってリンクされ、プログラムが生成される。

dmdの-libコマンドラインオプションは、ライブラリを作成するために使用される。次のコマンドは、"cat.d"と"dog.d"モジュールを含むライブラリを作成する。ライブラリの名前は、-ofスイッチで指定される:

dmd animal/cat.d animal/dog.d -lib -ofanimal -w -de
Bash

ライブラリファイルの実際の名前は、プラットフォームによって異なる。例えば、Linuxシステムでは、ライブラリファイルの拡張子は.aだ。animal.a

このライブラリが構築されると、個別に"animal/cat.d"と"animal/dog.d"モジュールを指定する必要はなくなる。ライブラリファイルだけで十分だ:

dmd deneme.d animal.a -w -de
Bash

上記のコマンドは、以下のコマンドを置き換える:

dmd deneme.d animal/cat.d animal/dog.d -w -de
Bash

例外として、Dの標準ライブラリPhobosはコマンドラインで指定する必要はない。このライブラリは、バックグラウンドで自動的にインクルードされる。それ以外の場合は、次のように指定する。

dmd deneme.d animal.a /usr/lib64/libphobos2.a -w -de
Bash

注釈:Phobosライブラリの名前と場所は、システムによって異なる場合がある。

他の言語のライブラリの使用

Dは、CやC++などの他のコンパイル言語で記述されたライブラリを使用することができる。ただし、言語によってリンクが異なるため、そのようなライブラリはDコードからはDバインディングを介してのみ利用可能だ。

リンクは、ライブラリ内のエンティティのアクセス可能性を決定するルールセットであり、それらのエンティティの名前(シンボル)がコンパイル済みコードでどのように表されるかを決定する。コンパイル済みコード内の名前は、プログラマーがソースコードに書く名前とは異なる:コンパイル済み名前は、特定の言語またはコンパイラのルールに従って名前がマングリングされる

例えば、Cのリンクでは、C関数名fooは、コンパイルされたコードでは、先頭にアンダースコアが付いて_fooとマングルされる。C++やDなどの言語では、異なるモジュール、構造体、クラスなどで異なるエンティティに同じ名前を使用したり、関数をオーバーロードしたりすることができるため、名前のマングルはより複雑になる。ソースコードでfooという名前のD関数は、プログラム内に存在する他のすべてのfooという名前と区別できるように、マングリングされる必要がある。通常、正確なマングリング名はプログラマーにとって重要ではないが、core.demangleモジュールを使用して、シンボルをマングリングおよびデマングリングすることができる。

module pind.samples.ja.modules.deneme;

import std.stdio;
import core.demangle;

void foo() {
}

void main() {
    writeln(mangle!(typeof(foo))("deneme.foo"));
}

注釈: mangle()は関数テンプレートであり、この時点ではその構文は不明だ。テンプレートについては、テンプレートに関する章で後で説明する。

上記のfooと同じ型で、deneme.fooという名前を持つ関数は、コンパイルされたコードでは次のようにマングリングされる。

_D6deneme3fooFZv

名前マングリングは、リンカーのエラーメッセージにユーザーフレンドリーな名前を含めることができない理由である。例えば、上記のエラーメッセージのシンボルは、animal.cat.Catではなく_D6animal3cat3Cat7__ClassZであった。

extern()属性は、エンティティのリンクを指定する。extern()で使用できる有効なリンク型は、CC++DObjective-CPascalSystem、およびWindowsである。例えば、DコードでCライブラリで定義されている関数を呼び出す必要がある場合、その関数は C リンクとして宣言する必要がある。

// 'foo'がCリンクである(つまり、Cライブラリで定義されている可能性がある)ことを宣言する
//
extern(C) void foo();

void main() {
    foo();  // この呼び出しは'_foo'への呼び出しとしてコンパイルされる
}

C++ リンクの場合、名前が定義されている名前空間は、extern()属性の2番目の引数として指定される。例えば、次の宣言では、bar()はC++ライブラリで定義されている関数a::b::c::bar()の宣言である(Dコードではコロンではなくドットを使用していることに注意)。

// 'bar'が名前空間a::b::c 内で定義されており、
// C++リンクを持つことを宣言する:
extern(C++, a.b.c) void bar();

void main() {
    bar();          // a::b::c::bar()の呼び出し
    a.b.c.bar();    // aboveと同じ
}

外部ライブラリの機能のD宣言を含むファイルは、そのライブラリのDバインディングと呼ばれる。幸いなことに、多くの場合、プログラマーはこれらを最初から作成する必要はない。なぜなら、多くの人気のある非DライブラリのDバインディングがDeimosプロジェクトを通じて利用可能だからだ。

リンク型を使用しない場合、extern属性は別の意味を持つ。これは、変数のストレージは外部ライブラリの責任であり、Dコンパイラはモジュール内にそのためのスペースを予約してはならないことを指定する。externextern()は意味が異なるため、一緒に使用することができる。

// 'g_variable'のストレージがCライブラリで
// 既に定義されていることを宣言する:
extern(C) extern int g_variable;
D

上記でextern属性が指定されていない場合、Cリンクを持つg_variableは、このDモジュールの変数になる。