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

前回の記事では、論理演算子のオーバーロードについて説明しました。
今回は補足として、その他の注意点や設計について書いていきます。

タプル型との相互変換

C# 7.0 以降の機能で、ユーザー定義型でも Deconstruct メソッドを追加することにより、タプル型と同様に分解を利用できます (後付けの拡張メソッドでも可)。次のコードは、タプル型とのキャスト演算子と Deconstruct メソッドにより、ユーザー定義の構造体をタプル型に近い形で扱うことを目指した実装例です。

namespace OperatorsLib.Structs
{
public struct Vector2
{
public double X { get; }
public double Y { get; }
public Vector2(double x, double y) => (X, Y) = (x, y);
public void Deconstruct(out double x, out double y) => (x, y) = (X, Y);
public static implicit operator Vector2((double x, double y) v) => new Vector2(v.x, v.y);
public static explicit operator (double, double)(Vector2 v) => (v.X, v.Y);
}
}
view raw Vector2.cs hosted with ❤ by GitHub
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OperatorsLib.Structs;
namespace UnitTest.Structs
{
[TestClass]
public class Vector2Test
{
[TestMethod]
public void Cast()
{
Vector2 v = (3, 4);
var (a, b) = v;
var t = ((double c, double d))v;
Assert.AreEqual(v, (a, b));
Assert.AreEqual(v, t);
Assert.AreEqual((v.X, v.Y), (t.c, t.d));
}
}
}
view raw Vector2Test.cs hosted with ❤ by GitHub

KeyValuePair

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

なお、最近のバージョンの基本クラスライブラリでは KeyValuePair<TKey,TValue> に Deconstruct メソッドが追加されているため、
例えば Dictionary<TKey,TValue> の要素を列挙するときに次のように分解を利用できます。

[TestMethod]
public void KeyValuePair_Deconstruct()
{
var d = Enumerable.Range(1, 100).ToDictionary(i => i, i => i / 2.0);
foreach (var (i, value) in d)
Console.WriteLine($"{i} {value}");
}
view raw Vector0Test.cs hosted with ❤ by GitHub

ValueType のような抽象クラスを作る

(1) の記事で、構造体は暗黙的に ValueType クラスを継承するため最初から Equals メソッドでフィールドごとの等価性評価ができると書きました。
これのクラス版で、パフォーマンスは気にしないけど簡単な実装で等値演算を備えたクラスを実装したいという場合、
ValueType と同様にリフレクションでフィールドごとの等価性評価をする抽象クラスを次のコードで作ることができます。
なお、ValueType クラスを直接継承することはできません。

using System;
using System.Reflection;
namespace OperatorsLib.Classes
{
public abstract class EquatableObject
{
// ValueType と同様に、すべてのフィールドで等価性を評価します。
public override bool Equals(object obj)
{
var type = GetType();
if (type != obj?.GetType()) return false;
foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
if (!Equals(field.GetValue(this), field.GetValue(obj))) return false;
return true;
}
public override int GetHashCode()
{
HashCode hc = default;
foreach (var field in GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
hc.Add(field.GetValue(this));
return hc.ToHashCode();
}
}
}
view raw EquatableObject.cs hosted with ❤ by GitHub
namespace OperatorsLib.Classes
{
public class VectorEq : EquatableObject
{
public double X { get; }
public double Y { get; }
public VectorEq(double x, double y) => (X, Y) = (x, y);
}
}
view raw VectorEq.cs hosted with ❤ by GitHub

構造体の既定のコンストラクター

主に (3) の記事への補足です。
構造体では引数付きのコンストラクターを追加しても、引数のない暗黙的な既定のコンストラクターの存在が残ります。
構造体の既定のコンストラクターは、既定値 default(T) を得るために使われます (クラスにおいては null に相当)。
開発者が明示的に既定のコンストラクターを宣言することはできません。

構造体では、この暗黙的な既定のコンストラクターが呼び出されるときはすべてのフィールドが型の既定値で初期化されます。
したがって、明示的なコンストラクターと暗黙的なコンストラクターの間で齟齬が発生しないように注意しなければなりません。
例えば、明示的なコンストラクターの中で、引数で渡された値を単純にフィールドに設定することの他に何らかの処理がある場合や、
0 や null などの既定値を不正 (invalid) な値として扱っている場合、
値が必要になったら計算してキャッシュさせるために -1 や NaN で初期化する場合などが該当します。

次のコードは Norm とAngle を初期化時に計算して設定している2次元ベクトルの例です。
すべてのプロパティの値が 0 でも問題なく使えることが期待されます。

using System;
namespace OperatorsLib.Structs
{
public struct VectorInit
{
public double X { get; }
public double Y { get; }
public double Norm { get; }
public double Angle { get; }
// ctor() と ctor(0, 0) の結果が同等になる必要があります。
public VectorInit(double x, double y)
{
(X, Y) = (x, y);
Norm = Math.Sqrt(X * X + Y * Y);
Angle = Math.Atan2(Y, X);
}
}
}
view raw VectorInit.cs hosted with ❤ by GitHub

演算子の優先順位

演算子が実行される優先順位は C# 演算子と式 (C# リファレンス) に載っています。
優先順位の低い演算を先に実行するには、丸括弧 () で囲みます。この表はなかなかすべて覚えられるものではないため、
優先順位が高くてもあまり馴染みのない演算の組合せの場合には丸括弧で囲むことがあります。
Visual Studio などのエディターに省略を推奨されるのであれば丸括弧を省略してよいでしょう (下図で丸括弧が灰色になっています)。
とくに省略を推奨されない (どちらでもよい) 組合せもあります。

VS-Precedence

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

作成したサンプル

バージョン情報

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

参照

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

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

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


コメントを残す

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

WordPress.com ロゴ

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

Google フォト

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

Twitter 画像

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

Facebook の写真

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

%s と連携中

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