CodeGym /コース /C# SELF /C#におけるオペレーターのオーバーロード

C#におけるオペレーターのオーバーロード

C# SELF
レベル 34 , レッスン 2
使用可能

1. はじめに

みんな、+ で数字を足したり、== で比較したりって、よく使うよね。こういうオペレーターは組み込み型ではバッチリ動く。でも、もし自分で Vector(ベクトル)みたいなクラスを作って、ベクトル同士の足し算を vector1 + vector2 みたいに自然に書きたい時はどうする?あるいは Money(お金)オブジェクト同士の比較を moneyAmount1 == moneyAmount2 みたいにしたい時は?こういう時のためにC#にはオペレーターのオーバーロードがあるんだ。

オペレーターのオーバーロードを使うと、標準のオペレーター(+, -, *, /, ==, !=, >, <, ++, -- などなど)が自分のクラスや構造体のインスタンスでどう動くかを自分で決められる。これでコードがもっと直感的で読みやすく、表現力もアップするし、自作型でもおなじみの構文が使えるようになるよ。

オペレーターオーバーロードの基本

オペレーターをオーバーロードするには、クラスや構造体の中で operator キーワードの後にオーバーロードしたいオペレーター記号を付けて、staticメソッドを宣言するんだ。

  • staticメソッド: オペレーターのオーバーロードメソッドは必ず public static じゃないとダメ。
  • メソッド名: メソッド名は operator キーワードの後にオペレーター記号(例: operator +, operator ==)を付ける。
  • パラメータ: パラメータの数はオペレーターの種類(単項か二項か)による。
    • 単項オペレーター+, -, !, ++, -- など)は1つだけ、そのクラス/構造体型のパラメータを取る。
    • 二項オペレーター+, -, *, /, ==, != など)は2つパラメータを取って、少なくとも1つはそのクラス/構造体型じゃないとダメ。
  • 戻り値の型: オペレーションが返す型。

2. 二項オペレーターのオーバーロード (Binary Operators)

二項オペレーターは2つのオペランドを取る。よく使うのは算術オペレーター(+, -, *, /)や比較オペレーター(==, !=, >, <, >=, <=)だね。

例: 足し算オペレーター (+) のオーバーロード

public struct Point
{
    public int X { get; set; }
    public int Y { get; set; }

    public Point(int x, int y) => (X, Y) = (x, y);

    // + オペレーターのオーバーロード
    public static Point operator +(Point p1, Point p2)
    {
        return new Point(p1.X + p2.X, p1.Y + p2.Y);
    }

    public override string ToString() => $"({X}, {Y})";
}

// 使い方:
Point point1 = new Point(1, 2);
Point point2 = new Point(3, 4);
Point sumPoint = point1 + point2; // ここでオーバーロードした + が呼ばれる

Console.WriteLine($"点の合計: {sumPoint}"); // 出力: 点の合計: (4, 6)

ここでは、Point オブジェクト同士の足し算が、それぞれの座標を足した新しい Point を返すようにしてるよ。

例: スカラー(数値)との掛け算オペレーター (*) のオーバーロード

public struct Vector
{
    public double X { get; set; }
    public double Y { get; set; }

    public Vector(double x, double y) => (X, Y) = (x, y);

    // * オペレーターのオーバーロード(ベクトル * 数値)
    public static Vector operator *(Vector vec, double scalar)
    {
        return new Vector(vec.X * scalar, vec.Y * scalar);
    }

    // * オペレーターのオーバーロード(数値 * ベクトル) - 対称性のため
    public static Vector operator *(double scalar, Vector vec)
    {
        return new Vector(vec.X * scalar, vec.Y * scalar);
    }

    public override string ToString() => $"<{X}, {Y}>";
}

// 使い方:
Vector vec1 = new Vector(2, 3);
Vector scaledVec1 = vec1 * 5;      // Vector * double が呼ばれる
Vector scaledVec2 = 5 * vec1;      // double * Vector が呼ばれる

