配列
配列には四種類あります:
構文 | 説明 |
---|---|
type* | データへのポインタ |
type[integer] | 静的配列 |
type[] | 動的配列 |
type[type] | 連想配列 |
ポインタ
int* p;
Cのポインタと類似の、 データへの単純なポインタがあります。 ポインタは、Cへのインターフェイスと、特定のシステム処理の 用途のために残されています。 ポインタには長さの情報が付随していないため、 コンパイラやランタイムで境界チェックなどをすることができません。 大抵の場合、ポインタは、 動的配列や out , ref パラメタと参照型で置き換えることが出来ます。
静的配列
int[3] s;
Cの配列に似たものもあります。 静的配列はコンパイル時に決まる固定サイズを持ちます。
静的配列の総サイズが16Mbを越えることは出来ません。 そのような巨大配列には、動的配列を使ってください。
サイズ0の静的配列を作ることも可能ですが、 そこにはメモリは割り当てられません。 これは主に可変長構造体の最後のメンバとしてや、 テンプレート展開の末端のケースに利用されます。
静的配列は値型ですが、Cの静的配列と同様に、 関数引数としては参照渡しされ、 関数から値を返すことはできません。
動的配列
int[] a;
動的配列は、サイズ情報と配列データへのポインタを保持しています。 複数の動的配列がデータの一部または全部を共有することも可能です。
配列の宣言
配列の宣言には、 前置と後置の二種類あります。 前置形式が、推奨される方式です。
前置配列宣言
識別子の前に書く前置形式では、配列宣言は 左から右へと読むことができます。従って:
int[] a; // int の動的配列 int[4][3] b; // int の4要素配列 の3要素配列 int[][5] c; // int の動的配列 の5要素配列 int*[]*[3] d; // int へのポインタ の動的配列 へのポインタ の3要素配列 int[]* e; // int の動的配列 へのポインタ
後置配列宣言
識別子の後ろに書く後置形式では、 配列宣言は右から左へと読みます。 それぞれのグループは等価な宣言です:
// int 動的配列 int[] a; int a[]; // int の4要素配列 の3要素配列 int[4][3] b; int[4] b[3]; int b[3][4]; // int の動的配列 の5要素配列 int[][5] c; int[] c[5]; int c[5][]; // int へのポインタ の動的配列 へのポインタ の3要素配列 int*[]*[3] d; int*[]* d[3]; int* (*d[3])[]; // int の動的配列 へのポインタ int[]* e; int (*e)[];
理由: 後置形式は、 CとC++での配列宣言の形式と一致しています。 このため、この形式をサポートすることでプログラマのDへの移行が簡単になるでしょう。
使い方
配列への操作は広範囲にわたりますが、 大きく二種類に分けることができます。 一つは配列のハンドルへの操作、 一つは配列の内容への操作です。 Cでは配列のハンドルへの操作しかありませんでしたが、 Dでは双方が使用可能です。
配列へのハンドルは、 配列の名前によって指定します:
int* p; int[3] s; int[] a; int* q; int[3] t; int[] b; p = q; // p は q と同じものを指す p = s.ptr; // p は 配列s の先頭要素を指す p = a.ptr; // p は 配列a の先頭要素を指す s = ...; // エラー // s は配列への静的な参照としてコンパイルされている a = p; // エラー // pによって指されている配列のサイズが不明 a = s; // a は配列s を指すように初期化 a = b; // a は配列b と同じものを指す
スライシング
配列を スライスするとは、配列の一部分を取り出すことを言います。 配列のスライスは元のデータのコピーではなく、 単なる別の参照になります。 例えば:
int[10] a; // intの10要素配列の宣言 int[] b; b = a[1..3]; // a[1..3] は、 // a[1] と a[2] からなる2要素配列 foo(b[1]); // foo(0)と同じ a[2] = 3; foo(b[1]); // foo(3)と同じ
単に [] と書くと、配列全体のスライスを表します。 例えば配列bへの代入は:
int[10] a; int[] b; b = a; b = a[]; b = a[0 .. a.length];
全て同じ意味となります。
スライシング は他の配列の一部分を参照するのに便利なだけでなく、 ポインタを境界チェック付きの配列に変換するのにも使えます。
int* p; int[] b = p[0..8];
配列のコピー
スライス演算子が代入式の左辺に現れると、 配列への参照ではなく、 配列の内容がコピーの対象となることを示します。 配列内容のコピーは、 左辺がスライスで右辺が同じ型の配列かポインタの時に発生します。
int[3] s; int[3] t; s[] = t; // t[3] の3つの要素が s[3] へコピーされる s[] = t[]; // t[3] の3つの要素が s[3] へコピーされる s[1..2] = t[0..1]; // s[1] = t[0] と同じ意味 s[0..2] = t[1..3]; // s[0] = t[1], s[1] = t[2] と同じ意味 s[0..4] = t[0..4]; // エラー。s には 3要素しかない s[0..2] = t; // エラー。左辺と右辺で要素数が違う
範囲の重なるコピーはエラーです:
s[0..2] = s[1..3]; // エラー,範囲重複 s[1..3] = s[0..2]; // エラー,範囲重複
範囲の重なるコピーを禁止することで、 Cの逐次のセマンティクスを越えた、 より強力なコードの並列最適化が可能になります。
配列へのデータセット
代入式の左辺にスライス、 右辺に要素型と同じ型の値が来ると、 左辺の配列の内容が全て 右辺の値にセットされます。
int[3] s; int* p; s[] = 3; // s[0] = 3, s[1] = 3, s[2] = 3 と同じ意味 p[0..2] = 3; // p[0] = 3, p[1] = 3 と同じ意味
配列の結合
二項演算子 ~ は、連結演算子です。 配列をつなげるのに使います:
int[] a; int[] b; int[] c; a = b ~ c; // bとcをつなげて // 新しい配列を作る
多くの言語は + 演算子を連結の意味でオーバーロードしていますが、 これは次の式の出力結果に混乱をもたらします:
"10" + 3 + 4
この式は 整数17 を作るのか、それとも文字列 "1034" あるいは "107" を結果とするのでしょうか? 言語の設計者は、これをあいまいにしないように注意深く規則 - 間違って実装されたり,見過ごされたり忘れられたりする規則 - を定めなくてはなりません。 それよりは、 + は加算の意味、 と決めてしまって連結には別の演算子を定義する方がベターです。
同様に、~= 演算子は末尾への追加の意味になります:
a ~= b; // a は a と b の連結
配列の連結は、 片方が長さ 0 の配列であっても必ずコピーを伴います:
a = b; // a は b を指す a = b ~ c[0..0]; // a は b のコピーを指す
配列への追加が必ずコピーを行うとは限りません。詳細は 動的配列のサイズ変更 をご覧下さい。
配列演算
多くの配列演算(ベクトル演算)を、 ループではなくより高レベルに表現することが可能です。 例えば、以下のループ
T[] a, b;
...
for (size_t i = 0; i < a.length; i++)
a[i] = b[i] + 4;
は、a の各要素に b の各要素に 4 を足した値を代入するものですが、 これは配列演算で以下のように記述できます:
T[] a, b; ... a[] = b[] + 4;
ベクトル演算は、スライス演算子が =, +=, -=, *=, /=, %=, ^=, &=, |= 演算子の左辺に来たときに行われます。 その場合の右辺式には、同じ長さと型の配列スライスか、 要素型の値を返す式の、 任意の組み合わせが使用できます。 ベクトル演算に対応している演算子は、 二項演算子 +, -, *, /, %, ^, &, |, と、 単項演算子 -, ~ です。
左辺のスライスと右辺のスライスには重なりがないものとします。 ベクトル代入演算は右から左の順番で評価され、 その他の二項演算子は左から右へ評価されます。 全てのオペランドはちょうど1回ずつ評価されます。 これは長さゼロの配列スライスを扱う場合も同様です。
配列の各要素が計算される順序は実装依存の定義で、 並列実行される可能性もあります。 アプリケーションは演算の順序に依存してはなりません。
実装ノート: 典型的なベクトル演算の多くは、 ターゲットマシンのベクトル演算命令を活用することが期待されます。
ポインタ演算
int[3] abc; // intの3要素静的配列 int[] def = [ 1, 2, 3 ]; // intの3要素動的配列 void dibb(int* array) { array[2]; // *(array + 2) と同義 *(array + 2); // 3番目の要素を得る } void diss(int[] array) { array[2]; // ok *(array + 2); // エラー。配列はポインタではない } void ditt(int[3] array) { array[2]; // ok *(array + 2); // エラー、配列はポインタではない }
"rectangular" 配列
熟練したFORTRANの数値プログラマは、多次元の"長方形型"配列が、 行列演算のようなものを実装するのに、"配列へのポインタの配列" を使うよりずっと高速なことをご存じでしょう。 例えば D の構文:
double[][] matrix;
は、配列へのポインタの配列を宣言します。(動的配列は、配列データへのポインタとして 実装されています。)これは配列のサイズが変わる可能性がある(動的配列なので)ためで、 このような実装はよく "ぎざぎざ" 配列と呼ばれています。最適化を考えるとさらに悪いことに、 しばしば違う行が同じ要素を指すなんてこともあるのです! 幸い、D の静的配列は、 同じ構文でも固定の長方形型のメモリレイアウトを持っています:
double[3][3] matrix;
これは3行3列の配列の宣言で、全ての要素がメモリ上に連続に配置されます。 他の言語でいうと、これは多次元配列と呼ばれ、次のように宣言するようです:
double matrix[3,3];
配列の長さ
静的/動的を問わず、配列の [ ] の中では、 lengthという名前の変数が暗黙に宣言され、 その配列の長さがセットされます。 $ という記号を使うこともできます。
int[4] foo; int[] bar = foo; int* p = &foo[0]; // 以下の式は全て同じ意味: bar[] bar[0 .. 4] bar[0 .. length] bar[0 .. $] bar[0 .. bar.length] p[0 .. length] // 'length' は未定義。pは配列ではない bar[0]+length // 'length' は未定義。[ ] の外 bar[length-1] // 配列の最後の要素を取得
配列のプロパティ
静的配列のプロパティは:
プロパティ | 説明 |
---|---|
.init | 要素型のデフォルト初期化子を返します |
.sizeof | 配列の長さと 一要素ごとのバイト数をかけた値 |
.length | 配列の要素数。 静的配列では、定数。 型は size_t となる。 |
.ptr | 配列の先頭要素を指すポインタを返す。 |
.dup | 同じサイズの動的配列を作り、 そこに要素をコピーして返す。 |
.idup | 同じサイズの動的配列を作り、 そこに要素をコピーして返す。 コピーの型はimmutableとなる。 D 2.0 のみ。 |
.reverse | 配列中の要素をin-placeで逆順に並べる。 配列自身を返す。 |
.sort | 配列中の要素をin-placeでソートする。 配列自身を返す。 |
動的配列のプロパティは:
プロパティ | 説明 |
---|---|
.init | null |
.sizeof | 動的配列の参照のサイズを返す。 32bitマシンでは 8。 |
.length | 動的配列の長さを取得/設定 型は size_t となる。 |
.ptr | 配列の先頭要素を指すポインタを返す。 |
.dup | 同じサイズの動的配列を作り、 そこに要素をコピーして返す。 |
.idup | 同じサイズの動的配列を作り、 そこに要素をコピーして返す。 コピーの型はimmutableとなる。 D 2.0 のみ。 |
.reverse | 配列中の要素をin-placeで逆順に並べる。 配列自身を返す。 |
.sort | 配列中の要素をin-placeでソートする。 配列自身を返す。 |
クラスオブジェクトの配列に対して .sort プロパティを使うには、 そのクラスに関数 int opCmp(Object) が定義されている必要があります。この関数は、 並べ替えの順序を決定します。引数の型は Object であって、そのクラス自身の型ではないことに注意してください。
構造体や共用体の配列に対して .sort プロパティを使うには、 その構造体/共用体に 関数 int opCmp(S) または int opCmp(S*) が定義されている必要があります。 型 S はその構造体や共用体自身の型です。 この関数は、並べ替えの順序を決定します。
例:
int* p; int[3] s; int[] a; p.length; // エラー、ポインタの長さはわからない s.length; // コンパイル時定数、3 a.length; // 実行時の値 p.dup; // エラー、長さ不明 s.dup; // 3要素の配列を作り、 // そこにsの要素をコピー a.dup; // a.length要素の配列を作り、 // そこにaの要素をコピー
動的配列のサイズ設定
動的配列の .length プロパティは、= 演算子の左辺値にして、 値を設定することが出来ます:
array.length = 7;
これによって、配列用のメモリが再確保され、 既存の内容がコピーされます。 新しい長さの方が短ければ、メモリの再確保は行われず、コピーも発生しません。 以下のスライシングと同じ意味となります:
array = array[0..7];新しい長さの方が長けれれば、 残りはデフォルト初期化されます。
実行効率を重視して、ランタイムは、 できる限りその場でバッファを リサイズすることで余計なコピーを避けようとします。 サイズの大きくなる場合は、new演算子やリサイズ操作で割り当てられたのではない配列は、かならずコピーされます。
これは、スライスを取った直後にリサイズすると、 リサイズ後の配列がスライスと重なる可能性があることを意味します。すなわち、
char[] a = new char[20]; char[] b = a[0..10]; char[] c = a[10..20]; b.length = 15; // a[]からのスライスで、15文字分の領域はあるので、 // 常にin-placeでリサイズされる。 b[11] = 'x'; // a[11] と c[1] にも影響する。 a.length = 1; a.length = 20; // メモリレイアウトに変化はない c.length = 12; // 常にコピーされる。なぜなら、c[] はGCの割り当てブロックの // 先頭ではない c[5] = 'y'; // a[] や b[] の内容には影響しない。 a.length = 25; // コピーは起きるかもしれない。起きないかもしれない。 a[3] = 'z'; // 古いa[3]と重なっているb[3]に影響するかもしれないし、 // しないかもしれない
確実にコピーさせるには、.dup プロパティを使ってリサイズ可能で ユニークな配列を取得します。
この問題は、~= 演算子による連結時にも生じます。 ~ 演算子による連結ならば必ずコピーされるので、 この問題はありません。
動的配列をリサイズするのは、比較的コストの高い操作です。そこで、 配列を値で埋める方法は:
int[] array; while (1) { c = getinput(); if (!c) break; array.length = array.length + 1; array[array.length - 1] = c; }
これでも動作しますが、非効率的です。 リサイズ回数を最小にするもっと実際的なアプローチは次の通りです:
int[] array; array.length = 100; // 初期推測値 for (i = 0; 1; i++) { c = getinput(); if (!c) break; if (i == array.length) array.length = array.length * 2; array[i] = c; } array.length = i;
うまい初期推測値を与えるのはある種のテクニックですが、 たいていの場合は99%の場合をカバーできる上手い値を見つけられます。 例えば、コンソールからのユーザー入力を受け取るには … 普通は80文字を越えないでしょう。
配列のプロパティとしての関数
関数の第一引数が配列だった場合、 その関数はあたかも配列のプロパティであるかのように呼び出せます:
int[] array; void foo(int[] a, int x); foo(array, 3); array.foo(3); // 同じ意味
配列の境界チェック
0より小さい値や配列の長さ以上の値をindexとして配列に与えるのは誤りです。 この種のエラーがコンパイル時に見つかればコンパイルエラーになり、 実行時ならば ArrayBoundsError 例外が発生します。 しかし、 境界チェックに頼ったプログラムを書くべきではありません。 例えば次のプログラムは間違っています:
try { for (i = 0; ; i++) { array[i] = 5; } } catch (ArrayBoundsError) { // ループ終了 }このループを正しく書くと:
for (i = 0; i < array.length; i++)
{
array[i] = 5;
}
実装ノート: コンパイラは、コンパイル時にも境界エラーを 検出できるようにすべきです。例えば:
int[3] foo; int x = foo[3]; // エラー、indexの範囲外
また、コンパイル時のスイッチによって、 実行時の境界チェックを ON/OFF できるのが望ましいです。
配列の初期化
デフォルトの初期化
- ポインタは null で初期化されます
- 静的配列の各要素は、 要素型のデフォルト初期化で初期化されます
- 動的配列は要素数0に初期化されます
- 連想配列は要素数0に初期化されます
void 初期化
void初期化は、配列の Initializer が void の時の処理です。これは、何も初期化が行われない - つまり、配列の内容が未定義になるという動作です。 これは、実行効率の最適化にもっとも役立ちます。 void初期化は熟練者向けの技術で、 プロファイルによって配列初期化がネックと判明した時にのみ使用すべきです。
静的配列の静的初期化
静的初期化は、配列要素を [ ] で囲って指定します。値は、 インデックスと : を前につけることもできます。 インデックスが指定されなかった場合は、 先頭要素なら 0、それ以外なら直前プラス1 となります。
int[3] a = [ 1:2, 3 ]; // a[0] = 0, a[1] = 2, a[2] = 3
配列のindexをenumで与えたいとき、この書き方が一番簡単です:
enum Color { red, blue, green }; int value[Color.max + 1] = [ Color.blue:6, Color.green:2, Color.red:5 ];
これらの配列は、グローバルスコープに現れたときのみstaticです。 そうでない場合は、static配列とするには、 const か static と宣言してください。
特殊な配列型
文字列
文字列とは、 文字の配列です。文字列リテラルは、 文字の配列を簡単に記述する単なる手段に過ぎません。 文字列リテラルは immutable (読み取り専用)です。
char[] str; char[] str1 = "abc"; str[0] = 'b'; // エラー。"abc" は読み取り専用。クラッシュの可能性あり
名前 string が char[] の alias として定義されています。 従って、上の宣言は以下のように書き直すことが出来ます:
string str;
string str1 = "abc";
char[] 文字列は UTF-8 形式です。 wchar[] 文字列は in UTF-16 形式です。 dchar[] 文字列は in UTF-32 形式です。
文字列は、コピー、比較、結合などの操作が可能です:
str1 = str2;
if (str1 < str3) ...
func(str3 ~ str4);
str4 ~= str1;
どれもご覧の通りの意味を持っています。途中で生じた一時オブジェクトは、 ガベージコレクタ(あるいは、alloca())が回収します。 それだけでなく、これらの操作は特別な文字列配列だけでなく、 どんな配列に対しても可能です。
charへのポインタも作れます:
char* p = &str[3]; // 第4要素へのポインタ char* p = str; // 先頭要素へのポインタ
しかし、Dの文字列は0終端ではないので、 Cの文字列へ変換するには 末尾に0を追加する必要があります:
str ~= "\0";
あるいは std.string.toStringz 関数を使います
文字列の型は、 コンパイル時の意味解析の段階で決定します。 型は、char[], wchar[], dchar[] のいずれかで、 暗黙の変換規則によって一つに決定します。 二種類の変換が同じくらい適切であった場合はエラーになります。 この曖昧さをなくすには、 キャストを使うか、接尾辞 c, w, d を使うと良いでしょう:
cast(wchar [])"abc" // これはwchar文字の配列 "abc"w // これも
接尾辞なしで、キャストもされていない文字列リテラルは、 必要に応じて暗黙に char[], wchar[], dchar[] に変換されます。
char c; wchar w; dchar d; c = 'b'; // c には 'b' が入る w = 'b'; // w には wchar文字 'b' が入る w = 'bc'; // エラー - 1度に1文字ずつ w = "b"[0]; // w にはwchar文字'b'が入る w = "\r"[0]; // w にはwcharの復帰文字が入る d = 'd'; // d には文字 'd' が入る
C の printf() と文字列
printf() はCの関数で、Dの一部ではありません。 printf() は0終端の、C形式の文字列をプリントしようとします。そこで、printf() で D形式の文字列を扱うには二つの方法があります。一つは、終端0 を補ってから、 (char*) へ変換する方法:
str ~= "\0"; printf("the string is '%s'\n", cast(char*)str);
あるいは:
import std.string; printf("the string is '%s'\n", std.string.toStringz(str));
文字列リテラルだけは、後ろに 0 が必ず入るようになっていて、 直接Cの関数に渡すこともできます:
printf("the string is '%s'\n", cast(char*)"string literal");
さて、printfの第一引数の文字列リテラルにキャストが要らないのはなぜでしょうか? それは、第一引数は char* 型とプロトタイプ宣言されていて、 文字列リテラルは char* に暗黙変換可能だからです。 これに対して、printf の残りの引数は ... で指定する可変長部分です。 文字列リテラルをそのまま与えると、可変長引数には (length,pointer) の組として渡ることになってしまいます。
2番目の方法は、精度指定を使うことです。Dの配列のメモリレイアウトは、 最初に長さが来るようになっていますので、次のコードはうまく行きます:
printf("the string is '%.*s'\n", str);
でも一番良いのは、Dの文字列を扱える std.stdio.writefln を使うことです:
import std.stdio; writefln("the string is '%s'", str);
暗黙変換
ポインタ T* は、 暗黙に以下の型へ変換できます。
- void*
静的配列 T[dim] は、 暗黙に以下のいずれかへと変換できます:
- T[]
- U[]
- void[]
動的配列 T[] は、 暗黙に以下のいずれかへと変換できます:
- U[]
- void[]
ただし U は T の基底クラスとします