配列

前回の章の演習の一つで、5つの変数を定義し、それらを特定の計算で使用した。それらの変数の定義は次の通りだった:

double value_1;
double value_2;
double value_3;
double value_4;
double value_5;
D

変数を個別に定義するこの方法は、さらに多くの変数が必要な場合には適用できない。1000個の値が必要だと想像してみてほしい。value_1からvalue_1000まで1000個の変数を定義することは、ほぼ不可能である。

このような場合に役立つのが配列だ。配列機能を使用すると、複数の値をまとめて1つの変数に定義することができる。配列は単純だが、値の集合を格納するために最もよく使用されるデータ構造だ。

この章では、配列の機能の一部のみを説明する。その他の機能は、後述のスライスとその他の配列機能の章で説明する。

定義

配列変数の定義は、通常の変数の定義とよく似ている。唯一の違いは、変数に関連付けられる値の数が角括弧で指定されることだ。2つの定義を次のように比較することができる。

int     singleValue;
int[10] arrayOfTenValues;
D

上の1行目は、これまで定義してきた変数と同じように、単一の値を格納する変数の定義だ。2行目は、10個の連続した値を格納する変数の定義だ。つまり、10個の整数値の配列を格納する。これは、同じ型の変数を10個定義すること、あるいは略して配列を定義することと考えることもできる。

したがって、上記の5つの別々の変数は、次の構文を使用して5つの値の配列として定義することができる。

double[5] values;
D

この定義は、5つのdouble値と読み替えることができる。単一の値を格納する変数と混同しないように、配列変数の名前は複数形にしたことに注意。単一の値のみを格納する変数は、スカラー変数と呼ばれる。

要約すると、配列変数の定義は、値の型、値の数、および値の配列を参照する変数の名前で構成される。

type_name[value_count] variable_name;
D

値の型は、ユーザー定義の型にすることもできる。(ユーザー定義の型については後で説明する。) 例えば、次のように記述できる。

// すべての都市の天気情報を保持する配列。
// ここでは、bool値は次の意味を持つ
//   false: 曇り
//   true : 晴れ
bool[cityCount] weatherConditions;

// 100個の箱の重量を保持する配列
double[100] boxWeights;

// 学校の生徒に関する情報
StudentInformation[studentCount] studentInformation;
D
コンテナと要素

特定の型の要素をまとめるデータ構造は、コンテナと呼ばれる。この定義によれば、配列はコンテナである。例えば、7月の毎日の気温を格納する配列は、31個のdouble値をまとめ、 double型の要素のコンテナを形成する。

コンテナの変数は要素と呼ばれる。配列の要素の数は、配列の長さと呼ばれる。

要素へのアクセス

前の章の演習で変数を区別するために、value_1のように、変数名にアンダースコアと数字を付け加えた。1つの配列が1つの名前ですべての値を格納する場合、これは不可能であり、必要もない。その代わりに、角括弧で囲んだ要素番号を指定して、要素にアクセスする。

values[0]
D

この式は、valuesという名前の配列の0番目の要素として読み取られる。つまり、配列を使用する場合は、value_1と入力する代わりに、values[0]と入力する必要がある。

ここで強調すべき2つの重要な点がある。

インデックス

要素の番号をインデックスと呼び、要素にアクセスする操作をインデックス付けと呼ぶ。

インデックスは定数値である必要はなく、変数の値もインデックスとして使用できるため、配列はさらに便利になる。例えば、以下のmonthIndex変数の値によって、月を決定することができる。

writeln("This month has ", monthDays[monthIndex], " days.");
D

monthIndexの値が2の場合、上記の式は3月の日数であるmonthDays[2]の値を出力する。

0から配列の長さより1少ないまでのインデックス値だけが有効だ。例えば、3要素の配列の有効なインデックスは0、1、2だ。無効なインデックスで配列にアクセスすると、プログラムはエラーで終了する。

配列は、要素がコンピュータのメモリ内に並べて格納されるコンテナである。例えば、各月の日数を格納する配列の要素は、次のように表示できる (2月が28日ある年を想定)。

インデックス01234567891011
要素312831303130313130313031

注釈:上記のインデックスは、説明のためにのみ使用されているもので、コンピュータのメモリには格納されていない。

インデックス0の要素の値は31(1月の日数)で、インデックス1の要素の値は28(2月の日数)などとなっている。

固定長配列と動的配列

配列の長さがプログラムが書かれた時点で指定されている場合、その配列は固定長配列である。配列の長さがプログラムの実行中に変更できる場合、その配列は動的配列である

