型変換

変数は、それらが参加する式と互換性がある必要がある。これまで見てきたプログラムから明らかなように、Dは静的型付け言語である。つまり、型の互換性はコンパイル時に検証される。

これまでに記述した式は、すべて型が互換性があった。そうでないと、コンパイラによってコードが拒否されてしまうからだ。以下は、型が互換性のないコードの例だ。

char[] slice;
writeln(slice + 5);    // ← コンパイルエラー
D

加算演算において、char[]intの型が互換性がないため、コンパイラはコードを拒否する。

エラー: ((slice) + (5))の型が一致しない: 'char[]'と'int'

型の非互換性は、型が異なることを意味するものではない。異なる型は、式内で安全に使用することができる。例えば、int変数は、double値の代わりに使用することができる。

double sum = 1.25;
int increment = 3;
sum += increment;
D

sumincrementは異なる型だが、double変数をint値で加算することは合法であるため、上記のコードは有効である。

自動型変換

自動型変換は、暗黙の型変換とも呼ばれる。

上記の式では、doubleintは互換性のある型だが、加算演算はマイクロプロセッサレベルでは特定の型として評価される必要がある。浮動小数点型の章で覚えているとおり、64ビット型doubleは32ビット型intよりも幅が広い(大きい)。さらに、intに収まる値は、doubleにも収まる。

コンパイラは、型が一致しない式を検出すると、まず式の各部分を共通の型に変換してから、式全体を評価する。コンパイラによって実行される自動変換は、データの損失がない方向に行われる。例えば、doubleは、intが保持できる値をすべて保持できるが、その逆は当てはまらない。上記の+=演算は、intの値はすべてdoubleに安全に変換できるため、正常に動作する。

変換の結果として自動的に生成された値は、常に匿名(そして多くの場合一時的な)変数である。元の値は変更されない。例えば、上記の+=での自動変換では、incrementの型は変更されない。これは常にintである。むしろ、double型の値incrementを使用して、一時的な値が構築される。バックグラウンドで実行される変換は、次のコードと同等である。

{
    double an_anonymous_double_value = increment;
    sum += an_anonymous_double_value;
}
D

コンパイラは、intの値を一時的なdoubleの値に変換し、その値を操作で使用する。この例では、一時変数は+=操作の間だけ存在する。

自動変換は、算術演算だけに限定されない。型が自動的に他の型に変換されるケースは他にもある。変換が有効である限り、コンパイラは型変換を利用して、式で値を使用できるようにする。例えば、intパラメータにbyte値を渡すことができる。

void func(int number) {
    // ...
}

void main() {
    byte smallValue = 7;
    func(smallValue);    // 自動型変換
}

上記のコードでは、まず一時的なint値が構築され、その値で関数が呼び出される。

整数の型変換

次の表の左側にある型の値は、その実際の型として算術式に参加することはない。各型は、まず表の右側にある型に昇格される。

変換元変換先
boolint
byteint
ubyteint
shortint
ushortint
charint
wcharint
dcharuint

整数昇格は、enum値にも適用される。

整数昇格が行われる理由は、歴史的な理由(この規則がCから来ている)と、マイクロプロセッサの自然な算術型がintであるという事実の両方がある。例えば、次の2つの変数はどちらもubyteだが、加算演算は、両方の値が個別にintに昇格された後にのみ実行される。

ubyte a = 1;
ubyte b = 2;
writeln(typeof(a + b).stringof);  // 追加はubyteではない
D

出力:

int

変数abの型は変化しないことに注意しよう。加算演算の実行中、その値だけが一時的にintに昇格されるだけだ。

算術変換

算術演算には、他にも変換規則が適用される。一般に、自動算術変換は、より狭い型からより広い型へと、安全な方向に適用される。この規則は覚えやすく、ほとんどの場合に正しいが、自動変換規則は非常に複雑であり、符号付きから符号なしへの変換の場合、バグのリスクを伴う。

算術変換のルールは次の通りだ。

  1. 値の1つがrealの場合、もう1つの値はreal
  2. それ以外の場合、いずれかの値がdoubleの場合、もう一方の値はdouble
  3. それ以外の場合、いずれかの値がfloatの場合、もう一方の値はfloat
  4. それ以外の場合、上記の表に従って整数変換が適用され、その後、以下の規則が適用される:
    1. 両方の型が同じ場合は、それ以上の手順は必要ない。
    2. 両方の型が符号付き、または両方の型が符号なしの場合、より狭い値がより広い型に変換される
    3. 符号付き型が符号なし型よりも広い場合、符号なし値は符号付き型に変換される
    4. それ以外の場合は、符号付き型が符号なし型に変換される

残念ながら、上記の最後のルールは subtle bugs を引き起こす可能性がある:

int    a = 0;
int    b = 1;
size_t c = 0;
writeln(a - b + c);  // 驚くべき結果!
D

意外にも、出力は-1ではなくsize_t.maxとなる:

18446744073709551615

(0 - 1 + 0)は-1と計算されると思うかもしれないが、上記の規則によると、式全体の型はintではなくsize_tであり、size_tは負の値を保持できないため、結果はオーバーフローしてsize_t.maxになる。

スライス変換

便宜上、固定長配列は関数を呼び出すときに自動的にスライスに変換することができる。

import std.stdio;

void foo() {
    int[2] array = [ 1, 2 ];
    bar(array);    // 固定長配列をスライスとして渡す
}

void bar(int[] slice) {
    writeln(slice);
}

void main() {
    foo();
}

bar() 固定長配列のすべての要素のスライスを受け取り、それを出力する:

[1, 2]

警告:関数が後で使用するためにスライスを格納する場合、ローカル固定長配列をスライスとして渡してはいけない。例えば、次のプログラムには、foo()が終了すると、bar()が格納するスライスが無効になるため、バグがある。

import std.stdio;

void foo() {
    int[2] array = [ 1, 2 ];
    bar(array);    // 固定長配列をスライスとして渡す

}  // ← 注釈: この時点以降、'array'は有効ではない

int[] sliceForLaterUse;

void bar(int[] slice) {
    // 無効になるスライスを保存する
    sliceForLaterUse = slice;
    writefln("Inside bar : %s", sliceForLaterUse);
}

void main() {
    foo();

    /* バグ: 配列要素ではなくなったメモリにアクセスする */
    writefln("Inside main: %s", sliceForLaterUse);
}

このようなバグの結果は、未定義の動作になる。実行例では、arrayの要素だったメモリが、すでに他の目的のために再利用されていることがわかる。

bar内部 : [1, 2]        ← 実際の要素
main内部: [4396640, 0]  ← 未定義動作の現れ
const変換

関数パラメータの章で見たように、参照型は同じ型のconstに自動的に変換される。constへの変換は、型の幅が変わらず、constは変数を変更しないという約束があるため、安全だ。

char[] parenthesized(const char[] text) {
    return "{" ~ text ~ "}";
}

void main() {
    char[] greeting;
    greeting ~= "hello world";
    parenthesized(greeting);
}

上記の変更可能なgreetingは、parenthesized()に渡されると、自動的にconst char[]に変換される。

前述のように、逆の変換は自動的に行われない。const参照は、自動的に変更可能な参照に変換されない。

char[] parenthesized(const char[] text) {
    char[] argument = text;  // ← コンパイルエラー
// ...
}
D

このトピックは参照についてのみ説明していることに注意。値型の変数はコピーされるから、コピーを通じて元の変数に影響を与えることはどうやってもできない。

const int totalCorners = 4;
int theCopy = totalCorners;      // コンパイルできる(値型)
D

上記のconstから変更可能への変換は、コピーは元の参照ではないため、合法である。

immutable変換

immutableは変数が変更できないことを指定しているため、immutableからimmutableへの変換も、 から への変換も自動的には行われない:

string a = "hello";    // 不変文字
char[] b = a;          // ← コンパイルエラー
string c = b;          // ← コンパイルエラー
D

上記のconstの変換と同様、このトピックも参照型についてのみ扱う。値型変数はとにかくコピーされるため、immutableへの変換および からの変換は有効だ。

immutable a = 10;
int b = a;           // コンパイルできる(値型)
D
enum変換

列挙型の章で見たように、enumは名前付き定数を定義するために使用される:

enum Suit { spades, hearts, diamonds, clubs }
D

上記では値が明示的に指定されていないため、enumメンバーの値は0から始まり、自動的に1ずつ加算されることに注意。したがって、Suit.clubsの値は3になる。

enum値は自動的に整数型に変換される。例えば、次の計算では、Suit.heartsの値は1とみなされ、結果は11になる。

int result = 10 + Suit.hearts;
assert(result == 11);
D

逆の変換は自動的に行われない。整数値は、対応するenum値に自動的に変換されない。例えば、以下のsuit変数はSuit.diamondsになることが予想されるが、このコードはコンパイルできない。

Suit suit = 2;    // ← コンパイルエラー
D

後で説明するように、整数からenum値への変換は可能だが、明示的に指定する必要がある。

bool変換

boolは論理式の自然な型だが、値は2つしかないので、1ビットの整数と見なすことができ、場合によっては整数のように振る舞う。falsetrueは、それぞれ0と1に自動的に変換される。

int a = false;
assert(a == 0);

int b = true;
assert(b == 1);
D

リテラル値に関しては、2つの特別なリテラル値についてのみ、逆の変換が自動的に行われる。0と1は、それぞれfalsetrueに自動的に変換される。

bool a = 0;
assert(!a);     // false

bool b = 1;
assert(b);      // true
D

その他のリテラル値は、boolに自動的に変換することはできない。

bool b = 2;    // ← コンパイルエラー
D

一部の文では、論理式を使用する。ifwhileなどだ。このような文の論理式では、boolだけでなく、他のほとんどの型も使用できる。値0は自動的にfalseに変換され、0以外の値は自動的にtrueに変換される。

int i;
// ...

if (i) {    // ← int値は論理式として使用されている
    // ... 'i'は0ではない

} else {
    // ... 'i'は0である
}
D

同様に、null参照は自動的にfalseに変換され、非null参照は自動的にtrueに変換される。これにより、実際に使用する前に参照が非nullであることを簡単に確認できる。

int[] a;
// ...

if (a) {    // ← 自動bool変換
    // ... nullではない; 'a'を使用できる ...

} else {
    // ... null; 'a'を使用できない ...
}
D
明示的な型変換

上記で見たように、自動変換が利用できないケースがある:

このような変換が安全であることがわかっている場合、プログラマは次のいずれかの方法で型変換を明示的に要求することができる。

コンストラクタ構文

structおよびclassの構文は、他の型でも使用できる。

DestinationType(value)
D

例えば、次の変換は、おそらく除算演算の小数部分を保持するために、int値からdouble値を作成する。

int i;
// ...
const result = double(i) / 2;
D
to() ほとんどの変換の場合

これまで主に値をstringに変換するために使用してきたto()関数は、実際には他の多くの型にも使用できる。その完全な構文は次の通りだ。

to!(DestinationType)(value)
D

テンプレートであるto()は、ショートカットテンプレートパラメータ表記を利用できる。 変換先の型が1つのトークン (通常は1 単語) だけで構成されている場合は、最初の括弧を省略して呼び出すことができる。

to!DestinationType(value)
D

次のプログラムは、doubleの値をshortに、stringの値をintに変換しようとしている。

void main() {
    double d = -1.75;

    short s = d;     // ← コンパイルエラー
    int i = "42";    // ← コンパイルエラー
}

すべてのdouble値がshortとして表現できるわけではなく、また、すべてのstringintとして表現できるわけではないため、これらの変換は自動的に行われない。変換が実際に安全である、あるいは潜在的な影響が許容範囲内であることがプログラマにわかっている場合は、to()を使用して型を変換することができる。

