ユーザー定義属性(UDA)

あらゆる宣言(構造体型、クラス型、変数など)に属性を割り当てることができ、その属性はコンパイル時にアクセスしてコードのコンパイル方法を変更することができる。ユーザー定義属性は、純粋にコンパイル時の機能だ。

ユーザー定義属性の構文は、@記号とそれに続く属性で構成され、属性が割り当てられる宣言の前に記述する。例えば、次のコードは、Encrypted属性をnameの宣言に割り当てている。

@Encrypted string name;
D

複数の属性は、個別に指定することも、括弧で囲んだ属性のリストとして指定することもできる。例えば、次の2つの変数は同じ属性を持っている。

@Encrypted @Colored string lastName;     // ← 別々に
@(Encrypted, Colored) string address;    // ← 一緒に
D

属性は、型名だけでなく、ユーザー定義型または基本型の値にもすることができる。ただし、その意味が明確でない場合があるため、42のようなリテラル値で構成される属性は使用しないことをお勧めする。

struct Encrypted {
}

enum Color { black, blue, red }

struct Colored {
    Color color;
}

void main() {
    @Encrypted           int a;    // ← 型名
    @Encrypted()         int b;    // ← オブジェクト
    @Colored(Color.blue) int c;    // ← オブジェクト
    @(42)                int d;    // ← リテラル(使用は推奨されない)
}

上記のabの属性は、種類が異なる。aの属性は型Encrypted自体だが、bの属性は Encryptedのオブジェクトだ。これは、コンパイル時に属性の使用方法に影響を与える重要な違いだ。この違いの例を以下に示す。

属性の意味は、プログラムの要件に応じてプログラマーが独自に決定する。属性は、コンパイル時に__traits(getAttributes)によって決定され、その属性に従ってコードがコンパイルされる。

以下のコードは、特定のstructメンバー(例:Person.name)の属性を__traits(getAttributes)からアクセスする方法を示している:

import std.stdio;

// ...

struct Person {
    @Encrypted @Colored(Color.blue) string name;
    string lastName;
    @Colored(Color.red) string address;
}

void main() {
    foreach (attr; __traits(getAttributes, Person.name)) {
        writeln(attr.stringof);
    }
}
D

プログラムの出力には、Person.nameの属性が一覧表示される:

Encrypted
Colored(cast(Color)1)

ユーザー定義の属性を扱う場合、次の2つの__traits式も便利だ。

import std.string;

// ...

void main() {
    foreach (memberName; __traits(allMembers, Person)) {
        writef("The attributes of %-8s:", memberName);

        foreach (attr; __traits(getAttributes,
                                __traits(getMember,
                                         Person, memberName))) {
            writef(" %s", attr.stringof);
        }

        writeln();
    }
}
D

プログラムの出力は、Personのすべてのメンバーのすべての属性を一覧表示する:

nameの属性Encrypted Colored(cast(Color)1)
lastNameの属性
addressの属性Colored(cast(Color)2)

もう1つの便利なツールはstd.traits.hasUDAで、シンボルが特定の属性を持つかどうかを判定する。次のstatic assertは、Person.nameEncrypted属性を持つため、パスする:

import std.traits;

// ...

static assert(hasUDA!(Person.name, Encrypted));
D

hasUDAは、属性型だけでなく、その型の特定の値とも使用できる。次のstatic assertは、Person.nameColored(Color.blue)属性があるため、両方ともチェックに合格する。

static assert(hasUDA!(Person.name, Colored));
static assert(hasUDA!(Person.name, Colored(Color.blue)));
D

structオブジェクトのすべてのメンバーの値をXML形式で出力する関数テンプレートを設計しよう。次の関数は、出力時に各メンバーのEncryptedおよびColored属性を考慮する。

void printAsXML(T)(T object) {
// ...

    foreach (member; __traits(allMembers, T)) {             // (1)
        string value =
            __traits(getMember, object, member).to!string;  // (2)

        static if (hasUDA!(__traits(getMember, T, member),  // (3)
                           Encrypted)) {
            value = value.encrypted.to!string;
        }

        writefln(`  <%1$s color="%2$s">%3$s</%1$s>`, member,
                 colorAttributeOf!(T, member), value);      // (4)
    }
}
D

コードのハイライト部分は以下で説明する:

  1. 型のメンバーは、__traits(allMembers)によって決定される。
  2. 各メンバーの値は、後で出力に表示する際に使用するために、stringに変換される。例えば、メンバーが "name"の場合、右側の式はobject.name.to!stringになる。
  3. 各メンバーは、hasUDAでテストされ、Encrypted属性があるかどうかが判断される。その属性がある場合、メンバーの値は暗号化される。(hasUDAはシンボルで動作するため__traits(getMember)を使用してメンバーをシンボルとして取得する方法(例:Person.name)に注意。)
  4. 各メンバーの色属性は、colorAttributeOf()で決定される。これは後で説明する。

colorAttributeOf()関数テンプレートは、次のコードのように実装できる。

