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

C# では、ユーザー定義型 (自作の型) においても、組込み型と同様に ==+ などの演算子を定義することができます。
例えば数式を表現する型を使う場合などにおいて、簡潔に記述できて便利です。

ユーザー定義のクラスまたは構造体に演算子を実装するときの注意点について、全6回にわたって書いていきます。
ただし、すべての演算子を扱うわけではなく、個別の文法について細かくは説明しません。

素のクラスおよび構造体

演算子を実装する前に、まず何も演算子を実装しなかったらどのような動作をするのか確認しておきましょう。
こちらはプロパティを定義しただけのクラス (参照型) です。このコードでオブジェクトの等価性を確認します。

namespace OperatorsLib.Classes
{
public class Vector0
{
public double X { get; }
public double Y { get; }
public Vector0(double x, double y) => (X, Y) = (x, y);
}
}
view raw Vector0.cs hosted with ❤ by GitHub
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OperatorsLib.Classes;
namespace UnitTest.Classes
{
[TestClass]
public class Vector0Test
{
[TestMethod]
public void Equals()
{
var v1 = new Vector0(3, 4);
var v2 = new Vector0(3, 4);
var ec = EqualityComparer<Vector0>.Default;
Assert.IsTrue(ReferenceEquals(v1, v1));
Assert.IsTrue(v1 == v1); // ReferenceEquals と同じ
Assert.IsTrue(Equals(v1, v1));
Assert.IsTrue(v1.Equals(v1));
Assert.IsTrue(ec.Equals(v1, v1));
// プロパティごとの比較はできません。
Assert.IsFalse(ReferenceEquals(v1, v2));
Assert.IsFalse(v1 == v2);
Assert.IsFalse(Equals(v1, v2));
Assert.IsFalse(v1.Equals(v2));
Assert.IsFalse(ec.Equals(v1, v2));
}
}
}
view raw Vector0Test.cs hosted with ❤ by GitHub

等値演算の方法がはじめからいくつか存在しており、実行結果はすべて、参照が同じであれば true、それ以外は false です。

次に、こちらはプロパティを定義しただけの構造体 (値型) です。

namespace OperatorsLib.Structs
{
public struct Vector0
{
public double X { get; }
public double Y { get; }
public Vector0(double x, double y) => (X, Y) = (x, y);
}
}
view raw Vector0.cs hosted with ❤ by GitHub
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OperatorsLib.Structs;
namespace UnitTest.Structs
{
[TestClass]
public class Vector0Test
{
[TestMethod]
public void Equals()
{
var v1 = new Vector0(3, 4);
var v2 = new Vector0(3, 4);
var ec = EqualityComparer<Vector0>.Default;
// 値型では、参照としての比較結果はつねに false です。
// == および != 演算子は定義されていませんが、Equals で各プロパティを比較できます。
Assert.IsFalse(ReferenceEquals(v1, v1));
Assert.IsFalse((object)v1 == (object)v1); // ReferenceEquals と同じ
Assert.IsTrue(Equals(v1, v1));
Assert.IsTrue(v1.Equals(v1));
Assert.IsTrue(ec.Equals(v1, v1));
//Assert.IsTrue(v1 == v1); // コンパイル エラー
Assert.IsFalse(ReferenceEquals(v1, v2));
Assert.IsFalse((object)v1 == (object)v2);
Assert.IsTrue(Equals(v1, v2));
Assert.IsTrue(v1.Equals(v2));
Assert.IsTrue(ec.Equals(v1, v2));
}
}
}
view raw Vector0Test.cs hosted with ❤ by GitHub

構造体の場合はなんと、Equals メソッドでプロパティごと (正確にはフィールドごと) の等価性評価が機能します。
これは、構造体は暗黙的に ValueType クラスを継承していることにより ValueType.Equals メソッドが呼び出され、
すべてのフィールドの値が等しいかどうか判定されるためです。