import std.conv;

void main() {
    double d = -1.75;

    short s = to!short(d);
    assert(s == -1);

    int i = to!int("42");
    assert(i == 42);
}

shortは小数値を保持できないため、変換された値は-1になることに注意。

to()は安全である。変換が不可能な場合は例外をスローする。

assumeUnique() 高速なimmutable変換のため

to() immutableの変換も実行可能:

int[] slice = [ 10, 20, 30 ];
auto immutableSlice = to!(immutable int[])(slice);
D

immutableSliceの要素が決して変更されないことを保証するために、sliceと同じ要素を共有することはできない。そのため、to()は、上記のimmutable要素を含む追加のスライスを作成する。そうしないと、sliceの要素を変更すると、immutableSliceの要素も変更されてしまう。この動作は、配列の.idupプロパティと同じだ。

immutableSliceの要素がsliceの要素のコピーであることは、それぞれの最初の要素のアドレスを確認することで確認できる。

assert(&(slice[0]) != &(immutableSlice[0]));
D

このコピーは不要な場合もあり、場合によってはプログラムの速度が著しく低下する可能性がある。この例として、immutableスライスを受け取る次の関数を見てみよう。

void calculate(immutable int[] coordinates) {
    // ...
}

void main() {
    int[] numbers;
    numbers ~= 10;
    // ... その他のさまざまな変更 ...
    numbers[0] = 42;

    calculate(numbers);    // ← コンパイルエラー
}

上記のプログラムは、呼び出し元がcalculate()immutable引数を渡していないため、コンパイルできない。先ほど見たように、immutableスライスをto()で作成できる:

import std.conv;
// ...
    auto immutableNumbers = to!(immutable int[])(numbers);
    calculate(immutableNumbers);    // ← 現在コンパイルできる
D

しかし、numbersはこの引数を生成するためにのみ必要であり、関数の呼び出し後は決して使用されない場合、その要素をimmutableNumbersにコピーする必要はない。assumeUnique()は、スライスimmutableの要素をコピーせずに、スライス を作成する。

import std.exception;
// ...
    auto immutableNumbers = assumeUnique(numbers);
    calculate(immutableNumbers);
    assert(numbers is null);    // 元のスライスがnullになる
D

assumeUnique()既存の要素へのアクセスを提供する新しいスライスを返す。また、元のimmutableスライス をnullに変更し、誤って変更されるのを防ぐ。

cast演算子

to()assumeUnique()は、プログラマーが使用できる変換演算子castを利用している。

cast演算子は、括弧で囲まれた宛先型を受け取る。

cast(DestinationType)value
D

castは、to()では安全に実行できない変換に対しても強力な機能だ。例えば、to()は、実行時に次の変換で失敗する。

Suit suit = to!Suit(7);    // ← 例外をスローする
bool b = to!bool(2);       // ← 例外をスローする
D
std.conv.ConvException@phobos/std/conv.d(1778): Value (7)は、
列挙型Suit'のメンバー値と一致しない
Undefined

整数値が有効なenum値に対応しているかどうか、あるいは整数値をboolとして扱うことが妥当であるかどうかは、プログラマーしか判断できない場合がある。cast演算子は、プログラムのロジックに従って変換が正しいことが分かっている場合に使用できる。

// おそらく間違っているが、可能性はある:
Suit suit = cast(Suit)7;

bool b = cast(bool)2;
assert(b);
D

castポインタ型との変換では、これが唯一の選択肢である。

void * v;
// ...
int * p = cast(int*)v;
D

まれだが、一部のCライブラリインターフェイスでは、ポインタ値をポインタ型以外の型として格納する必要がある。変換によって実際の値が保持されることが保証されている場合は、castを使用してポインタ型とポインタ型以外の型の間で変換することもできる。

size_t savedPointerValue = cast(size_t)p;
// ...
int * p2 = cast(int*)savedPointerValue;
D
要約