概要
D って何?
Dは、汎用のシステム/アプリケーション プログラミング言語です。 高級言語ですが、高速なコードを書く能力や、 OS の API やハードウェアへの直接アクセスの機能も維持しています。Dは、 中規模から100万行単位の大規模ソフトウェアをチームで開発するのに適しています。 Dは学習が簡単で、 プログラマを補助する沢山の機能を提供し、 コンパイラの強力な最適化技術と相性の良い言語です。
Dはスクリプト言語でも、 インタプリタ言語でもありません。 VM を持たず、 宗教やおおげさな哲学とも縁がありません。 信頼性・保守性の高い・読みやすいコードを書いて仕事をサクサク進める必要のある 現実的なプログラマのための、 現実的な言語です。
D は、様々な言語のコンパイラを実装し、 その言語で大きなプロジェクトを構築してきた数十年の経験の集大成です。 D はそれらの言語(特に、C++)からインスピレーションを受け、 経験によって、 現実の実用性の面から調整して取り込んでいます。
なぜ、 D ?
なぜ、まったく、誰がまた新しいプログラミング言語を必要とすると言うのでしょうか?
ソフトウェア産業は、C言語が開発されて以来、 長い道を歩んできました。 沢山の新しい概念が C++言語 に盛り込まれましたが、C との互換性、 特にCの設計上の欠点もほとんど全て、そのまま保たれています。 それらの欠点を修正しようという試みは多くありましたが、 しかし、互換性の問題が足かせになっていました。 一方で、CもC++も、新機能の増大を経験しています。 これら新機能は、 古いコードを書き直す必要がないように、 既存の機構に注意深くはめ込まれなければなりませんでした。 その結果は、物凄い複雑さ - C言語の標準規格は約500ページ、 C++ に至っては 750ページ! - です。 C++ は実装するのにコストがかかり難しい言語で、 その結果、異なる実装の間で完全に移植性のある C++ コードを書くのはかなりイライラする作業となってしまいました。
C++では、可変長配列や文字列の結合といった機能は言語の一部ではなく、 標準ライブラリとして実装されています。
C++のパワーと可能性を取り出して再設計し、 単純で、 直交性のある、 実用的な言語へと変換することは可能でしょうか? その言語を、 コンパイラ作成者が正しい実装を簡単に書けるように、 そして積極的に最適化を進めたコードを生成できるように、 まとめあげることは果たしてできるのでしょうか?
現在のコンパイラ技術は進歩の結果、 かつて未熟だったコンパイラ技術を補うために存在した 幾つかの言語仕様を省くことができるようになりました。 (例としては、C言語のキーワード ‘register’ であったり、 もっと微妙な例ではCのマクロプリプロセッサがあります。) 私たちは現在のコンパイラ最適化技術に頼って、 原始的なコンパイラからまともな品質のコードを得るために必要だった言語仕様から 抜け出すことができます。
D言語の設計の主な目標
言語設計の全ては、何らかのトレードオフです。 なにかの原理を常に念頭に置いておくことは、正しい決定をする助けになります。
- コンパイラからコンパイラへ、 マシンからマシンへ、 OSからOSへの移植できるコードを簡単に書けること。 実用的でありうる範囲で可能な限り、未定義動作や処理系依存の動作を取り除くこと。
- よくある間違いを無くす、あるいは少なくとも減らすように構文と意味論を構成すること。 サードパーティの静的コード検査器の必要を減らす、 あるいは完全に無くすこと。
- メモリ安全なプログラミングのサポート。
- マルチパラダイムプログラミングのサポート。つまり少なくとも、 手続き型、構造化、オブジェクト指向、ジェネリックプログラミング、 さらに関数型プログラミングをサポートすること。
- 間違ったやり方より正しいやり方の方が簡単に書けるようにすること。
- CやC++, Javaに慣れたプログラマに対する 短い学習曲線。
- 低レベルなマシン直接操作を自由にできる言語の提供。 腕利きのプログラマならば、必要な時には言語のチェックを迂回できる方法を提供すること。
- コンパイラの実装が十分に簡単な言語にすること。
- 各環境でのCアプリとのバイナリインターフェイス互換性の保持。
- DのコードがCと同じに見えるときには、同じに振る舞うか、 明示的にエラーを出すかのどちらかにすること。
- 文脈自由な文法。 意味解析なしで構文解析ができるようにすること。
- 国際化されたアプリケーションを容易に書けるようサポート。
- 契約プログラミングや単体テストとの相互運用性。
- 軽量の、単独で動作するプログラムをビルド可能。
- ドキュメント作成の手間を削減。
- コンパイラの最適化技術の進展を有効活用できるような十分な意味論の提供。
- 数値解析プログラマの必要に応えること。
- 当然ながら、これら複数のゴールはしばしば衝突します。 その解消は、usability の良い案を優先する、という方針で行われます。
既存言語から引き継いだ特徴
D の見かけは、全体的に C や C++ に似ています。これによって、 Dを学習したりコードを移植したりするのが容易になります。 C/C++ から D への以降は自然で、 新しいことを丸々学んだりする必要がありません。
Dを使うと言うことは、 プログラマが特別な実行時VM (仮想マシン) に縛られる Java VM や Smalltalk VM のような機構とは無関係です。 D VM なんてものは存在せず、 D は普通にリンクできるオブジェクトファイルを生成する、 直接的なコンパイラです。D は C と同様にOSに直結しています。 make のような一般的なツールも、 Dの開発にピッタリ使うことができます。
- 全体的なC/C++の ルック&フィール は維持されています。 同じように数式を書けますし、ほとんどの式や文、 全体的なレイアウトも同じ形です。
- D のプログラムは、 C の 関数とデータ スタイルでも C++ の オブジェクト指向 スタイルでも C++ の テンプレートメタプログラミング スタイルでも あるいはその3つの混合でも記述できます。
- コンパイル/リンク/デバッグ という開発モデルも引き継いでいます。 Dをバイトコードへコンパイルしたり インタープリタで動かしたりすることを妨げる要因もまた、 ありませんが。
- 例外処理。 例外処理に関する経験を積めば積むほど、エラーコードやグローバル変数 errnoを使うCの伝統的な方法と比べて、 例外による方法が優れていることがわかります。
- 実行時型識別。 これはC++に部分的に実装されています。D では、 これをさらにもう一歩進めました。実行時型識別に完全に対応することで、 よりよりガベージコレクションやデバッグのサポート、 より自動化された永続性などが可能になります。
- Cの呼び出し規約 を持つ関数との呼び出し互換性を維持しています。 Dのプログラムから直接OSのAPIへアクセスできます。 プログラマの、 既存のプログラミングAPIやパラダイムに関する知識と経験は、 最小の作業でDへと引き継ぐことができます。
- 演算子オーバーロード。 D のプログラムは、演算子オーバーロードを有効にすることで、 ユーザー定義型によって基本型を拡張できます。
- テンプレート メタプログラミング。 テンプレートは generic programming を実装する方法の一つです。 他には、マクロを使うとか可変データ型(Variant)を使うといった方法があります。 マクロを使うのは駄目です。Variantを使うのは直接的ですが、 非効率ですし型チェックの欠如が問題です。 C++のtemplateが難しいのは、その複雑さが原因でした。 言語の構文とうまくなじまず、 その上に変換やオーバーロードの大量の規則が築かれているなどなど。。。 D では、template を使うずっと簡単な方法を提供します。
- RAII (「リソース獲得は初期化時に」イディオム)。 RAII のテクニックは、 信頼性の高いソフトウェアを書くために欠かすことのできない部分です。
- Down and dirty programming。 D には、 低レベルでダーティなプログラミングをする能力が保たれています。 他の言語でコンパイルした外部モジュールなどに頼る必要はありません。 システム関係の仕事をしていると、時には、 ポインタを使う必要やアセンブラを弄る必要が出てくるものです。 D の目標は、低レベルでダーティなプログラミングを 妨げる ことではなく、 日常的なコーディングの際にその必要を最小にすることです。
取り除かれた特徴
- Cとのソースコード互換性。互換性を保ったCの拡張は、 既になされています(C++ と ObjectiveC)。 この方面でのこれ以上の拡張は、 大量のレガシーなコードが足かせになりますし、 大した改善も見込めないように思います。
- C++とのリンク互換性。C++の実行時オブジェクトモデルは複雑すぎます - これ適切にサポートするとなると、D を完全な C++ コンパイラにするのと本質的に変わりません。
- Cのプリプロセッサ。マクロプリプロセスは、言語を拡張して、 実際にはない(デバッガには見えない)模造の特徴を追加する簡易な方法です。 条件コンパイルや、テキストの #include 、 マクロ、トークン結合など… 実質的にCは明確な区別のない二つの言語の混在となっています。 さらに悪いことに(もしかしたら幸いなことに?)C のプリプロセッサは非常に原始的なマクロ言語です。 今が、一度振り返ってプリプロセッサの用途を見直し、 それらの用途をカバーする設計を言語自体に組み込む時です。
- 多重継承。データテーブルの値に関する複雑な機能です。 効率良くこれを実装するのは非常に大変で、 実装の際にコンパイラに沢山のバグを持たらす傾向にあります。 MI のほとんど全ての価値は、 単一継承にインターフェイスとアグリゲートを合わせれば達成できます。 残った部分だけでは、 MIを実装する手間を正当化するには足りません。
- 名前空間。 独立に開発されたコードどうしで名前が衝突することを避けることを 目的とした技術です。モジュールを使う考え方の方が簡単で、 ずっとうまく動作します。
- タグ名空間。構造体のタグ名を別のシンボルテーブルに置く、というのは C言語の間違った機能です。C++ ではタグ名と通常のシンボル名を一つに合わせると同時に、 古いCコードとの後方互換性をも残そうとしました。 結果は、不必要な混乱を招くものになっています。
- 前方宣言。Cコンパイラは、 カレント状態よりテキスト的に前に処理した部分のみを知っています。 C++はこれを少しだけ拡張して、 クラスのメンバ同士は順番によらず参照可能になりました。Dはこれを その当然の論理的帰結へと押し進め、前方宣言は完全に不要になりました。Cでは、 前方宣言を書くのを嫌って呼び出し関係の順に関数を並べることがよくありましたが、 Dではモジュールレベルで、 自然な順序に関数を書くことができます。
- ファイルのinclude。 コンパイル単位毎に巨大なファイルを何度もparseするのは、 コンパイルを遅くさせる主な原因となっています。ファイルの取り込みは、 シンボル表の import によってなされるべきです。
- 三連文字や二連文字。今は Unicode の時代です。
- 非仮想メンバ関数。C++では、関数がvirtualになるかどうかは クラスの設計者が前もって決定します。 メンバ関数をオーバーライドすることにしたのに、基底クラスの方で改造を忘れる… というのは、よくある(けれども非常に見つけにくい)コーディングミスです。 全てのメンバはvirtualにしておき、 オーバーライドが存在しないことをコンパイラが検知して非 virtual に変える、というアプローチの方が信頼性があります。
- 任意サイズのビットフィールド。ビットフィールドは複雑で、 非効率かつめったに使われない機能です。
- 16bitマシンのサポート。 Dでは、near/far ポインタをはじめとする、良い 16bit コードの生成に必要な機能は考慮されていません。 D言語は、最低32bitのフラットなメモリ空間を前提に設計されています。 Dは64bitアーキテクチャにはうまくフィットします。
- コンパイラパスの相互依存。C++では、ソーステキストをきちんと構文解析するには シンボル表と、様々なプリプロセッサ命令が必要です。このせいで、 C++のソースをあらかじめparseしておくことが不可能になり、また、 コード解析ソフトや構文強調を行うエディタを正確に実装するのが 無茶苦茶大変になっています。
- コンパイラの複雑さ。 実装の複雑さを軽減することで、 複数の 正しい 実装が登場しやすくなります。
- 低機能な浮動小数点数。 今やモダンな浮動小数点演算器の乗ったハードウェアが使われている以上、 マシン間の最小公約数にあわせた低機能なものではなく、 高度な浮動小数点数の機能がプログラマに使えるようになっているべきです。 特に、Dの実装は必ず IEEE 754 算術をサポートすることになっており、 また拡張精度演算のあるハードウェアではそれも必ずサポートされることになっています。
- テンプレートでの、< と > の多重利用。 この記号が選択されたことで、プログラマとC++実装者とC++ソース解析ツールのベンダの間には、 長年の間、バグと嘆きと困惑が積もり積もっています。 これがあるせいで、ほとんど完全なC++コンパイラそのものを作らない限り、 正しくC++のコードを構文解析するのは不可能になっています。 D は !( と ) を使っており、 見た目も悪くありませんし文法も明確になっています。
D を必要とする人は?
- 日常的にlintなどのコード分析ツールを使って、 コンパイルより前にバグを取り除こうとしているプログラマ。
- コンパイラの警告レベルはいつも最大にして、 警告はエラーとして扱うように設定している人。
- Cでよくあるタイプのバグを避けられるようなプログラミングスタイルを採用したい、 と考えているプロジェクト管理者。
- C++は複雑すぎてオブジェクト指向プログラミングを十分に行えない、 と考えている人。
- C++の強力な表現力は好きだけれど、 明示的なメモリ管理や ポインタ周りのバグには 悩まされているというプログラマ。
- built-inのテストや検証機能が必要なプロジェクト。
- 何百万行にもなるアプリケーションを書くチーム。
- 言語は、 直接ポインタを扱うことによる危険性を未然に不要にするだけの 十分な機能を提供すべきだ、と考えているプログラマ。
- 数値計算プログラマ。 D は、数値計算に必要な機能を多く直接サポートしています。 例えば、拡張精度浮動小数点数や 組み込みの複素数と虚数型、 NaN や∞に対する動作の定義など。 (これらは新しいC99標準には追加されましたが、 C++には存在しません。)]
- アプリケーションを基本的にはRubyやPythonのようなスクリプト言語で書いて、 ボトルネック部分だけスピードを稼ぐためにC++で書いているようなプログラマ。 Dには、 RubyやPythonにあるような生産性の高い機能が多く実装されていて、 アプリケーション全体を1つの言語で書くことが可能になっています。
- Dの字句解析器と構文解析器は、お互いに、そして意味解析器とも完全に独立しています。これは、 コンパイラを全て作りあげずとも、Dのソースを扱うツールを簡単に書けることを意味しています。 これはまた、特別なアプリケーションのために、 ソースをトークン分けした状態で伝達できることも意味しています。
D を必要としない人は?
- 現実問題として、 何百万行ものCやC++プログラムをDへ変換する人はいないでしょう。 DはC/C++のコードをそのままではコンパイルできないため、 過去のレガシーなソフトの開発にはDは使えません。 (しかしながら、 D はレガシーな C の API はしっかりサポートしています。 D からは、C言語インターフェイスを提供しているコードであれば全て直接呼び出すことができます。)
- 最初に学ぶプログラミング言語として。 Basic か Java が初心者には向いています。 D は、中級~上級者プログラマが二番目に修得する言語として優れています。
- 言語原理主義者。 D は実用的な言語であって、 どの側面もその観点から作られています。理想を追う観点からではありません。 例えば、D ではポインタの必要性を実質的になくすセマンティクスが組まれています。 しかし、時には規則を破る必要もありますから、 ポインタはまだ残されています。 同様に型システムを上書きする必要のあるときを考え、 キャストもまた残っています。
D の主要な特徴
この節では、様々なカテゴリにわたる、 D の興味深い特徴をリストアップします。
オブジェクト指向プログラミング
クラス
D のオブジェクト指向的特徴は、 クラスによるものです。 継承のモデルは、 単一継承 + インターフェイス となっています。 Objectクラスが継承階層のルートに存在し、 全てのクラスがその機能を実装します。 クラスは参照としてインスタンス化されるため、 例外発生時の複雑なクリーンアップコードが必要でなくなります。
演算子オーバーロード
クラスは、新しい型をサポートするよう型システムを拡張することで、 既存の演算子と共にはたらくように作ることができます。 例としては、bignumber クラスを作って、通常の代数的な構文を使えるよう、 +, -, *, / 演算子をオーバーロードする、などがあります。
関数型プログラミング
関数型プログラミングは、カプセル化や並行プログラミング、メモリ安全性、 プログラム合成などに多くの利点をもたらします。 Dでの関数型のプログラミングスタイルをサポートする機能は、以下の通りです:
- pure 関数
- immutable 型とデータ構造
- 無名関数とクロージャ
生産性
モジュール
ソースファイルは、モジュールと1対1に対応しています。 ファイルから宣言をテキストとして #include する代わりに、 単にモジュール内のシンボルを import します。 複数回同じモジュールをimportする心配は不要で、 ヘッダを #ifndef/#endif で囲うラッパや #pragma once などの必要がなくなりました。
宣言 vs 定義
C++という言語は通常、関数やクラスを2回宣言することを要求します - .h ヘッダファイルに書く宣言と、.c ソースファイルに書く定義と。 これはエラーを生みやすく、退屈な作業です。明らかに、 プログラマが記述する必要があるのは1回だけで、 あとはコンパイラがシンボルのimportに必要な宣言情報を取り出せばよいのです。 そしてそれが、まさしくDのする動作です。
例:
class ABC
{
int func() { return 7; }
static int z = 7;
}
int q;
もはや、メンバ関数やstaticメンバ、externなどの定義を分けて書く必要はありません。 次のようなわずらわしい構文も不要です:
int ABC::func() { return 7; }
int ABC::z = 7;
extern int q;
注:もちろん、C++ でも { return 7; } のような単純な関数は インライン で書けます。が、複雑なものはそうではありません。それに加え、 前方参照がある場合は関数のプロトタイプ宣言が必要になります。 次のコードはC++では動作しません:
class Foo
{
int foo(Bar *c) { return c->bar(); }
};
class Bar
{
public:
int bar() { return 3; }
};
しかし、これと同等のDのコードは動作します:
class Foo
{
int foo(Bar c) { return c.bar; }
}
class Bar
{
int bar() { return 3; }
}
Dの関数がインライン化されるかどうかは、 最適化の設定によって決定されます。
テンプレート
D のテンプレートは、部分特殊化能力の提供によって、 ジェネリックプログラミングを綺麗にサポートします。 テンプレートクラスもテンプレート関数も利用可能で、 可変個引数テンプレートやタプルといった強力な機能を備えています。
連想配列
連想配列は、自然数のindexに制限されず、 任意の型の値を配列の添え字として使えるようにした配列です。 連想配列の本質は、ハッシュ表です。連想配列によって、 高速で効率的な、 バグのないシンボルテーブルが簡単に実装できます。
ドキュメント化
ドキュメント化は、従来、2つのステップに分かれて行われてきました - まずコメントとして機能の説明が書かれ、 それとは別のhtmlやmanページとして改めて書き直されるという形で。 当然の帰結として、時間がたつにつれ、コードは更新されても分離されたドキュメントは 更新されず、次第に説明と実態が乖離していくようになります。 ソースに書き込まれたコメントから 必要なドキュメントを直接生成できるようにすることで、 ドキュメント書きの時間を半分に減らせるだけでなく、 コードとドキュメントの一貫性を保つのもずっと簡単になります。 Ddoc が、Dのドキュメント生成の仕様となっています。 このページもDdocによって生成されています。
C++にもサードパーティ製のツールはいくつか作られましたが、 それらには重大な欠点がありました:
- C++を100%完全に構文解析するのは非常に難しい作業で、実質的に、 完全なC++コンパイラを書くのと同等な作業が必要です。サードパーティ製のツールは大抵、 C++の一部分だけしか正しく解析できず、 書けるコードがそれによって制限されてしまいます。
- 異なるコンパイラは異なったバージョンのC++をサポートし、 異なる拡張をC++に施しています。 サードパーティ製のツールでは、これら全てをカバーするのは困難です。
- サードパーティ製のツールは、 必要な全てのプラットフォームで動作しないことがあります。 コンパイラとは別々の更新サイクルが必要になります。
- ドキュメント化ツールをコンパイラの一部として組み込むことで、 全てのDの実装で標準化されます。いつでもデフォルトのものが使えるということは、 コメントによるドキュメント生成がより一層使われうることを意味しています。
関数
D には、グローバル関数、関数のオーバーロード、インライン化、 メンバ関数、仮想関数、関数ポインタ…など、 従来通りの関数は期待されるとおりにサポートしています。 それに加えて:
ネストした関数
関数は他の関数の中にネストすることができます。 これは、コードの局所性や、 関数クロージャを使うテクニックで有用です。
関数リテラル
無名関数を式中に直接埋め込めます。
動的クロージャ
ネストした関数やクラスのメンバ関数は、 クロージャ(デリゲートとも呼ばれます) として参照でき、ジェネリックプログラミングをより簡単で型安全にします。
in, out, ref 引数
これらの引数を指定するのは、 関数の可読性を上げるだけでなく、 何も失うことなくポインタの必要な場面を減らし、また、 コンパイラがコードの問題を発見しやすくする可能性を残します。
これによって、D を様々な外部のAPIと直接繋ぐことが可能になります。 "インターフェイス定義言語 (IDL)" のような対策は不要になるでしょう。
配列
Cの配列には、いくつか直すべき欠点があります:
- 配列に次元情報が保持されず、 別に格納したり引数として渡す必要があります。 皆さんご存じの例をあげれば、 main(int argc, char *argv[]) の引数です。 (Dでは、main は main(char[][] args)と宣言されます。)
- 配列がfirst-classオブジェクトではありません。配列を関数へ渡すと、 例えプロトタイプ宣言は配列に見える書き方をされていても、 ポインタに変換されます。 この変換によって、全ての配列型情報は失われます。
- Cの配列はサイズを変更できません。これは、単純な構造、例えばスタックでさえ、 複雑なクラスとして構成しなければならないことを意味しています。
- Cの配列はどこが境界であるかの情報を持たないため、 境界チェックができません。
- 配列は識別子の後ろに []
を置くことで宣言されます。
これは、配列へのポインタなどを宣言する際に構文がややこしくなる原因です:
int (*array)[3];
Dでは、配列宣言の [] は左に行きました:
int[3]* array; // int の3要素配列 へのポインタ を宣言 long[] func(int x); // longの配列を返す関数の宣言
ずっと理解しやすくなっています。
Dの配列には、 ポインタ、静的配列、動的配列、連想配列といった種類があります。
参照: 配列.
文字列
文字列の処理は非常にありふれた作業ですが、C/C++では扱いにくいものになっています。 この機能は、言語の直接サポートが必要です。 モダンな言語は文字列の連結やコピーなどをサポートしますが、D も同様です。 文字列周りの改善は、配列の扱いを改善したことの直接の帰結です。
リソース管理
自動メモリ管理
Dのメモリ割り当ては完全にガベージコレクタの下で行われます。 C++では経験的に、メモリ解放を管理するためには複雑な機能が 沢山必要であることがわかっています。ガベージコレクタによって、 言語は非常に簡単になります。
今は、ガベージコレクションは怠け者や初心者プログラマのためのものだ、 という認識があります。これと同じことが C++ に対しても言われていたのを思い出します。C++にできてCでできないこと、 もっと言えばアセンブラでできないこと、は結局の所存在しないのだそうな。
ガベージコレクションは、 CやC++では必要だった、 面倒な、 エラーの元になるメモリ割り当て管理のコードを不要にします。 これは開発時間を早め保守のコストを下げるだけでなく、 できたプログラムもしばしばより高速に動作するようになるのです!
もちろん、C++で使えるガベージコレクタはありますし、 実際私も自分の C++のプロジェクトでは使っています。しかしながら、 この言語はGCとの親和性が悪く、その効果性は十分発揮されません。 ランタイムライブラリの大くもGCと同時に使うことができないのです。
より完全な議論については、 ガベージコレクション の項をご覧下さい。
明示的なメモリ管理
DはGCを採用した言語ではありますが、 特定のクラスでは new/delete 演算をオーバーライドして、独自のメモリ割り当てを行うことができます。
RAII
RAII は、リソースの管理と解放を管理するための、 モダンなソフトウェア開発技術です。 Dは、RAIIをガベージコレクションのサイクルとは独立の、 制御・予測可能な方法でサポートします。
パフォーマンス
Lightweightな構造体
D は、C形式の単純な構造体をサポートしています。これには、 Cのデータ構造との互換性と、クラスの能力が過剰であるときに使えるように、 との二つの理由があります。
インラインアセンブラ
デバイスドライバやハイパフォーマンスが必要なシステムアプリケーション、 組み込みシステムなど、特化されたコードはしばしば、 アセンブリ言語に浸って仕事をなしとげる必要があります。 Dの実装は必ずしもインラインアセンブラを実装する必要はありませんが、 インラインアセンブラは、言語の一部として定義されています。 ほとんど場合必要なアセンブラコードは扱えるので、別のアセンブラやDLLは不必要になります。
多くのDの実装は、Cでのそれに似た、 組み込み関数をサポートしています。 I/Oポートの制御や特別な浮動小数点数演算への直接アクセスなどができます。
信頼性
モダンな言語は、できる限り、プログラマがバグを発見する手助けとなるべきです。 様々な形の"手助け"があります。 よりロバストな技術を簡単に使えるようにしたり、 明らかに間違ったコードを実行時に検出させるフラグをコンパイラにつけるなどなど。
契約
契約プログラミング (Contract Programming。B. Meyer によって発明されました)は、 プログラムの正しさを保証する助けとなる革命的な技術です。 DでのDBCは、関数の事前条件、事後条件、クラス不変条件、 assert契約を含んでいます。 Dの実装については、契約 の項をご覧下さい。
単体テスト
単体テストをクラスへ追加して、プログラムの起動時に自動実行することができます。 この機能は、気付かぬ内にクラスの実装にバグが入っていないかを、 ビルドのたびに確かめる助けになります。 単体テストは、クラスのソースコードの一部です。 書き上げたコードをテスターに放り投げる従来のテスト方法とは対照的に、 テストを作ることが、自然にクラスの開発の過程に含まれるようになります。
単体テストは他の言語でも可能ですが、 言語自体がその概念にぴったり適合していないため、 しっくりこない結果になります。単体テストは D の最大の特徴です。 Dのライブラリ関数は、単体テストによって、実際に関数が動くことの保証と、 使い方の説明の両方がうまくなされています。
WebでダウンロードできるC++のライブラリや、 アプリケーションのコードを考えて下さい。 そのうちのどれだけが、単体テストはもちろん、*多少なりとも*検証を行っているでしょう。 1% 未満? 普通我々は、コンパイルが通ったなら、動作すると仮定しています。 そして、コンパイラが警告を吐くと、実際にバグなのかそれとも余計なお世話なのか、 疑問に思って悩む程度です。
単体テストを契約プログラミングとともに用いることで、D は、 信頼性のあるロバストなシステムを構築するための最高の言語となります。 単体テストは、手に入れたDのコード片の質の簡単な評価手段にもなります - つまり、もしそのコードに単体テストや契約が書かれていなければ、 却下。
debug属性・debug文
デバッグは、言語の構文の一部になりました。 デバッグコードは、マクロやプリプロセッサのコマンドを使わずとも、 コンパイル時に有効にしたり無効にしたりできます。 debug構文は、 リリース用とデバッグ用の両方のコードを生成しなければならない現実のソースコードを、 理解しやすく、一貫性と移植性のあるものにします。
例外処理
単なる try-catch ではなく、より優れた try-catch-finally モデルが使われています。 finally を実現するためのダミーオブジェクトとデストラクタを作る必要はありません。
同期
マルチスレッドのプログラミングが、どんどん主流になってきています。 そこで、D はマルチスレッドのプログラムを作るための要素を提供します。 同期処理は、メソッドまたはオブジェクト単位でなされます。
synchronized int func() { ... }
同期された関数は、 一度に一つのスレッドだけが実行することができます。
同期された文は、周りにmutexを置くことで、 オブジェクト単位、またはグローバルにアクセスを制御します。
ロバストな技術のサポート
- ポインタの代わりに、動的配列
- ポインタの代わりに、参照変数
- ポインタの代わりに、参照オブジェクト
- 明示的メモリ管理の代わりに、ガベージコレクション
- スレッド同期処理のための組み込みプリミティブ
- コードを気付かぬうちに汚す"マクロ"を持たない
- マクロの代わりに、インライン関数
- ポインタの必要性を大幅に削減
- 整数型のサイズが明確
- char型の符号の扱いに不明確さなし
- ソースとヘッダで同じ宣言を書く必要なし
- デバッグコードの追加の明示的な構文サポート
コンパイル時チェック
- 強い型チェック
- ループ本体の 空の ; 禁止
- 代入の結果を論理値として使えない
- 廃止されたAPI の非推奨性チェック
実行時チェック
- assert() 式
- 配列の境界チェック
- switchの、未定義状態例外
- メモリ不足例外
- 事前,事後,クラス不変条件 による「契約プログラミング」サポート
互換性
演算子の優先順位、評価規則
D は、Cの演算子とその優先順位、 式の評価順序規則、 型の昇格規則を全て保っています。これにより、 C風に書いたのに実際の動作は違う、 という見つけにくいバグを避けることができます。
C API への直接アクセス
Dは、C の型に対応するデータ型があるだけでなく、 C の関数への直接アクセスを提供しています。 ラッパ関数や、パラメタの調整、集成体のメンバを一つ一つ設定するコード、 などは一切必要ありません。
C の全ての型のサポート
これによって、既存のCで書かれたライブラリやC形式のAPIとの インターフェイシングが可能になります。 Dは、構造体、共用体、 列挙型、ポインタ、C99の全ての基本データ型のサポートを含みます。 外部で規定されたデータ形式との互換性を保つために、 構造体メンバのアラインメントを設定する機能も備えています。
OS例外の処理
Dの例外処理メカニズムは、 動作するOSがアプリケーション の例外を扱う方法と結合されています。
既存のツールの利用
D は標準的なオブジェクトファイル形式のコードを生成します。これにより、 標準的なアセンブラ、リンカ、デバッガ、プロファイラ、exe圧縮ソフト、 その他の解析ソフトが使用でき、 他の言語で書かれたコードとのリンクも可能になります。
プロジェクト管理
バージョン付け
D には、 同じソースから複数のバージョンを生成する機能のサポートが組み込まれています。 これは C プリプロセッサでの #if/#endif テクニックに置き換わるものです。
非推奨属性
コードが発展して行くにつれ、古いライブラリのコードは、新しい、 より良いものに置き換わっていきます。 レガシーなコードに対応するためには古い版も残しておく必要がありますが、これらには deprecated という印をつけられます。deprecated な機能を使おうとするコードは 通常不正となりますが、コンパイラスイッチの設定によって許可もできます。 また、 メンテナンスプログラマが非推奨の機能への依存箇所を認識するのが簡単になります。
D プログラムのサンプル (sieve.d)
/* エラトステネスの素数ふるい */
import std.stdio;
bool[8191] flags;
int main()
{
int i, count, prime, k, iter;
writefln("10 iterations");
for (iter = 1; iter <= 10; iter++)
{
count = 0;
flags[] = 1;
for (i = 0; i < flags.length; i++)
{
if (flags[i])
{
prime = i + i + 3;
k = i + prime;
while (k < flags.length)
{
flags[k] = 0;
k += prime;
}
count += 1;
}
}
}
writefln("%d primes", count);
return 0;
}