上記で定義した両方の配列は、プログラムが書かれた時点で要素数が5と12と指定されているため、固定長配列である。これらの配列の長さは、プログラムの実行中に変更できない。長さを変更するには、ソースコードを修正し、プログラムを再コンパイルする必要がある。

動的配列の定義は、固定長配列の定義よりも簡単である。なぜなら、長さを省略するだけで動的配列になるからだ:

int[] dynamicArray;
D

このような配列の長さは、プログラムの実行中に増加または減少する可能性がある。

固定長配列は、静的配列とも呼ばれる。

.lengthを使用して要素の数を読み書きする

配列にもプロパティがあるが、ここでは.lengthだけを見てみよう。.lengthは、配列の要素数を返す。

writeln("The array has ", array.length, " elements.");
D

さらに、このプロパティに値を割り当てることで、動的配列の長さを変更することができる。

int[] array;            // 最初は空
array.length = 5;       // 現在は5つの要素がある
D
配列の例

5つの値を使った演習をもう一度見て、配列を使って書き直しよう。

import std.stdio;

void main() {
    // この変数はループカウンタとして使用される
    int counter;

    // 5要素のdouble型の固定長配列の定義
    //
    double[5] values;

    // ループで値を読み込む
    while (counter < values.length) {
        write("Value ", counter + 1, ": ");
        readf(" %s", &values[counter]);
        ++counter;
    }

    writeln("Twice the values:");
    counter = 0;
    while (counter < values.length) {
        writeln(values[counter] * 2);
        ++counter;
    }

    // 値の5分の1を計算するループは
    // 同様に記述する
}

観察: counterの値によって、ループが繰り返される (反復される) 回数が決まる。その値がvalues.length未満である間にループを反復することで、要素ごとに1回ずつループが実行される。この変数の値は各反復の最後に加算されるため、values[counter]式は配列の要素を1つずつ参照する。values[0]values[1]など。

このプログラムが前のプログラムよりも優れている点を理解するには、20個の値を読み込む必要がある場合を考えてみよう。上記のプログラムでは、5を20に置き換えるという1つの変更だけで済む。一方、配列を使用しないプログラムでは、20個の変数定義が必要になる。さらに、20個の値を反復処理するためのループを使用できないため、単一の値を持つ変数ごとに20回、複数の行を繰り返し記述する必要がある。

要素の初期化

Dのすべての変数と同様に、配列の要素も自動的に初期化される。要素の初期値は、要素の型によって異なる。intでは 0、doubleではdouble.nanなどだ。

上記のvalues配列のすべての要素は、double.nanに初期化されている:

double[5] values;     // 要素はすべてdouble.nanである
D

もちろん、要素の値はプログラムの実行中に後で変更することができる。これは、配列の要素に値を代入するときにも、すでに確認した。

monthDays[11] = 31;
D

これは、入力から値を読み込むときにも起こった。

readf(" %s", &values[counter]);
D

配列を定義する時点で、要素の望ましい値がわかっている場合もある。そのような場合、要素の初期値は、代入演算子の右側、角括弧で囲んで指定することができる。ユーザーから月の番号を読み込み、その月の日数を表示するプログラムで、これを確認しよう。

import std.stdio;

void main() {
    // 2月は28日あると仮定する
    int[12] monthDays =
        [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];

    write("Please enter the number of the month: ");
    int monthNumber;
    readf(" %s", &monthNumber);

    int index = monthNumber - 1;
    writeln("Month ", monthNumber, " has ",
            monthDays[index], " days.");
}

ご覧のとおり、monthDays配列は定義と初期化が同時に行われている。また、1から12までの範囲の月の番号は、0から11までの範囲の有効な配列インデックスに変換されていることに注意。1から12の範囲外の値が入力されると、プログラムはエラーで終了する。

配列を初期化するとき、右側に単一の値を使用することができる。その場合、配列のすべての要素がその値に初期化される。

int[10] allOnes = 1;    // すべての要素が1に設定されている
D
基本的な配列操作

配列は、すべての要素に適用される便利な操作を提供する。

固定長配列のコピー

代入演算子は、右側のすべての要素を左側にコピーする:

int[5] source = [ 10, 20, 30, 40, 50 ];
int[5] destination;

destination = source;
D

注釈:動的配列の場合、代入演算の意味はまったく異なる。これについては、後の章で説明する。

動的配列への要素の追加

~=演算子は、動的配列の末尾に新しい要素を追加する:

int[] array;                // 空
array ~= 7;                 // 配列は[7]と同じになった
array ~= 360;               // 配列は[7, 360]と同じになった
array ~= [ 30, 40 ];        // 配列は[7, 360, 30, 40]と同じになった
D

