C# では、+ や - などの演算子を自作型に対して定義できます。これを operator overloading と呼びます。
たとえばベクトル、金額、日付、版番号など、演算の意味が自然に決まる型では、Add(a, b) のようなメソッドよりも a + b のほうが読みやすくなります。
この記事では次の 5 つを順に説明します。
- 二項演算子のオーバーロード
- += と -= 演算子はどうなる?
- 単項演算子のオーバーロード
- 等価演算子のオーバーロード
- 比較演算子のオーバーロード
1. 二項演算子のオーバーロード
二項演算子は、左辺と右辺の 2 つの値を受け取る演算子です。C# では +、-、*、/、%、&、|、^ などをオーバーロードできます。
ここでは 2 次元ベクトル Vector2 を例に、+ と - を定義します。
public readonly struct Vector2
{
public double X { get; }
public double Y { get; }
public Vector2(double x, double y)
{
X = x;
Y = y;
}
public static Vector2 operator +(Vector2 left, Vector2 right)
=> new(left.X + right.X, left.Y + right.Y);
public static Vector2 operator -(Vector2 left, Vector2 right)
=> new(left.X - right.X, left.Y - right.Y);
public override string ToString() => $"({X}, {Y})";
}
var a = new Vector2(3, 4);
var b = new Vector2(10, 20);
Console.WriteLine(a + b); // (13, 24)
Console.WriteLine(b - a); // (7, 16)
ポイントは、演算子の実装は static メソッドとして書く ことです。operator キーワードの後に演算子記号を置き、戻り値と引数の型を定義します。
2. += と -= 演算子はどうなる?
+= や -= は見た目としては演算子ですが、C# では直接オーバーロードできません。代わりに、対応する二項演算子 + や - を定義しておけば、+= や -= はそれを使って処理されます。
つまり、次のようなコードは:
var value = new Vector2(1, 2);
value += new Vector2(3, 4);
内部的にはおおむね次のように扱われます。
value = value + new Vector2(3, 4);
さきほどの Vector2 に + が定義されていれば、+= も自然に使えます。
var value = new Vector2(1, 2);
value += new Vector2(3, 4);
Console.WriteLine(value); // (4, 6)
+= や -= を個別に実装する必要はありません。まず + と - を正しく設計する のが重要です。
3. 単項演算子のオーバーロード
単項演算子は、1 つの値に対して働く演算子です。C# では +、-、!、~、++、-- などをオーバーロードできます。
ここでは Vector2 に対して単項 + と単項 - を追加します。単項 - は向きを反転するのに自然です。
public readonly struct Vector2
{
public double X { get; }
public double Y { get; }
public Vector2(double x, double y)
{
X = x;
Y = y;
}
public static Vector2 operator +(Vector2 value)
=> value;
public static Vector2 operator -(Vector2 value)
=> new(-value.X, -value.Y);
}
var v = new Vector2(3, -5);
Console.WriteLine(+v); // (3, -5)
Console.WriteLine(-v); // (-3, 5)
++ と -- は、値を 1 つ増減する型で使うことがあります。たとえばカウンター型なら次のように書けます。
public struct Counter
{
public int Value { get; private set; }
public Counter(int value) => Value = value;
public static Counter operator ++(Counter value)
=> new(value.Value + 1);
public static Counter operator --(Counter value)
=> new(value.Value - 1);
public override string ToString() => Value.ToString();
}
var counter = new Counter(10);
counter++;
counter--;
Console.WriteLine(counter); // 10
単項演算子は便利ですが、意味が直感的な型に限定して使う のが安全です。無理に定義すると、コードの意図が読みづらくなります。
4. 等価演算子のオーバーロード
等価演算子 == と != は、オブジェクト同士が同じかどうかを比較します。C# ではこの 2 つをペアで定義するのが基本です。
ここでは金額を表す Money を例にします。通貨と金額が一致するときに等しいとみなします。
using System;
public readonly struct Money : IEquatable<Money>
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency ?? throw new ArgumentNullException(nameof(currency));
}
public bool Equals(Money other)
=> Amount == other.Amount && Currency == other.Currency;
public override bool Equals(object? obj)
=> obj is Money other && Equals(other);
public override int GetHashCode()
=> HashCode.Combine(Amount, Currency);
public static bool operator ==(Money left, Money right)
=> left.Equals(right);
public static bool operator !=(Money left, Money right)
=> !left.Equals(right);
public override string ToString() => $"{Amount} {Currency}";
}
var a = new Money(100m, "JPY");
var b = new Money(100m, "JPY");
var c = new Money(100m, "USD");
Console.WriteLine(a == b); // True
Console.WriteLine(a != c); // True
等価演算子を定義する場合は、Equals と GetHashCode も必ず整合させる 必要があります。== が true なら Equals も true になるように揃えてください。
5. 比較演算子のオーバーロード
比較演算子 >、<、>=、<= は、大小関係を表します。これらもオーバーロードできますが、4 つをセットで定義する のが基本です。
ここでは VersionNumber を例に、メジャー、マイナー、パッチの順で比較できるようにします。
using System;
public readonly struct VersionNumber : IComparable<VersionNumber>, IEquatable<VersionNumber>
{
public int Major { get; }
public int Minor { get; }
public int Patch { get; }
public VersionNumber(int major, int minor, int patch)
{
Major = major;
Minor = minor;
Patch = patch;
}
public int CompareTo(VersionNumber other)
{
var majorCompare = Major.CompareTo(other.Major);
if (majorCompare != 0) return majorCompare;
var minorCompare = Minor.CompareTo(other.Minor);
if (minorCompare != 0) return minorCompare;
return Patch.CompareTo(other.Patch);
}
public bool Equals(VersionNumber other)
=> Major == other.Major && Minor == other.Minor && Patch == other.Patch;
public override bool Equals(object? obj)
=> obj is VersionNumber other && Equals(other);
public override int GetHashCode()
=> HashCode.Combine(Major, Minor, Patch);
public static bool operator ==(VersionNumber left, VersionNumber right)
=> left.Equals(right);
public static bool operator !=(VersionNumber left, VersionNumber right)
=> !left.Equals(right);
public static bool operator <(VersionNumber left, VersionNumber right)
=> left.CompareTo(right) < 0;
public static bool operator >(VersionNumber left, VersionNumber right)
=> left.CompareTo(right) > 0;
public static bool operator <=(VersionNumber left, VersionNumber right)
=> left.CompareTo(right) <= 0;
public static bool operator >=(VersionNumber left, VersionNumber right)
=> left.CompareTo(right) >= 0;
public override string ToString() => $"{Major}.{Minor}.{Patch}";
}
var v1 = new VersionNumber(1, 2, 0);
var v2 = new VersionNumber(1, 3, 0);
var v3 = new VersionNumber(1, 3, 0);
Console.WriteLine(v1 < v2); // True
Console.WriteLine(v2 > v1); // True
Console.WriteLine(v2 == v3); // True
Console.WriteLine(v1 <= v3); // True
比較演算子を定義する型では、IComparable<T> を実装して CompareTo を用意しておくと、List<T>.Sort() などの標準機能とも相性が良くなります。
実装時の注意点
演算子オーバーロードは強力ですが、乱用すると読みやすさを損ないます。特に次の点は意識しておくと安全です。
- 意味が自然な型にだけ使う
==と!=、<と>、<=と>=は整合させる- 等価演算子は
EqualsとGetHashCodeも揃える +=や-=は個別にオーバーロードできないので、対応する二項演算子を実装する
まとめ
C# の operator overloading を使うと、自作型を配列や数値のように自然に扱えます。
| トピック | ポイント |
|---|---|
| 二項演算子のオーバーロード | + や - を static operator で定義する |
+= / -= |
直接は定義せず、+ / - を用意する |
| 単項演算子のオーバーロード | +、-、++、-- などを 1 引数で定義する |
| 等価演算子のオーバーロード | == と != をペアで定義し、Equals も整える |
| 比較演算子のオーバーロード | >, <, >=, <= をセットで定義し、IComparable<T> も実装する |
演算子を使うとコードは短くなりますが、その短さが読みやすさにつながる場合にだけ採用する のがコツです。意味が明快なドメインモデルに絞って使うと、C# の表現力をうまく引き出せます。