Color colorAttributeOf(T, string memberName)() {
    foreach (attr; __traits(getAttributes,
                            __traits(getMember, T, memberName))) {
        static if (is (typeof(attr) == Colored)) {
            return attr.color;
        }
    }

    return Color.black;
}
D

コンパイル時の評価が完了すると、printAsXML()関数テンプレートは、Person型に対して、以下の関数と同等のインスタンス化される。

/* printAsXML!Personインスタンスと同等。 */
void printAsXML_Person(Person object) {
// ...

    {
        string value = object.name.to!string;
        value = value.encrypted.to!string;
        writefln(`  <%1$s color="%2$s">%3$s</%1$s>`,
                 "name", Color.blue, value);
    }
    {
        string value = object.lastName.to!string;
        writefln(`  <%1$s color="%2$s">%3$s</%1$s>`,
                 "lastName", Color.black, value);
    }
    {
        string value = object.address.to!string;
        writefln(`  <%1$s color="%2$s">%3$s</%1$s>`,
                 "address", Color.red, value);
    }
}
D

完全なプログラムにはさらに説明がある:

import std.stdio;
import std.string;
import std.algorithm;
import std.conv;
import std.traits;

/* 割り当てられたシンボルを暗号化することを指定する。
 * */
struct Encrypted {
}

enum Color { black, blue, red }

/* 割り当てられたシンボルの色を指定する。
 * デフォルトの色はColor.blackだ。 */
struct Colored {
    Color color;
}

struct Person {
    /* このメンバーは、暗号化されて青色で表示されるように指定されている。
     * */
    @Encrypted @Colored(Color.blue) string name;

    /* このメンバーには、ユーザー定義の属性はない。
     * */
    string lastName;

    /* このメンバーは、赤色で表示されるように指定されている。 */
    @Colored(Color.red) string address;
}

/* 指定したメンバーにColored属性がある場合はその値を返し、
 * ない場合はColor.blackを返す。 */
Color colorAttributeOf(T, string memberName)() {
    auto result = Color.black;

    foreach (attr;
             __traits(getAttributes,
                      __traits(getMember, T, memberName))) {
        static if (is (typeof(attr) == Colored)) {
            result = attr.color;
        }
    }

    return result;
}

/* 指定された文字列のシーザー暗号化されたバージョンを返す。
 * (注意: シーザー暗号は、非常に弱い暗号化方法だ。)
 * */
auto encrypted(string value) {
    return value.map!(a => dchar(a + 1));
}

unittest {
    assert("abcdefghij".encrypted.equal("bcdefghijk"));
}

/* 指定されたオブジェクトを、そのメンバーの属性に従って
 * XML形式で表示する。 */
void printAsXML(T)(T object) {
    writefln("<%s>", T.stringof);
    scope(exit) writefln("</%s>", T.stringof);

    foreach (member; __traits(allMembers, T)) {
        string value =
            __traits(getMember, object, member).to!string;

        static if (hasUDA!(__traits(getMember, T, member),
                           Encrypted)) {
            value = value.encrypted.to!string;
        }

        writefln(`  <%1$s color="%2$s">%3$s</%1$s>`,
                 member, colorAttributeOf!(T, member), value);
    }
}

void main() {
    auto people = [ Person("Alice", "Davignon", "Avignon"),
                    Person("Ben", "de Bordeaux", "Bordeaux") ];

    foreach (person; people) {
        printAsXML(person);
    }
}

プログラムの出力は、メンバーが正しい色を持ち、nameメンバーが暗号化されていることを示している:

<Person>
  <name color="blue">Bmjdf</name>                ← 青色で暗号化済み
  <lastName color="black">Davignon</lastName>
  <address color="red">Avignon</address>         ← 赤
</Person>
<Person>
  <name color="blue">Cfo</name>                  ← 青色で暗号化済み
  <lastName color="black">de Bordeaux</lastName>
  <address color="red">Bordeaux</address>        ← 赤
</Person>
ユーザー定義属性の利点

ユーザー定義属性の利点は、プログラムの他の部分を変更することなく、宣言の属性を変更できることだ。例えば、以下の簡単な変更により、PersonのすべてのメンバーをXML出力で暗号化することができる。

struct Person {
    @Encrypted {
        string name;
        string lastName;
        string address;
    }
}

// ...

    printAsXML(Person("Cindy", "de Cannes", "Cannes"));
D

出力:

<Person>
  <name color="black">Djoez</name>              ← 暗号化済み
  <lastName color="black">ef!Dbooft</lastName>  ← 暗号化済み
  <address color="black">Dbooft</address>       ← 暗号化済み
</Person>

さらに、printAsXML()およびそれが考慮する属性は、他の型でも使用することができる。

struct Data {
    @Colored(Color.blue) string message;
}

// ...

    printAsXML(Data("hello world"));
D

出力:

<Data>
  <message color="blue">hello world</message>    ← 青
</Data>
要約