Console.WriteLine($"スケーリングしたベクトル1: {scaledVec1}"); // 出力: スケーリングしたベクトル1: <10, 15>
Console.WriteLine($"スケーリングしたベクトル2: {scaledVec2}"); // 出力: スケーリングしたベクトル2: <10, 15>

掛け算オペレーターは両方の順番で使えるように2回オーバーロードしてるよ:Vector * doubledouble * Vector

例: 比較オペレーター (==!=) のオーバーロード

==!= をオーバーロードする時は重要なルールがある:どっちかをオーバーロードしたらもう一方も必ずオーバーロードしないとダメ。それに、このオペレーターの動きは Equals()GetHashCode() の動きと合わせるのが超大事。

public class Money
{
    public decimal Amount { get; set; }
    public string Currency { get; set; }

    public Money(decimal amount, string currency)
    {
        Amount = amount;
        Currency = currency;
    }

    // == / != をオーバーロードするなら Equals と GetHashCode も必ずオーバーライド
    public override bool Equals(object? obj)
    {
        if (obj is not Money other) return false;
        return Amount == other.Amount && Currency == other.Currency;
    }

    public override int GetHashCode() => HashCode.Combine(Amount, Currency);

    // == オペレーターのオーバーロード
    public static bool operator ==(Money? m1, Money? m2)
    {
        // 参照型のnullチェック
        if (ReferenceEquals(m1, null)) return ReferenceEquals(m2, null);
        return m1.Equals(m2); // オーバーライドした Equals を使う
    }

    // != オペレーターのオーバーロード(== をオーバーロードしたら必須)
    public static bool operator !=(Money? m1, Money? m2)
    {
        return !(m1 == m2);
    }
}

// 使い方:
Money cash1 = new Money(100, "USD");
Money cash2 = new Money(100, "USD");
Money cash3 = new Money(50, "USD");
Money cash4 = new Money(100, "EUR");

Console.WriteLine($"cash1 == cash2: {cash1 == cash2}"); // True
Console.WriteLine($"cash1 == cash3: {cash1 == cash3}"); // False
Console.WriteLine($"cash1 == cash4: {cash1 == cash4}"); // False

値型(struct)の場合、==!= はデフォルトでビットごとの比較をするから普通はOK。でもクラス(class)だとデフォルトの == は参照比較になるから、値比較したい時はオーバーロードが便利だよ。

3. 単項オペレーターのオーバーロード (Unary Operators)

単項オペレーターは1つのオペランドで動く。例:+(単項プラス)、-(単項マイナス)、!(論理NOT)、~(ビット反転)、++(インクリメント)、--(デクリメント)。

例: 単項マイナス (-) のオーバーロード

public struct Vector3D
{
    public double X, Y, Z;

    public Vector3D(double x, double y, double z) => (X, Y, Z) = (x, y, z);

    // 単項 - オペレーターのオーバーロード
    public static Vector3D operator -(Vector3D vec)
    {
        return new Vector3D(-vec.X, -vec.Y, -vec.Z);
    }

    public override string ToString() => $"<{X}, {Y}, {Z}>";
}

// 使い方:
Vector3D originalVec = new Vector3D(1, -2, 3);
Vector3D invertedVec = -originalVec; // オーバーロードした単項 - が呼ばれる

Console.WriteLine($"元のベクトル: {originalVec}"); // 出力: 元のベクトル: <1, -2, 3>
Console.WriteLine($"反転ベクトル: {invertedVec}"); // 出力: 反転ベクトル: <-1, 2, -3>

例: インクリメント (++) とデクリメント (--) オペレーターのオーバーロード

これらのオペレーターはオペランドを変更して、変更後の値を返すよ。

public struct Counter
{
    public int Value { get; set; }

    public Counter(int value) => Value = value;

    // ++ オペレーターのオーバーロード
    public static Counter operator ++(Counter c)
    {
        // 注意: 構造体がイミュータブルなら新しいオブジェクトを返す
        // クラスなら自分自身を変更して返してもOK
        return new Counter(c.Value + 1);
    }