固定長配列には要素を追加することはできない:

int[10] array;
array ~= 7;                 // ← コンパイルエラー
D
動的配列から要素を削除する

配列の要素は、std.algorithmモジュールにあるremove()関数を使って削除することができる。同じ要素が複数のスライスに分割されている場合があるため、remove()は実際には配列の要素の数を変更することはできない。むしろ、配列の要素の一部を1つ以上左に移動する必要がある。そのため、remove操作の結果は、同じ配列変数に再代入する必要がある。

remove()には2つの異なる使用方法がある。

  1. 削除する要素のインデックスを指定する。例えば、次のコードは、インデックス1の要素を削除する。
    import std.stdio;
    import std.algorithm;
    
    void main() {
        int[] array = [ 10, 20, 30, 40 ];
        array = array.remove(1);                // 配列に再割り当て
        writeln(array);
    }
    [10, 30, 40]
  2. 削除する要素をラムダ関数で指定する。これについては、後の章で説明する。例えば、次のコードは、配列の要素のうち42に等しい要素を削除する。
    import std.stdio;
    import std.algorithm;
    
    void main() {
        int[] array = [ 10, 42, 20, 30, 42, 40 ];
        array = array.remove!(a => a == 42);    // 配列に再割り当て
        writeln(array);
    }
    [10, 20, 30, 40]
配列の結合

~演算子は、2つの配列を結合して新しい配列を作成する。~=は、2つの配列を結合し、結果を左側の配列に代入する。

import std.stdio;

void main() {
    int[10] first = 1;
    int[10] second = 2;
    int[] result;

    result = first ~ second;
    writeln(result.length);     // 20を表示

    result ~= first;
    writeln(result.length);     // 30を表示
}

~=演算子は、左側の配列が固定長配列の場合に使用できない:

int[20] result;
// ...
result ~= first;          // ← コンパイルエラー
D

配列のサイズが等しくない場合、代入時にエラーでプログラムが終了する:

int[10] first = 1;
int[10] second = 2;
int[21] result;

result = first ~ second;
D
object.Error@(0): コピーの配列の長さが一致しない: 20 != 21
Undefined
要素の並べ替え

std.algorithm.sortは、さまざまな型のコレクションの要素をソートすることができる。整数の場合、要素は小さい値から大きい値へとソートされる。sort()関数を使用するには、まずstd.algorithmモジュールをインポートする必要がある。(関数については、後の章で説明する。)

import std.stdio;
import std.algorithm;

void main() {
    int[] array = [ 4, 3, 1, 5, 2 ];
    sort(array);
    writeln(array);
}

出力:

[1, 2, 3, 4, 5]
要素の逆順化

std.algorithm.reverseは、要素をその位置で逆順に並べ替える(最初の要素が最後の要素になり、以下同様):

import std.stdio;
import std.algorithm;

void main() {
    int[] array = [ 4, 3, 1, 5, 2 ];
    reverse(array);
    writeln(array);
}

出力:

[2, 5, 1, 3, 4]
演習
  1. ユーザーに入力する値の数を尋ね、それらをすべて読み込むプログラムを作成しよう。sort()を使用して要素をソートし、reverse()を使用してソートした要素を逆順にしよう。
  2. 入力から数字を読み込み、奇数と偶数を順番に別々に表示するプログラムを書いてみよう。値-1は、数字の終わりを判断するために特別に扱い、その値は処理しないでほしい。

    例えば、次の数値が入力された場合、

    14723811-1

    プログラムは次のように出力する。

    13711248

    ヒント:要素を別々の配列に入れると良いかもしれない。数値が奇数か偶数かを判断するには、%(剰余)演算子を使用できる。

  3. 以下のプログラムは、期待したとおりに動作しない。このプログラムは、入力から5つの数値を読み込み、それらの数値の2乗を配列に格納するように書かれている。その後、プログラムは2乗を出力に表示しようとするが、代わりにエラーで終了してしまう。

    このプログラムのバグを修正し、期待どおりに動作するようにしよう:

    import std.stdio;
    
    void main() {
        int[5] squares;
    
        writeln("Please enter 5 numbers");
    
        int i = 0;
        while (i <= 5) {
            int number;
            write("Number ", i + 1, ": ");
            readf(" %s", &number);
    
            squares[i] = number * number;
            ++i;
        }
    
        writeln("=== The squares of the numbers ===");
        while (i <= squares.length) {
            write(squares[i], " ");
            ++i;
        }
    
        writeln();
    }