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

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

実装例 (構造体)

例として、2次元ベクトルを表す構造体を実装してみます。
等値演算子に加え、単項演算子 (+, -) と二項演算子 (+, -, *, /) をオーバーロードします。

using System;
namespace OperatorsLib.Structs
{
// デバッグ時の表示を ToString と異なるものにしたい場合、[DebuggerDisplay] を追加します。
public struct Vector : IEquatable<Vector>
{
public static Vector Zero { get; } = new Vector();
public static Vector UnitX { get; } = new Vector(1, 0);
public static Vector UnitY { get; } = new Vector(0, 1);
public double X { get; }
public double Y { get; }
public double Norm => Math.Sqrt(X * X + Y * Y);
public double Angle => Math.Atan2(Y, X);
public Vector(double x, double y) => (X, Y) = (x, y);
public override string ToString() => $"({X}, {Y})";
#region Equality Operators
public bool Equals(Vector other) => X == other.X && Y == other.Y;
public static bool operator ==(Vector v1, Vector v2) => v1.Equals(v2);
public static bool operator !=(Vector v1, Vector v2) => !v1.Equals(v2);
public override bool Equals(object obj) => obj is Vector v && Equals(v);
public override int GetHashCode() => HashCode.Combine(X, Y);
#endregion
#region Unary Operators
public static Vector operator +(Vector v) => v;
public static Vector operator -(Vector v) => new Vector(-v.X, -v.Y);
#endregion
#region Binary Operators
public static Vector operator +(Vector v1, Vector v2) => new Vector(v1.X + v2.X, v1.Y + v2.Y);
public static Vector operator -(Vector v1, Vector v2) => new Vector(v1.X - v2.X, v1.Y - v2.Y);
public static Vector operator *(double c, Vector v) => new Vector(v.X * c, v.Y * c);
public static Vector operator *(Vector v, double c) => new Vector(v.X * c, v.Y * c);
public static Vector operator /(Vector v, double c) => new Vector(v.X / c, v.Y / c);
// ドット積 (dot product)、内積 (inner product)
// 実際には静的メソッドとして定義することが多いです。
public static double operator *(Vector v1, Vector v2) => v1.X * v2.X + v1.Y * v2.Y;
#endregion
public static double Area(Vector v1, Vector v2) => Math.Abs(v1.X * v2.Y - v2.X * v1.Y) / 2;
}
}
view raw Vector.cs hosted with ❤ by GitHub
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OperatorsLib.Structs;
namespace UnitTest.Structs
{
[TestClass]
public class VectorTest
{
[TestMethod]
public void Arithmetic()
{
var v1 = new Vector(2, 2);
var v2 = new Vector(3, 4);
var v3 = new Vector(18, 24);
Assert.AreEqual(new Vector(5, 6), v1 + v2);
Assert.AreEqual(-new Vector(1, 2), v1 - v2);
Assert.AreEqual(new Vector(6, 8), 2 * v2);
Assert.AreEqual(new Vector(6, 8), v3 / 3);
Assert.AreEqual(14, v1 * v2);
}
}
}
view raw VectorTest.cs hosted with ❤ by GitHub

これで、ベクトルの基本的な四則演算が定義され、数式に近い記述ができるようになりました。
またこの例では、二次的な計算結果である Norm, Angle プロパティと、静的メソッド Area を追加しています。
インスタンスに付随する概念や演算子で表現することが難しい概念に対しては、このようにプロパティや静的メソッドで実装します。

単項演算子または二項演算子をオーバーロードするには、引数のいずれかに定義元の型 (ここでは Vector) が含まれていなければなりません。戻り値については、void 以外の型を自由に設定できます。

この例では、ベクトルのドット積 (内積) を * 演算子で定義しています。
しかし、戻り値が Vector である他の * 演算子とは意味が変わってしまうため、演算子ではなく静的メソッド (名前は Dot など) で定義することが多いです。(しかもベクトルには内積も外積もある、という事情もあります。)

また、数値を表す型で ^ 演算子を累乗として利用することも、同様の理由で推奨されていません。
C# の組込み型では ^ 演算子が累乗の意味で使われていないため、累乗は静的メソッド (名前は Pow または Power) として定義することが多いです。

演算子の戻り値の型を自由に設定することは原理上は可能であり、個人の範囲で利用する分にはかまわないと思うのですが、
他の開発者に公開するライブラリでは利用を避けます。
演算子として利用できる記号の種類は決まっており、しかも組込み型で既に意味を与えられているものばかりです。
コンパイラは引数の型の組合せから呼び出すべきメソッドを解決している、と考えれば、演算子の「オーバーロード」と呼ぶことに納得できるでしょう。

クラスとして実装する場合

構造体 (値型) の場合との大きな違いとして、クラス (参照型) の場合は引数で渡されるオブジェクトが null 値である可能性を考慮しなければなりません。このため、数式のように演算子を多くオーバーロードする型を実装するときは構造体にしたほうが簡潔なのですが、例えば継承を利用するためにクラスとして実装したい、というケースもあります。
実装やテストを少し簡単にするために、null 値を new Vector() と同一視する方針も考えられます。

デバッグ時の表示

Visual Studio において、デバッグ時の [ローカル] ウィンドウや [ウォッチ] ウィンドウで値を表示するとき、既定では ToString メソッドを呼び出した結果が利用されます。したがって、ToString メソッドをオーバーライドしていない場合は型名が表示されますが、オーバーライドしておくことで、下図のようにそのオブジェクトを表現する文字列を表示させることができます。

VS-Debug-ToString

このデバッグ時の表示を、ToString メソッドの結果以外のものに設定する方法については次回で紹介します。

次回はキャスト演算子、インデクサーなどについてです。

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

作成したサンプル

バージョン情報

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

参照

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

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

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

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

    […] 前回の記事では、算術演算子のオーバーロードについて説明しました。 今回は例として、.NET の基本クラスライブラリにある BitArray クラスや BitVector32 構造体のような、整数をビットの配列として扱えるものを実装します。キャスト演算子、インデクサー、インクリメント演算子 (算術演算子の一部) をオーバーロードしており、その他にもいろいろな観点が詰め込まれています。 […]

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

    […] (3) の記事への補足です。 […]


コメントを残す

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

WordPress.com ロゴ

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

Google フォト

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

Twitter 画像

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

Facebook の写真

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

%s と連携中

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