この仕組みにより、何もしなくても構造体のインスタンスを Array.IndexOf や LINQ の Distinct、さらに Dictionary のキーとしても使うことができます。
ただし、その内部ではリフレクションが利用されているため、パフォーマンスを最適化するには等値演算をカスタム実装したほうがよいでしょう (.NETのクラスライブラリ設計より)。それほどパフォーマンスを気にせず、楽な実装で等値演算を実現したいのであれば、上記のようなコードだけで済ませることもできます。

タプル型など

ついでに、匿名型 (参照型)、Tuple (参照型) および ValueTuple (値型) の動作にも触れておきます。

これらはいずれも Equals メソッドで各要素の等価性が評価されます。
C# 7.3 以降の ValueTuple では言語の機能として == および != 演算子が使え、コンパイラにより各要素の等値演算に展開されます。
なお、内部でフィールド名は無視され、フィールドの定義順により Item1, Item2, ・・・となります (下図は ILSpy)。

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace UnitTest.Structs
{
[TestClass]
public class ValueTupleTest
{
[TestMethod]
public void Equals()
{
// == および != 演算子が使えます。(C# 7.3 以降)
// 内部でフィールド名は無視されます。
var v1 = (x: 3, y: 4);
var v2 = (y: 3, x: 4);
var ec = EqualityComparer<(int, int)>.Default;
Assert.IsFalse(ReferenceEquals(v1, v1));
Assert.IsFalse((object)v1 == (object)v1); // ReferenceEquals と同じ
Assert.IsTrue(Equals(v1, v1));
Assert.IsTrue(v1.Equals(v1));
Assert.IsTrue(ec.Equals(v1, v1));
Assert.IsTrue(v1 == v1);
Assert.IsFalse(ReferenceEquals(v1, v2));
Assert.IsFalse((object)v1 == (object)v2);
Assert.IsTrue(Equals(v1, v2));
Assert.IsTrue(v1.Equals(v2));
Assert.IsTrue(ec.Equals(v1, v2));
Assert.IsTrue(v1 == v2);
}
}
}
view raw ValueTupleTest.cs hosted with ❤ by GitHub

ValueTuple-ILSpy

さらに、Tuple および ValueTuple はともに IComparable インターフェイスを実装しており、
そのまま Array.Sort や LINQ のソートにおいてキーとして利用できます。評価は Item1, Item2, ・・・の順に優先されます。
なお、IComparable インターフェイスを実装しているものの、比較演算子 (<, >, <=, >=) は定義されていません。

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace UnitTest.Structs
{
[TestClass]
public class ValueTupleTest
{
[TestMethod]
public void Sort()
{
var titles = new[]
{
("Book", 22),
("Book", 3),
("article", 111),
("Article", 22),
(null, -1),
("book", 111),
("article", 3),
};
Array.Sort(titles);
foreach (var (name, number) in titles)
Console.WriteLine($"{name} #{number}");
}
}
}
view raw ValueTupleTest.cs hosted with ❤ by GitHub

ValueTuple-Sort

したがって、ユーザー定義型を作成しなくても ValueTuple で済んでしまうケースもあるでしょう。

さて、次回はユーザー定義型に等値演算子および比較演算子を実装します。

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

作成したサンプル

バージョン情報

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

参照

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

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

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

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

    […] (1) のタプル型など の補足となりますが、タプル型 (ValueTuple) が登場する以前は、2つの値を値型で保持するために KeyValuePair<TKey,TValue> 構造体を使うことがありました。この KeyValuePair<TKey,TValue> には等値演算子のオーバーロードも Equals メソッドのオーバーライドもないため、いちおう Equals メソッドで等価性評価はできるもののパフォーマンスは最適化されていません。 […]


コメントを残す

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

WordPress.com ロゴ

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

Google フォト

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

Twitter 画像

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

Facebook の写真

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

%s と連携中

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