C# で演算子を実装する (2)

ユーザー定義型で演算子を実装することを「演算子をオーバーロードする」と呼びます。
オーバーロードできる演算子の一覧は、演算子のオーバーロード (C# リファレンス) に記載されています。

今回は、実装の頻度が高く、かつ最もややこしい等値演算子 (==, !=) と比較演算子 (<, >, <=, >=) を扱います。

インターフェイスの実装について

等値演算子をオーバーロードするときは、IEquatable<T> インターフェイスを実装します。
また、比較演算子をオーバーロードするときは、IComparable<T> インターフェイスを実装します。

原理上はこれらのインターフェイスを実装しなくても演算子のオーバーロードはできるのですが、これらのジェネリックなインターフェイスを実装することで、各種のライブラリ (Array.IndexOf<T>, Array.Sort<T> など) からは型変換を伴わずに Equals(T) メソッドおよび CompareTo(T) メソッドが呼び出されます。構造体であれば値のボックス化を避けることができます。

これは、各種のライブラリが EqualityComparer<T>.Default および Comparer<T>.Default を通じて各インターフェイスにアクセスする仕組みによるものです。
逆に、自身が型引数 T に対して等値演算・比較演算を呼び出すライブラリを作る立場のときは、
EqualityComparer<T>.Default および Comparer<T>.Default を利用しましょう。

また、各種ライブラリからの等値演算・比較演算さえできればよいというケース (ソートに使うだけ、など) では、
演算子をオーバーロードせずにインターフェイスを実装するだけで済ませる、という選択肢もあります。

実装例 (構造体)

まず、構造体で等値演算子と比較演算子をオーバーロードする実装例を示します。
この構造体は、string 型と int 型のプロパティを持ちます。

using System;
namespace OperatorsLib.Structs
{
public struct Title : IEquatable<Title>, IComparable<Title>
{
public string Name { get; }
public int Number { get; }
public Title(string name, int number) => (Name, Number) = (name, number);
public override string ToString() => $"{Name} #{Number}";
#region Equality Operators
public bool Equals(Title other) => Name == other.Name && Number == other.Number;
public static bool operator ==(Title v1, Title v2) => v1.Equals(v2);
public static bool operator !=(Title v1, Title v2) => !v1.Equals(v2);
public override bool Equals(object obj) => obj is Title v && Equals(v);
public override int GetHashCode() => HashCode.Combine(Name, Number);
// HashCode.Combine を利用できない場合
//public override int GetHashCode() => (Name, Number).GetHashCode();
#endregion
#region Comparison Operators
public int CompareTo(Title other)
{
// 参照型の場合は null 値があるため、静的メソッドが実装されることも多いです。
var c1 = string.Compare(Name, other.Name);
if (c1 != 0) return c1;
return Number.CompareTo(other.Number);
}
public static bool operator <(Title v1, Title v2) => v1.CompareTo(v2) < 0;
public static bool operator >(Title v1, Title v2) => v1.CompareTo(v2) > 0;
public static bool operator <=(Title v1, Title v2) => v1.CompareTo(v2) <= 0;
public static bool operator >=(Title v1, Title v2) => v1.CompareTo(v2) >= 0;
#endregion
}
}
view raw Title.cs hosted with ❤ by GitHub

== および != 演算子をオーバーロードするときは、Equals(object) および GetHashCode メソッドもオーバーライドします。
これらをオーバーライドしないと、警告が出ます (エラーではない)。

ここでは HashCode.Combine メソッドを利用していますが、これは比較的新しく、.NET Standard 2.1 には含まれ、.NET Standard 2.0 には含まれていません。もし HashCode.Combine メソッドを利用できない環境であれば、ValueTuple や Tuple を構成してその GetHashCode メソッドを呼び出すとよいでしょう。

さて、実装しなければならないメソッドがいくつもあるのですが、本題である各プロパティに対する等価性評価をいずれかの一か所で実装して、他からそれを呼び出す形にすればよいです。
ここではインターフェイスを実装する Equals(T) メソッドの中に実際の処理を一元的に記述しています。

いずれの場所に実装しても、処理が本質的に同じであれば問題ないでしょう。
例えば、対称性や読みやすさを重視して、静的メソッドや == 演算子に実装するという考え方もあります。
ただし、Equals(object) メソッドで一元的に実装するのは、ボックス化が発生するため避けます。

比較演算についても同様に CompareTo(T) メソッドの中に実際の処理を記述し、各比較演算子からこれを呼び出しています。

実装例 (クラス)

クラスでも同様に、等値演算子と比較演算子をオーバーロードする実装例を示します。

using System;
namespace OperatorsLib.Classes
{
public class Title : IEquatable<Title>, IComparable<Title>
{
public string Name { get; }
public int Number { get; }
public Title(string name, int number) => (Name, Number) = (name, number);
public override string ToString() => $"{Name} #{Number}";
#region Equality Operators
// other != null では無限ループ。
public bool Equals(Title other) => !(other is null) && Name == other.Name && Number == other.Number;
// 参照型の場合は null 値があるため、静的メソッドが実装されることも多いです。
public static bool Equals(Title v1, Title v2) => v1?.Equals(v2) ?? (v2 is null);
public static bool operator ==(Title v1, Title v2) => Equals(v1, v2);
public static bool operator !=(Title v1, Title v2) => !Equals(v1, v2);
public override bool Equals(object obj) => Equals(obj as Title);
public override int GetHashCode() => HashCode.Combine(Name, Number);
#endregion
#region Comparison Operators
public int CompareTo(Title other)
{
if (other is null) return 1;
var c1 = string.Compare(Name, other.Name);
if (c1 != 0) return c1;
return Number.CompareTo(other.Number);
}
// 参照型の場合は null 値があるため、静的メソッドが実装されることも多いです。
public static int Compare(Title v1, Title v2) => v1?.CompareTo(v2) ?? (v2 is null ? 0 : -1);
public static bool operator <(Title v1, Title v2) => Compare(v1, v2) < 0;
public static bool operator >(Title v1, Title v2) => Compare(v1, v2) > 0;
public static bool operator <=(Title v1, Title v2) => Compare(v1, v2) <= 0;
public static bool operator >=(Title v1, Title v2) => Compare(v1, v2) >= 0;
#endregion
}
}
view raw Title.cs hosted with ❤ by GitHub

クラス (参照型) の場合は null 値を考慮するため、構造体 (値型) のコードよりも複雑になっています。

とくに等値演算子の実装をするときは、循環参照に注意しなければなりません。
例えば、このコードにある !(other is null)other != null としてしまうと無限ループです。
プログラミングに慣れているつもりでも陥りがちですが、このような実装ミスを予防するハックとしては、
定義する順に記述する (上に書いたメンバーから下に書いたメンバーを呼び出さない) などの方法があります。

クラスでは初めから等値演算子が参照の等価比較として定義されているため、Equals メソッドをオーバーライドするのであれば同時に等値演算子もオーバーロードします。
基本的な方針として、各プロパティの値が不変 (immutable) であり、参照ではなくその値であることに意味がある場合に限り、Equals メソッドをオーバーライドして等値演算子をオーバーロードします。逆に、値としての等価性が期待されておらず、等値演算が参照の等価比較を表すことが望ましい場合にはこれらの実装を追加しません。

Visual Studio による自動生成

Visual Studio には等値演算に関するコードを自動生成する機能があります。
型名のところで [Ctrl + .] を押し、[Equals および GetHashCode を生成する] を選択して利用できます。

VS-Equality-1

VS-Equality-2

すると、だいたい上記の実装例のようなコードが生成されます。構造体とクラスのパターンがあります。
実装が面倒な場合はこの機能を使うのもよいでしょう。

次回は算術演算子のオーバーロードについてです。

前回: C# で演算子を実装する (1)
次回: C# で演算子を実装する (3)

作成したサンプル

バージョン情報

  • C# 8.0
  • .NET Standard 2.1
  • .NET Core 3.1

参照

2件のフィードバック to “C# で演算子を実装する (2)”

  1. C# で演算子を実装する (1) | Do Design Space Says:

    […] 次回: C# で演算子を実装する (2) […]

  2. C# で演算子を実装する (3) | Do Design Space Says:

    […] 前回の記事では、等値演算子と比較演算子のオーバーロードについて説明しました。 今回は算術演算子を扱います。算術演算子には、単項演算子 (+, -, ++, –) と二項演算子 (+, -, *, /, %) があります。 […]


コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Google フォト

Google アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中

%d人のブロガーが「いいね」をつけました。