ネストされた関数、構造体、およびクラス

これまで、関数、構造体、およびクラスは、最外側のスコープ (つまり、モジュールスコープ)で定義してきた。これらは、内側のスコープでも定義することができる。内側のスコープで定義すると、シンボルの可視性を狭めることでカプセル化に役立ち、また、関数ポインタ、デリゲート、およびラムダの章で見たクロージャを作成することができる。

例として、次のouterFunc()関数には、ネストされた関数、ネストされたstruct、およびネストされたclassの定義が含まれている。

void outerFunc(int parameter) {
    int local;

    void nestedFunc() {
        local = parameter * 2;
    }

    struct NestedStruct {
        void memberFunc() {
            local /= parameter;
        }
    }

    class NestedClass {
        void memberFunc() {
            local += parameter;
        }
    }

    // このスコープ内のネストされた定義を使用する:

    nestedFunc();

    auto s = NestedStruct();
    s.memberFunc();

    auto c = new NestedClass();
    c.memberFunc();
}

void main() {
    outerFunc(42);
}

他の変数と同様に、ネストされた定義は、その外側のスコープで定義されているシンボルにアクセスできる。例えば、上記の3つのネストされた定義はすべて、parameterおよびlocalという名前の変数を使用できる。

通常どおり、ネストされた定義の名前は、それらが定義されているスコープ内でのみ有効だ。例えば、nestedFunc()NestedStruct、およびNestedClassは、main()からはアクセスできない。

void main() {
    auto a = NestedStruct();              // ← コンパイルエラー
    auto b = outerFunc.NestedStruct();    // ← コンパイルエラー
}

その名前にはアクセスできないが、ネストされた定義は他のスコープでは使用できる。例えば、Phobosのアルゴリズムの多くは、Phobos関数内で定義されたネストされた構造体によってタスクを処理している。

この例を見るために、両端を交互に消費するスライスを消費する関数を設計しよう。

import std.stdio;
import std.array;

auto alternatingEnds(T)(T[] slice) {
    bool isFromFront = true;

    struct EndAlternatingRange {
        bool empty() const {
            return slice.empty;
        }

        T front() const {
            return isFromFront ? slice.front : slice.back;
        }

        void popFront() {
            if (isFromFront) {
                slice.popFront();
                isFromFront = false;

            } else {
                slice.popBack();
                isFromFront = true;
            }
        }
    }

    return EndAlternatingRange();
}

void main() {
    auto a = alternatingEnds([ 1, 2, 3, 4, 5 ]);
    writeln(a);
}

ネストされたstructmain()内では名前を付けることができないが、依然として使用可能だ:

[1, 5, 2, 4, 3]

注釈:これらの型は、そのスコープ外ではその名前を使用できないため、ハリー・ポッターの登場人物にちなんで"ヴォルデモート型"と呼ばれている。

alternatingEnds()が返すネストされたたstructには、メンバー変数は一切ないことに注意。structは、関数パラメータsliceとローカル関数変数isFromFrontのみを使用してそのタスクを処理する。返されたオブジェクトは、それが作成されたコンテキストを離れた後も、これらの変数を安全に使用できるのは、自動的に作成されクロージャによるものである。クロージャについては、関数ポインタ、デリゲート、およびラムダの章で説明した。

staticクロージャが不要な場合

ネストされた定義はコンテキストを存続させるため、通常の定義よりもコストがかかる。さらに、関連付けられているコンテキストを決定するためのコンテキストポインタを含める必要があるため、ネストされた定義のオブジェクトはより多くのスペースを占める。例えば、次の2つの構造体はまったく同じメンバー変数を持っているが、そのサイズは異なる。

import std.stdio;

struct ModuleStruct {
    int i;

    void memberFunc() {
    }
}

void moduleFunc() {
    struct NestedStruct {
        int i;

        void memberFunc() {
        }
    }

    writefln("OuterStruct: %s bytes, NestedStruct: %s bytes.",
             ModuleStruct.sizeof, NestedStruct.sizeof);
}

void main() {
    moduleFunc();
}

他の環境では、2つの構造体のサイズが異なる場合もある:

外側の構造体ネストされた構造体
4バイト16バイト

ただし、一部のネストされた定義は、外側のコンテキストの変数にアクセスする必要がなく、可能な限りローカルに保持するためだけに使用される。このような場合、関連するコストは不要である。staticキーワードは、ネストされた定義からコンテキストポインタを削除し、モジュール内の対応する定義と等価にする。その結果、staticネストされた定義は外側のコンテキストにアクセスできない:

void outerFunc(int parameter) {
    static class NestedClass {
        int i;

        this() {
            i = parameter;    // ← コンパイルエラー
        }
    }
}
D

ネストされたclassオブジェクトのコンテキストポインタは、その.outerプロパティを通じてvoid*として利用できる。例えば、同じスコープで定義されているため、次の2つのオブジェクトのコンテキストポインタは等しい。

void foo() {
    class C {
    }

    auto a = new C();
    auto b = new C();

    assert(a.outer is b.outer);
}
D

後で説明するように、クラス内にネストされたクラスでは、コンテキストポインタの型は、void*ではなく、外側のクラスの型になる。

クラス内にネストされたクラス

classが別の 内にネストされている場合、ネストされたオブジェクトが関連付けられるコンテキストは、外側のオブジェクト自体になる。

このようなネストされたクラスは、this.new構文で構築される。必要に応じて、ネストされたオブジェクトの外側のオブジェクトはthis.outerでアクセスできる:

class OuterClass {
    int outerMember;

    class NestedClass {
        int func() {
            /* ネストされたクラスは、外側のクラスのメンバーに
             * アクセスできる。 */
            return outerMember * 2;
        }

        OuterClass context() {
            /* ネストされたクラスは、'.outer'によって、その外側のオブジェクト
             * (つまりそのコンテキスト)にアクセスできる。 */
            return this.outer;
        }
    }

    NestedClass algorithm() {
        /* 外側のクラスは、'.new'によって、
         * ネストされたオブジェクトを構築できる。 */
        return this.new NestedClass();
    }
}

void main() {
    auto outerObject = new OuterClass();

    /* 外側のクラスのメンバー関数は、
     * ネストされたオブジェクトを返している: */
    auto nestedObject = outerObject.algorithm();

    /* ネストされたオブジェクトは、プログラム内で使用される: */
    nestedObject.func();

    /* 当然、nestedObjectのコンテキストは
     * outerObjectと同じだ: */
    assert(nestedObject.context() is outerObject);
}

this.newthis.outerの代わりに、既存のオブジェクトに対して.new.outerを使用することもできる:

auto var = new OuterClass();
auto nestedObject = var.new OuterClass.NestedClass();
auto var2 = nestedObject.outer;
D
要約