可変長パラメータ

この章では、関数を呼び出す際のパラメータの柔軟性を高める2つのDの機能について説明する。

デフォルト引数

関数パラメータの便利な機能として、デフォルト値を指定できることがある。これは、構造体メンバーのデフォルト初期値と似ている。

一部の関数のパラメータは、ほとんど同じ値で呼び出される。この例を見るために、string[string]型の連想配列の要素を出力する関数を考えてみよう。この関数は、区切り文字もパラメータとして受け取るものとする。

import std.algorithm;

// ...

void printAA(string title,
             string[string] aa,
             string keySeparator,
             string elementSeparator) {
    writeln("-- ", title, " --");

    auto keys = sort(aa.keys);

    // 最初の要素の前に要素区切り文字を表示しない
    if (keys.length != 0) {
        auto key = keys[0];
        write(key, keySeparator, aa[key]);
        keys = keys[1..$];    // 最初の要素を削除する
    }

    // 残りの要素の前に要素区切り文字を表示する
    foreach (key; keys) {
        write(elementSeparator);
        write(key, keySeparator, aa[key]);
    }

    writeln();
}
D
parameter_flexibility.1

この関数は、以下で ":"をキー区切り文字として、 ", "要素区切り文字として呼び出されている:

void main() {
    string[string] dictionary = [
        "blue":"mavi", "red":"kırmızı", "gray":"gri" ];

    printAA("Color Dictionary", dictionary, ":", ", ");
}
D

出力:

-- 色の辞書 --
青:mavi, 灰色:gri, 赤:kırmızı

セパレータがほとんどの場合この2つになる場合は、デフォルト値を定義しておくことができる。

void printAA(string title,
             string[string] aa,
             string keySeparator = ": ",
             string elementSeparator = ", ") {
    // ...
}
D

デフォルト値を持つパラメータは、関数を呼び出すときに指定する必要はない。

printAA("Color Dictionary",
        dictionary);  /* ← 区切り文字が指定されていない。
                       *   両方のパラメータは
                       *   それぞれのデフォルト値になる。 */
D

必要に応じて、パラメータの値を指定することはできる。必ずしもすべて指定する必要はない。

printAA("Color Dictionary", dictionary, "=");
D

出力:

-- 色の辞書 --
青=mavi, 灰色=gri, 赤=kırmızı

次の呼び出しでは、両方のパラメーターを指定している:

printAA("Color Dictionary", dictionary, "=", "\n");
D

出力:

-- 色の辞書 --
青=mavi
灰色=gri
赤=kırmızı

デフォルト値は、パラメータリストの最後に位置するパラメータにのみ定義できる。

デフォルト引数としての特殊キーワード

次の特別なキーワードは、コード内の出現位置に対応する値を持つコンパイル時リテラルのように機能する。

これらはコードのどこでも使用できるが、デフォルト引数として使用すると動作が異なる。通常のコードで使用した場合、その値はコード内の出現場所を参照する。

import std.stdio;

void func(int parameter) {
    writefln("Inside function %s at file %s, line %s.",
             __FUNCTION__, __FILE__, __LINE__);    // ← 行は7
}

void main() {
    func(42);
}
D
parameter_flexibility.2

報告された7行目は関数内にある。(訳注: 改変前のソースでは__LINE__は5行目にある)

ファイルdeneme.dの7行目、関数deneme.func 内で。

しかし、関数の定義がある場所ではなく、関数が呼び出されている行を特定したい場合もある。これらの特別なキーワードがデフォルト引数として指定されている場合、その値は関数が呼び出されている場所を参照する。

import std.stdio;

void func(int parameter,
          string functionName = __FUNCTION__,
          string file = __FILE__,
          int line = __LINE__) {
    writefln("Called from function %s at file %s, line %s.",
             functionName, file, line);
}

void main() {
    func(42);    // ← 行は14
}
D
parameter_flexibility.3

この場合、特別なキーワードは、関数の呼び出し元であるmain()を指している。(訳注: 改変前のソースでは__LINE__は12行目にある)

ファイルdeneme.dの14行目にある関数deneme.mainから呼び出された。

上記に加えて、コンパイラや時刻に応じて値を取る、以下の特別なトークンもある

可変長引数関数

見た目とは違って、デフォルトのパラメータ値は関数が受け取るパラメータの数を変更しない。例えば、いくつかのパラメータにデフォルト値が割り当てられている場合でも、printAA()は常に4つのパラメータを受け取り、実装に応じてそれらを使用する。

一方、可変長引数関数は、引数の数が指定されていない状態で呼び出すことができる。この機能は、writeln()などの関数ですでに利用されている。writeln()は、引数の数がいくつでも呼び出すことができる。

writeln(
    "hello", 7, "world", 9.8 /*, 必要に応じて任意の数の
                              *  その他の引数 */);
D

Dでは、可変長引数関数を4つの方法で定義することができる。

可変長引数関数の引数は、スライスとして関数に渡される。可変長引数関数は、特定の型のスライスを1つの引数として定義し、その直後に...文字を続けて定義する。

double sum(double[] numbers...) {
    double result = 0.0;

    foreach (number; numbers) {
        result += number;
    }

    return result;
}
D

この定義により、sum()は可変長引数関数となり、double、またはdoubleに暗黙的に変換できるその他の型であれば、引数の数を問わず受け取ることができる。

writeln(sum(1.1, 2.2, 3.3));
D

単一のスライスパラメータと...文字は、すべての引数を表す。例えば、5つのdouble値で関数が呼び出された場合、スライスは5つの要素を持つことになる。

実際、変数個のパラメーターは単一の切り出しとして渡すこともできる:

