はじめに
令和2年、D言語版の基礎文法最速マスターです。
よく見る文法や記述をサクッと紹介していきます。
これを見ていただければ少なからず言語としての雰囲気が分かっていただけると思います。
過去に @repeatedly さんが大体書かれているので、少し令和風に追記・編集する形でやっていきます。
- D言語基礎文法最速マスター (Thanks @repeatedly!)
動かしながら網羅的に学びたい方は以下D言語ツアーを、正規表現などもう少し実用的な書き方を知りたい方はD言語Cookbookもご覧ください。
- D言語ツアー
- D言語Cookbook
正直D言語ツアーで十分な気もしますが頑張ってやっていきます。
1. 基礎
ビルドツール DUB
プロジェクト準備
dub
というビルドツールがあり、多くの場合はこれを使えばソースファイルなどが作られてプログラムを書く準備が整います。
細かく色々やりたい場合はコンパイラを直接使うこともできます。
こちらOSのパッケージマネージャーや公式サイトからダウンロードでき、LinuxでもWindowsでもMacでも同じコマンドになっています。
mkdir app
cd app
dub init
app
source
app.d
dub.json
ビルド・実行
実行ファイルの作成や実行もコマンド1つで簡単です。
dub build
dub run
ソースファイル
文字コードはUTF-8で記述します。ファイルの拡張子は .d
です。
main
D言語は用意した main
という関数から実行されます。エントリポイントと呼ばれます。
処理のまとまりを {}
で区切るスタイルで、C言語やJava、C#などと同様になっています。
void main() { }
void main(string[] args) { }
C言語では必要だった戻り値が省略できます。記述することもできます。
int main() { }
int main(string[] args) { }
import文
import
文を使って各種ライブラリを読み込みます。
たとえばコンソール画面に文字を出力するには、標準ライブラリの std.stdio
を読み込んで使います。
これによって write
や writeln
、 writef
や writefln
といった関数が使えるようになります。
import std.stdio; // 読み込み
void main() {
write(1); // 改行なし
writeln(1); // 改行あり
writef("%02d", 1); // フォーマット指定改行なし
writefln("%02d", 1); // フォーマット指定改行あり
}
特定のシンボル(定義)のみをインポートすることもできます。これは選択インポートと呼ばれます。
import std.stdio : writeln;
writeln("Hello");
write("Hello"); // 利用できない
変数の宣言
D言語では 型 変数名;
というように宣言します。
int a; // デフォルト値で初期化されます。intなら0です。
また、初期化に使った値から型を推論して省略するための auto
という予約語があります。
auto a = "auto"; // aはstring型
他の修飾子がある場合も型推論が効きます。
const a = "hoge"; // const(string)型
コメント
主に3種類あります。
// 一行コメント
/*
複数行コメント
*/
/+ /+
ネスト可能複数行コメント
+/ +/
これに定義の説明文などを書き、ドキュメント生成したりツールチップで表示されるよう拡張した「DDocコメント」と呼ばれるバージョンがあります。
記述ではMarkdown構文も利用できます。
また複数行コメントは形式的に毎行 *
や +
を付けて見やすくデコレーションする文化があります。
/// 1行で関数の説明を書くDDocコメント
/**
* 複数行で説明を書くDDocコメント
*
* Params:
* x = 引数の説明など、他にも細かい記述が可能
*/
/++
+ 複数行コメントと同様のネスト可能バージョン
+/
2. データ型
数値
整数と浮動小数点数があります。
C言語とは異なり、すべてサイズ固定です。
整数は4種、頭にuがつくと符号なしになります。
byte n1; // 1byte 符号あり
ubyte n2; // 1byte 符号なし
short n3; // 2byte 符号あり
ushort n4; // 2byte 符号なし
int n5; // 4byte 符号あり
uint n6; // 4byte 符号なし
long n7; // 8byte 符号あり
ulong n8; // 8byte 符号なし
浮動小数点数は3種です。基本は double
で、 float
にするには末尾に f
を付けます。
float num1 = 1.234f; // fを付けるとfloat
double num2 = 1.234;
real num3 = 5.678; // ハードウェアがサポートする最大精度(x86 CPUなら80bit)
ちなみに複素数型もありますが非推奨です。標準ライブラリの std.complex
を利用します。
四則演算
よくある記号のパターンで、 +
-
*
/
の4つを使います。
int num; // 整数型
num = 1 + 1;
num = 1 - 1;
num = 1 * 2;
num = 5 / 2; // 2
num = 5 % 2; // 1
演算子のどちらかが浮動小数点数の場合、結果も浮動小数点数になります。
double num = 5.0 / 2; // 2.5(numがintなどの整数型だとコンパイルエラー)
インクリメントとデクリメント
前置も後置もサポートされています。
「数値的に1を足す/引く」という論理的な意味が規定されています。ポインタを進めるような用途では利用されません。
i++;
++i;
--i;
i--;
真偽値
真と偽の2値を表す専用の bool
型が存在します。
真が true
で偽が false
です。
bool flag = true;
flag = false;
定数(enum)
定数を表す enum
があります。
enum StringTypes {
UTF8,
UTF16,
UTF32,
}
auto t = StringTypes.UTF8;
文字列
文字列は基本UTF-8型で string
型が用意されており、ダブルクォートで囲んで構築します。
文字単体では char
型です。
ダブルクォートの中では、バックスラッシュを使ったエスケープシーケンスで \t
(タブ)や \n
(改行)などの特殊文字を利用することができます。
string str1 = "abc";
string str2 = "a\tbc\n";
char c = str1[0];
これにUTF-16用の wchar
ベースで wstring
、UTF-32用の dchar
ベースで dstring
と3種あり、それぞれ接尾語をつけることでリテラルの型を明示的に指定できます。
string str3 = "hello"c // 各文字はchar型
wstring str4 = "hello"w // 各文字はwchar型
dstring str5 = "hello"d // 各文字はdchar型
なお、D言語での文字列は、後述する配列の一種に過ぎません(string
は immutable(char)[]
と同じ)。
これは「変更できないUTF-8型の文字を要素に持つ動的配列(スライス)」という意味です。
文字列操作
// 結合
auto str = "aaa" ~ "bbb";
// 長さ(バイト)
auto length = "abcdef".length;
// 切り出し
auto substr = "abcd"[0..2]; // "ab"
/* これ以降のものはstd.stringが必要です */
import std.string;
// 分割
auto record = "aaa,bbb,ccc".split(","); // ["aaa", "bbb", "ccc"]
// 検索
auto idx = "abcd".indexOf("bc"); // 見つかった場合はその位置、見つからなかった場合は-1
配列(動的配列)
配列の型は要素型の後ろに []
を付けて宣言します。
初期化は [要素, 要素]
と []
の中にカンマ区切りで記述します。
int[] arr = [100, 200, 300];
string[] urls = ["https://dlang.org", "https://github.com/"];
C言語とよく比較されますが、[]
が型のほうについているのが違いです。
変数宣言は一貫して 型 変数名;
という形式で読めるようになっています。
要素の参照と代入
int[] a = [10, 20];
// 参照
a = arr[0]; // 10
b = arr[1]; // 20
c = arr[5]; // Error! 要素数より多いと例外が投げられる
// 代入
arr[0] = 1;
arr[1] = 2;
arr[5] = 5; // 参照と同じくエラー
要素の個数
size_t len = arr.length; // size_t は 32bit環境ならuint、64bit環境ならulongです
// 要素を増やす(増えた分はデフォルト値で埋められます)
arr.length += 10;
配列のスライス
配列の一部領域を取り出したり、まとめて値を設定したりできます。
int[] arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
int[] r = arr[2 .. 5]; // [2, 3, 4]
arr[6 .. 8] = 10; // [0, 1, 2, 3, 4, 5, 10, 10, 8, 9]
全体のスライス、ある場所から最後まで、といった操作に対して簡単な記法がサポートされています。
arr[] = 100; // 全要素を 100 に設定
arr[5 .. $] = 200; // インデックスで5から最後までを 200 に設定
スライス同士の代入なども可能です。
arr[0 .. 100] = arr2[100 .. 200]; // あるスライスを別のスライスに代入可能
ベクトル演算
スライスの応用で、配列の一部に対するベクトル演算が可能です。ループも必要なくなるかもしれません。
ちなみにベクトル演算とは、CPUの特殊な命令(AVX2など)を用いて複数要素の計算をまとめて行うことで高速化を図るものです。
auto a = [1, 2, 3];
a[] += 10; // [11, 12, 13]
配列の操作
std.array
を使うとD言語での標準的なインターフェイス(Range)が利用できます。
import std.array;
auto arr = [1, 2, 3];
// 先頭を取得
auto a = arr.front; // aは1
// 先頭を削除
arr.popFront(); // arrは[2, 3]
// 先頭に追加(push系がないのでinsertで)
arr.insert(0, 5); // arrは[5, 2, 3]
// 末尾を取得
auto b = arr.back; // bは3
// 末尾を削除
arr.popBack(); // arrは[5, 2]
// 末尾に追加
arr ~= 9; // arrは[5, 2, 9]
連想配列
連想配列も []
を使います。
連想配列の型は、 要素型[キー型]
という形式で宣言します。
初期化は []
の中でキーと値を :
で区切ります。
int[string] hash = ["a" : 1, "b" : 2];
要素の参照と代入
// 参照
hash["a"] // 1
hash["b"] // 2
hash["z"] // 配列と同じく例外が投げられる
// 代入
hash["c"] = 5
hash["d"] = 7
連想配列の操作
// キーの取得
auto hashKeys = hash.keys; // ["a", "b", "c", "d"]
// 値の取得
auto hashValues = hash.values; // [1, 2, 5, 7]
// キーの存在確認
auto val = "a" in hash; // valには1へのポインタ、なければnull
// ハッシュのペアの削除
hash.remove("a");
値がなければデフォルト値、といった記述には require
関数を使います。
int[string] settings;
auto timeout = settings.require("timeout", 120);
3. 制御文
if文
if (cond) {
// do something
}
if ~ else文
if (cond) {
// do something
} else {
// do something
}
if ~ else if 文
if (cond) {
// do something
} else if (cond) {
// do something
}
switch文
Cとは違い、文字列が使えたり case
を並べて書くことが出来ます。
switch (command) {
case "foo", "bar":
// do something
break;
case "baz":
// do something
break;
default:
}
これを拡張した final switch
という構文もあります。
これは主に enum
を対象としたもので、すべての分岐を網羅的に書かないとコンパイルエラーになるというものです。
enum Types {
A,
B,
}
Types t;
final switch (t) {
case Types.A:
writeln("A");
// Bの分岐がないためエラー
}
while文
uint i;
while (i < 5) {
// do something
++i;
}
for文
for (size_t i = 0; i < 5; i++) {
// do something
}
foreach文
配列などの要素を処理するため、効率よく記述できるようにしたものです。
配列や連想配列、opApply
を実装していたり Range
の機能を満たすオブジェクトが処理対象となります。
foreach (elem; arr) {
// do something
}
インデックスがほしいときも変数を増やすだけです。
foreach (i, elem; arr) {
// do something
}
連想配列も同様です。
int[string] dic;
foreach (key, value; dic) {
}
要素を弄りたい場合は ref
をつけます。
foreach (ref elem; arr) {
// do something
elem += 10;
}
数値的に区間を定めることもできます。
開始と終了のインデックスは一度しか評価されないことが保証されています。
foreach (i; 0 .. 100) {
// do something
}
with文
要素へのアクセスを簡略化する with
文があります。VB.NETと同じような機能です。
struct S {
int value;
}
S s;
auto a = s.value;
with (s) {
auto x = value; // 単なる value が見つからない場合、 s.value と同じ意味になる
}
いろいろな記述を省略できるようにするためのものなので、final switch
構文で各 case
における enum
へのアクセスを省略するような書き方で使われることが多いです。
enum Types {
A,
B,
}
Types t;
final switch (t) with (Types) {
case A:
writeln("A");
break;
case B:
writeln("B");
break;
}
スコープガード文
処理のブロックを抜けるときの処理をブロックの途中に書くことができる機能です。
使い終わったら破棄が必要なオブジェクトに対し、初期化とセットで破棄を書いて破棄忘れを予防するような機能です。
scope (条件) 処理
といった記述になります。
auto f = new Resource();
scope (exit) f.dispose();
// do something
他であまり見られない機能としては、「抜けるときに必ず」「成功」「失敗」で3種の書き分けができることです。
処理が失敗したときだけロールバックしたり通知する、といった処理も簡単に書くことができます。
auto res = new Resource();
scope (exit) {
res.dispose();
writeln("必ず実行");
}
scope (success) writeln("成功時のみ");
scope (failure) writeln("失敗時のみ");
Synchronized文
マルチスレッドの同期を簡単に行う synchronized
文が組み込まれています。
これによって1つのスレッドしか実行できない範囲を明示することができます。
Mutex
で同期のコンテキストを指定することもできます。
ulong count = 0;
Thread[] threads;
foreach (i; 0 .. 1000) {
auto t = new Thread({
synchronized {
count++;
}
});
threads ~= t;
}
// do something
4. 関数/デリゲート
関数
関数はC言語と同じように記述します。
戻り値 関数名(引数一覧) { }
という形式です。
int square(int n) {
return n * n;
}
D言語ではこれに色々と追加の指定があります。
たとえば引数に in
を付けると入力を表し書き換え不可、 ref
は参照として書き換え可能、他にも遅延評価(使うときまで計算されない)のための lazy
などがあります。
void foo(in int x, ref int y, lazy int z) { }
関数の処理内容に対して制約掛けるための属性も多数あります。
例外(Exception)を投げない nothrow
、 安全とマークされた関数しか実行しない @safe
、グローバル変数などの副作用がない pure
、GCが動作しないことを示す @nogc
などです。
void foo() nothrow pure @safe @nogc { }
// まとめて指定する範囲指定の構文やブロック構文もあります
nothrow:
void hoge() {} // nothrow
@nogc {
void fuga() {} // nothrow @nogc
void hage() pure {} // nothrow pure @nogc
}
デリゲート
デリゲートはネストされた関数などのことです。
クロージャと呼ばれる動的に作られた関数オブジェクトもデリゲート型の一種となります。
uint delegate() counter; // 引数なし、戻り値がuintのデリゲート
// デリゲートを返すネストされた関数
uint delegate() createCounter()
{
uint count;
return { return ++count; }; // {}はdelegateリテラル(引数がないので()は省略)
}
counter = createCounter();
writeln(counter()); // 1
writeln(counter()); // 2
以下のようなラムダシンタックスもあります。
void f(uint delegate(uint) d)
{
writeln(d(10));
}
void main()
{
f((x) { return x + 2; }); // 12
f(x => x + 2); // ラムダシンタックス(上と等価)
}
5. 知っておいた方がよい文法・高度な機能
標準出力
標準入出力は std.stdio
というモジュールにあり、 write
や writeln
といった便利な関数が提供されます。
これを使うと文字列以外にも数値や配列を含む引数の出力が簡単に行えます。
特に writeln
は様々な用途で見かけるもので非常に便利に使うことができます。
-
write
- 引数をそのまま出力
-
writeln
- 引数を出力後、最後に改行コードを出力
-
writef
- 引数を書式化文字列に従って出力
-
writefln
- 引数を書式化文字列に従って出力後、最後に改行コードを出力
import std.stdio;
void main() {
// Hello, world!\n
write("Hello, ");
writeln("world!");
// [10, 20, 30]
int[] arr = [10, 20, 30];
writeln(arr);
}
書式化文字列に使える構文はC言語に近い構成になっています。
// -----
// 10, 20, 30
// -----
writef("-----\n%d, %d, %d\n-----", 10, 20, 30);
// test: 100
writefln("test: %d", 100);
また、書式化文字列はテンプレート引数として渡すことにより引数の型と一致をチェックしたり、実行時の解析コストをなくすこともできます。
// コンパイル時に引数チェックと書式化文字列の解析を行う
writefln!"-----\n%d, %d, %d\n-----"(10, 20, 30);
インターフェース
JavaやC#と同様の機能を提供しています。
後述のクラスに対して実装すべき関数などを制約として定義するものです。
interface MyIntRange {
bool empty() const;
int front() const;
void popFront();
}
なお final
という属性を付けることで、クラス側に書かなくてもインターフェースで固定された実装を持たせることができるようになっています。
interface MyMessage {
final void print() {
writeln("MyMessage.print");
}
}
クラス
JavaやC#と同様の機能を提供しています。
class Person
{
private: // 以降はprivate
string name_;
public: // 以降はpublic
@property
{
// @properyをつけるとname()ではなくnameで呼び出せるようになる
// p.name
string name() const { return name_; }
// p.name = newName
void name(string name) { name_ = name; }
}
this(string name) // コンストラクタはthis
{
name_ = name;
}
...
} // Cなどと違い;はいらない
その他にも interface
、PODとして扱える struct
や union
、演算子オーバーロードなどもあります。
また、D言語はGCを使ってメモリ管理しているため、new
した後 delete
する必要はありません(自ら malloc
などした場合は勿論 free
が必要です)。
テンプレート
C++と同じようなテンプレート機能を提供しています。
これは特定の宣言のうち、一部の型などを入れ替えて使う汎用的な定義であり、文字通り「テンプレート」として動作します。
static if
といったコンパイル時計算の分岐もあり、C++よりも幾分すっきり書けるようになってます。以下は階乗を計算するテンプレートです。
template factorial(int n)
{
static if (n == 1)
enum factorial = 1;
else
enum factorial = n * factorial!(n - 1);
}
/* C++などとは違い<>を使わず!()を使う */
factorial!(5) // 120
この static if
は通常の if
と違ってスコープを生成しないため、変数宣言の切り替えなどが非常にやりやすくなっています。
クラスや関数(UFCSの項参照)でも使えます。
class Hoge(T)
{
static if (is(T == long)) {
int value;
} else {
T value;
}
}
テンプレートミックスイン
単一継承のD言語では、クラス間で実装を共有する時にテンプレートを使います。
mixin template HogeImpl()
{
uint hoge_;
uint hogeTwice() { return hoge_ * hoge_; }
}
class A
{
mixin HogeImpl; // Aで定義したかのように使える
}
class B
{
mixin HogeImpl; // 同様
}
UFCS (Uniform Function Call Syntax)
クラスなどのメソッド呼び出しのように第一引数を外に出して使えるシンタックスシュガーです。
void foo(T)(T obj) {}
// 本来はfoo([1, 2, 3])とか
[1, 2, 3].foo();
5.foo();
2.5f.foo();
std.array
や std.string
で定義された関数が var.method()
のように呼べたのはこの機能のおかげです。
例外処理
一般によく知られている Exception
クラスを使った例外機構があります。
構文的には、 throw
および try
- catch
- finally
が利用できます。
void foo() {
throw new Exception("エラー");
}
try {
foo();
}
catch (Exception e) {
writeln(e.msg); // エラー
}
finally {
writeln("終了");
}
Range
D言語の標準ライブラリは今このコンセプトを中心に開発されており、核となる std.range
に満たすべきインターフェイスが定義されています。
以下は std.range
ベースのアルゴリズムの例です。
import std.algorithm;
auto arr = [1, 2, 3, 4, 5];
// 条件に合うものだけを選ぶ
filter!("a % 2 == 0")(arr); // [2, 4]
// '"a % 2 == 0"'の代わりに'a => a % 2 == 0'でもOK、delegateと文字列両方受け付ける
filter!(a => a % 2 == 0)(arr); // [2, 4]
// 条件に合うものを除く
remove!("a % 2 == 0")(arr); // [1, 3, 5]
// 加工結果をRangeで返す
map!("a * a")(arr); // [1, 4, 9, 16, 25]
// 降順にソートする
sort!("a > b")(arr); // [5, 4, 3, 2, 1]
基本的に、これらのアルゴリズムが返すのは arr
の型ではなく、それぞれの計算を隠蔽した Range
オブジェクトです。これによって、Range
は基本的には計算を遅延し、それぞれの Range
をつなげてもなるべく中間オブジェクトを作らないように工夫されています。
配列として結果を返して欲しい時は std.array
の array
関数を使います。
import std.array;
array(map!("a * a")(arr));
これらはUFCSを使うと、以下のように処理の流れ通り気持ちよく記述することができます。
// 連続した操作もすっきり
arr.filter!("a % 2 == 0").map!("a * a").array();
型変換
数値を文字列にしたり、逆に文字列を数値に変換するなど、型変換を簡単に行うために to
という汎用テンプレート関数があります。
import std.conv;
int num = 100;
auto str = to!string(num);
auto num2 = to!int(str);
UFCSの機能を使うと変換を後置できるので、処理の流れを綺麗に書くこともできます。
auto str = num.to!string();
auto num2 = str.to!string();
単体テスト
unittest
というキーワードで囲った所に単体テストをかけます。
満たすべき条件は assert
という式で書くことができます。
大体どこにでも書けるのが特徴で、クラス内など宣言の近くに書くのがD言語の作法です。
unittest
{
bool foo() { /* do something */ return flag; }
assert(foo());
}
この単体テストは dub
コマンドを使うと、 dub test
という形式で実行することができます。
またDDoc形式のドキュメントコメントをHTMLなどに出力する際、直前の定義のサンプルとして使うこともできます。
関数とセットにすると以下のようになります。
/// 引数を二乗します
double square(double x) {
return x * x;
}
/// ditto
unittest {
assert(square(1.0) == 1.0);
}
ちなみに ditto
というのは「同上」という意味の特別なワードです。
契約
関数の入力や出力が満たすべき条件を記述することができ、関数の責務を明示することができます。
それぞれ「事前条件」と「事後条件」と呼ばれ、 in
と out
というキーワードで条件を描くことにより定義します。
またリリースモードでは処理が取り除かれるのでパフォーマンスには影響しません。
double sqrt(double x)
in (x >= 0) // 入力に負数は禁止
out (result; result * result == x) // 戻り値は2回掛けると元に戻る
do {
// do something
}
構造体やクラスに向けて、必ず満たしているべき条件を書く「不変条件」というのもあります。
キーワードとしては invariant
というものです。
これはクラスが2つの配列を持っていて、それらが必ず同じ長さでないといけない場合などに使います。
たとえば以下の例だと、arr1
だけ設定するとその時点で長さがずれてエラーになるため、必ず setArrays
を呼ぶことを強制できます。
class SomeArrays {
int[] arr1;
double[] arr2;
void setArrays(int[] arr1, double[] arr2) {
this.arr1 = arr1;
this.arr2 = arr2;
}
invariant {
assert(arr1.length == arr2.length); // 必ず満たすべき条件
}
}
CTFE(Compile Time Function Excecute)
D言語ではコンパイル時に色々な処理をすることが出来ます。
コンパイル時にメモリの確保やクラスの生成も出来ますし、例外も投げれますし、もちろん数値計算や配列も操作出来ます。
そのため、コンパイル時に定数を渡せばそれなりに色々な関数が動作します。
たとえば、以下のような乱数生成も普通にコンパイル時に動きます。(シードは0固定ですが)
enum
で宣言した変数に対し、初期化に使う式がコンパイル時計算の対象となります。
ulong gen()
{
Random r;
popFrontN(r, 1000);
return r.front;
}
enum num = gen(); // gen()がコンパイル時に計算されて結果だけが保持される
終わりに
まだまだ書けてない機能は多数ありますが、よく見る主要な機能や魅力的な部分は大体書けたかと思います。
メタプログラミング関連の機能が強く、CやJavaに似た構成であり学習曲線が低いのが特徴です。
コンパイルや実行も高速で様々な分野で利用されています。
Qiitaにもいろいろ記事がありますが、わからないことがあればTwitterで #dlang をつけてつぶやくと大体誰かが答えてくれるのでぜひ聞いてみてください。