C# では、自作型に対して変換演算子(conversion operator)を定義することで、キャスト構文 (TargetType)value や暗黙的な代入で型を変換できるようになります。
この記事では Rectangle と Square の 2 つの構造体を例に、次の 4 つを順に説明します。
explicitとimplicitの違い- カスタム変換ルーチンの作成
explicit変換の追加例(int との相互変換)implicit変換の定義
explicit と implicit の違い
変換演算子には explicit(明示的)と implicit(暗黙的)の 2 種類があります。
| キーワード | 呼び出し方 | 特徴 |
|---|---|---|
explicit |
キャスト構文 (型)値 が必須 |
情報の欠落や精度の低下が起こりうる変換に使う |
implicit |
キャストなしで代入できる | 安全な変換(情報が失われない)に使う |
// explicit の場合: キャストが必要
Square sq = (Square)rect;
// implicit の場合: キャスト不要
Rectangle rect = sq;
explicit は「コンパイラに対して『この変換は意図的だ』と明示する」もので、精度の低下やデータの欠落が想定されるときに使います。implicit は「自然に変換できる」ときに使いますが、暗黙的に変換されるため、気づかないうちに変換が走ることに注意が必要です。
カスタム変換ルーチンの作成
まず Rectangle(長方形)と Square(正方形)の 2 つの構造体を定義します。正方形は縦横が等しいため、長方形から正方形に変換するときは高さだけを使います。
public struct Rectangle
{
public int Width { get; set; }
public int Height { get; set; }
public override string ToString() => $"Rectangle [Width={Width}, Height={Height}]";
}
public struct Square
{
public int Length { get; set; }
// Rectangle → Square の明示的変換
public static explicit operator Square(Rectangle r)
{
Square s = new Square { Length = r.Height };
return s;
}
public override string ToString() => $"Square [Length={Length}]";
}
explicit にしているのは、長方形を正方形に変換する際に幅(Width)の情報が失われるからです。変換後の正方形は高さ(Height)のみを辺の長さとして使うため、「情報の欠落が伴う変換」として明示的なキャストを要求しています。
var rect = new Rectangle { Width = 20, Height = 10 };
// キャストが必要(explicit のため)
Square sq = (Square)rect;
Console.WriteLine(rect); // Rectangle [Width=20, Height=10]
Console.WriteLine(sq); // Square [Length=10]
キャストを書かずに代入しようとするとコンパイルエラーになります。
Square sq = rect; // コンパイルエラー: 明示的なキャストが必要
explicit 変換の追加
正方形と int の間でも変換できると便利です。たとえば Square sq = (Square)90; のように整数から正方形を作ったり、int side = (int)sq; のように辺の長さを整数として取り出したりできるようにします。
public struct Square
{
public int Length { get; set; }
// Rectangle → Square の変換(前節と同じ)
public static explicit operator Square(Rectangle r)
{
Square s = new Square { Length = r.Height };
return s;
}
// int → Square の明示的変換
public static explicit operator Square(int sideLength)
{
Square newSq = new Square { Length = sideLength };
return newSq;
}
// Square → int の明示的変換
public static explicit operator int(Square s) => s.Length;
public override string ToString() => $"Square [Length={Length}]";
}
// int → Square
Square sq2 = (Square)90;
Console.WriteLine(sq2); // Square [Length=90]
// Square → int
int side = (int)sq2;
Console.WriteLine(side); // 90
int → Square を explicit にしているのは、「整数をそのまま正方形の辺の長さとして扱う」という意図的な変換であることをコード上で示したいためです。暗黙的に変換されると、Square s = someInt; のような代入が意図せず成立してしまい、読みづらくなります。
implicit 変換の定義
逆に、正方形を長方形として扱う変換は「情報の欠落がない」ため implicit で定義できます。正方形は縦横が等しいので、長方形の Width と Height を両方埋めることができます。今回は幅を辺の長さの 2 倍にする仕様とします。
public struct Rectangle
{
public int Width { get; set; }
public int Height { get; set; }
// Square → Rectangle の暗黙的変換
public static implicit operator Rectangle(Square s)
{
Rectangle r = new Rectangle
{
Height = s.Length,
Width = s.Length * 2 // 幅は辺の長さの 2 倍とする仕様
};
return r;
}
public override string ToString() => $"Rectangle [Width={Width}, Height={Height}]";
}
implicit なので、キャストを書かずにそのまま代入できます。
Square s3 = new Square { Length = 83 };
// キャスト不要(implicit のため)
Rectangle rect2 = s3;
Console.WriteLine(s3); // Square [Length=83]
Console.WriteLine(rect2); // Rectangle [Width=166, Height=83]
もちろん、明示的にキャストして書くことも問題なく動作します。
Rectangle rect3 = (Rectangle)s3; // これも動く
構造体をまとめると
ここまでの定義をまとめた完全なコードです。
public struct Rectangle
{
public int Width { get; set; }
public int Height { get; set; }
// Square → Rectangle(implicit)
public static implicit operator Rectangle(Square s)
{
return new Rectangle
{
Height = s.Length,
Width = s.Length * 2
};
}
public override string ToString() => $"Rectangle [Width={Width}, Height={Height}]";
}
public struct Square
{
public int Length { get; set; }
// Rectangle → Square(explicit: Width の情報が失われる)
public static explicit operator Square(Rectangle r)
{
return new Square { Length = r.Height };
}
// int → Square(explicit: 意図的な変換であることを明示)
public static explicit operator Square(int sideLength)
{
return new Square { Length = sideLength };
}
// Square → int(explicit)
public static explicit operator int(Square s) => s.Length;
public override string ToString() => $"Square [Length={Length}]";
}
// Rectangle → Square(explicit)
var rect = new Rectangle { Width = 20, Height = 10 };
Square sq1 = (Square)rect;
Console.WriteLine(sq1); // Square [Length=10]
// int → Square(explicit)
Square sq2 = (Square)90;
Console.WriteLine(sq2); // Square [Length=90]
// Square → int(explicit)
int side = (int)sq2;
Console.WriteLine(side); // 90
// Square → Rectangle(implicit)
Square s3 = new Square { Length = 83 };
Rectangle rect2 = s3;
Console.WriteLine(rect2); // Rectangle [Width=166, Height=83]
まとめ
| 変換 | キーワード | 理由 |
|---|---|---|
Rectangle → Square |
explicit |
Width の情報が失われる |
int → Square |
explicit |
意図的であることを明示したい |
Square → int |
explicit |
意図的であることを明示したい |
Square → Rectangle |
implicit |
情報の欠落がなく自然な変換 |
変換演算子の使い分けの基本は「情報が失われる可能性があれば explicit、安全に変換できるなら implicit」です。implicit は書く側には便利ですが、読む側が変換に気づきにくいため、本当に自然な変換にだけ使うのが安全です。