writeln(sum([ 1.1, 2.2, 3.3 ]));    // 上記と同じ
D

可変長引数関数には、パラメータリストで最初に定義しなければならない必須のパラメータも指定できる。例えば、次の関数は、括弧内に指定された数のパラメータを、その数に関係なく出力する。この関数は、要素の数を柔軟に指定できるが、括弧は常に指定する必要がある。

char[] parenthesize(
    string opening,  // ← 最初の2つのパラメータは
    string closing,  //   関数を呼び出すときに指定する必要がある
    string[] words...) {  // ← 指定する必要はない
    char[] result;

    foreach (word; words) {
        result ~= opening;
        result ~= word;
        result ~= closing;
    }

    return result;
}
D

最初の2つのパラメーターは必須である:

parenthesize("{");     // ← コンパイルエラー
D

必須パラメーターが指定されている限り、残りはオプションだ:

writeln(parenthesize("{", "}", "apple", "pear", "banana"));
D

出力:

{apple}{pear}{banana}
可変長引数関数の引数の有効期間は短い

可変長引数に対して自動的に生成されるスライス引数は、寿命の短い一時的な配列を指す。関数がその実行中にのみ引数を使用する場合、この事実は問題にはならない。しかし、関数が後で使用するためにそれらの要素へのスライスを保持すると、バグになる。

int[] numbersForLaterUse;

void foo(int[] numbers...) {
    numbersForLaterUse = numbers;    // ← バグ
}

struct S {
    string[] namesForLaterUse;

    void foo(string[] names...) {
        namesForLaterUse = names;    // ← バグ
    }
}

void bar() {
    foo(1, 10, 100);  /* 一時的な配列[ 1, 10, 100 ]は
                       * この先では無効。 */

    auto s = S();
    s.foo("hello", "world");  /* 一時的な配列
                               * [ "hello", "world" ]は
                               * この先では無効。 */

    // ...
}

void main() {
    bar();
}
D
parameter_flexibility.4

独立関数foo()とメンバー関数S.foo()は、プログラムスタック上に存在する自動的に生成された一時配列へのスライスを保存しているため、どちらもエラーになる。これらの配列は、可変長引数関数の実行中にのみ有効である。

そのため、関数が可変長引数の要素のスライスを格納する必要がある場合は、まずそれらの要素のコピーを取得する必要がある。

void foo(int[] numbers...) {
    numbersForLaterUse = numbers.dup;    // ← 正しい
}

// ...

    void foo(string[] names...) {
        namesForLaterUse = names.dup;    // ← 正しい
    }
D

ただし、可変長引数関数は、適切な配列のスライスでも呼び出すことができるため、その場合は要素のコピーは不要になる。

正しく効率的な解決策は、同じ名前で、1つは可変長引数を受け取り、もう1つは適切なスライスを受け取る2つの関数を定義することだ。呼び出し側が可変長の引数を渡した場合、可変長引数を受け取る関数が呼び出され、呼び出し側が適切なスライスを渡した場合、適切なスライスを受け取る関数が呼び出される。

int[] numbersForLaterUse;

void foo(int[] numbers...) {
    /* これはfoo()の可変長引数版であるため、
     * スライスを格納する前に要素のコピーを
     * 作成する必要がある。 */
    numbersForLaterUse = numbers.dup;
}

void foo(int[] numbers) {
    /* これはfoo()の非可変長引数版であるため、
     * スライスをそのまま格納できる。 */
    numbersForLaterUse = numbers;
}

struct S {
    string[] namesForLaterUse;

    void foo(string[] names...) {
        /* これはS.foo()の可変長引数版であるため、
         * スライスを格納する前に
         * 要素のコピーを作成する必要がある。 */
        namesForLaterUse = names.dup;
    }

    void foo(string[] names) {
        /* これはS.foo()の非可変長引数版であるため、
         * スライスをそのまま格納できる。 */
        namesForLaterUse = names;
    }
}

void bar() {
    // この呼び出しは、可変引数関数に送信される。
    foo(1, 10, 100);

    // この呼び出しは適切なスライス関数にディスパッチされる。
    foo([ 2, 20, 200 ]);

    auto s = S();

    // の呼び出しは、可変引数関数に送信される。
    s.foo("hello", "world");

    // この呼び出しは適切なスライス関数にディスパッチされる。
    s.foo([ "hi", "moon" ]);

    // ...
}

void main() {
    bar();
}
D
parameter_flexibility.5

同じ名前で異なるパラメータを持つ複数の関数を定義することを、関数のオーバーロードと呼ぶ。これは次の章で説明する。

演習

次のenumが既に定義されていると仮定する:

enum Operation { add, subtract, multiply, divide }
D

また、操作とその2つのオペランドの計算を表すstructが存在すると仮定する:

struct Calculation {
    Operation op;
    double first;
    double second;
}
D

例えば、オブジェクトCalculation(Operation.divide, 7.7, 8.8)は、7.7を8.8で割った結果を表す。

これらのstructオブジェクトを不特定の数受け取り、各Calculationの結果を計算し、その結果をすべてdouble[]型のスライスとして返す関数を設計しよう。

例えば、次のコードのように関数を呼び出すことができるようにする必要がある。

void main() {
    writeln(
        calculate(Calculation(Operation.add, 1.1, 2.2),
                  Calculation(Operation.subtract, 3.3, 4.4),
                  Calculation(Operation.multiply, 5.5, 6.6),
                  Calculation(Operation.divide, 7.7, 8.8)));
}
D

コードの出力は次のようなものになるはずだ:

[3.3, -1.1, 36.3, 0.875]