    // -- オペレーターのオーバーロード
    public static Counter operator --(Counter c)
    {
        return new Counter(c.Value - 1);
    }

    public override string ToString() => $"[カウンター: {Value}]";
}

// 使い方:
Counter myCounter = new Counter(5);
myCounter++; // 後置インクリメント
Console.WriteLine(myCounter); // 出力: [カウンター: 6]

++myCounter; // 前置インクリメント
Console.WriteLine(myCounter); // 出力: [カウンター: 7]

myCounter--;
Console.WriteLine(myCounter); // 出力: [カウンター: 6]

オーバーロードした ++-- は常に前置(先に変更してから返す)として動く。後置の動きはコンパイラが自動でやってくれるよ。

4. 型変換オペレーターのオーバーロード

自分の型のインスタンスを他の型に明示的または暗黙的に変換する方法も定義できるよ。

  • implicit(暗黙の変換): 変換が常に安全でデータ損失がない場合に使う(例: int から long)。
  • explicit(明示的な変換): データ損失やエラーの可能性がある場合に使う。キャスト((Type)obj)が必要。

例: Score から int への暗黙変換

public struct Score
{
    public int Points { get; set; }
    public Score(int points) => Points = points;

    // Score から int への暗黙変換
    public static implicit operator int(Score s)
    {
        return s.Points;
    }
}

// 使い方:
Score examScore = new Score(95);
int scoreValue = examScore; // 暗黙変換
Console.WriteLine($"得点: {scoreValue}"); // 出力: 得点: 95

例: Celsius から Fahrenheit への明示的変換

public struct Celsius
{
    public double Degrees { get; set; }
    public Celsius(double degrees) => Degrees = degrees;
}

public struct Fahrenheit
{
    public double Degrees { get; set; }
    public Fahrenheit(double degrees) => Degrees = degrees;

    // Fahrenheit から Celsius への明示的変換
    public static explicit operator Celsius(Fahrenheit f)
    {
        return new Celsius((f.Degrees - 32) * 5 / 9);
    }

    // Celsius から Fahrenheit への明示的変換
    public static explicit operator Fahrenheit(Celsius c)
    {
        return new Fahrenheit(c.Degrees * 9 / 5 + 32);
    }
}

// 使い方:
Celsius c = new Celsius(25);
Fahrenheit f = (Fahrenheit)c; // 明示的変換
Console.WriteLine($"25°C = {f.Degrees}°F"); // 出力: 25°C = 77°F

Fahrenheit f2 = new Fahrenheit(212);
Celsius c2 = (Celsius)f2; // 明示的変換
Console.WriteLine($"212°F = {c2.Degrees}°C"); // 出力: 212°F = 100°C

5. 制限とおすすめ

すべてのオペレーターがオーバーロードできるわけじゃない: &&, ||, ?., new, typeof, is, as, ==string 用はすでにオーバーロード済みで特別な動きがある), ()(メソッド呼び出し), =(代入)などはオーバーロードできないよ。

対称性: 二項オペレーターをオーバーロードする時は、逆順のオペランドでも意味があるなら両方オーバーロードすると便利(Vector * double の例みたいに)。

契約: 特に比較オペレーター(==, !=, >, <, >=, <=)と Equals(), GetHashCode() の関係は必ず守ろう。契約を破るとコレクションで予期しないバグが出るよ。

読みやすさ・直感性: オペレーターのオーバーロードは、コードが直感的で読みやすくなる時だけにしよう。動きが分かりにくい時や誤解を招きそうな時は、Add() みたいな普通のメソッド名を使った方がいい。

ミュータブル(変更可能)なオブジェクト: 変更可能な型でオペレーターをオーバーロードする時は注意。例えば Vector v1 = v2 + v3; は普通は新しいオブジェクトを作るけど、もし既存オブジェクトを変更しちゃうと分かりにくくなる。

構造体 vs クラス: オペレーターのオーバーロードは、値を表すことが多い構造体(例: Point, ComplexNumber など)でよく使われる。こういう型は数学的な演算や値の比較が自然だからね。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION