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

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

まずはソースコードを示します。

using System;
using System.Diagnostics;
namespace OperatorsLib.Structs
{
// デバッグ時の表示を ToString と異なるものにしたい場合、[DebuggerDisplay] を追加します。
[DebuggerDisplay(@"\{{Value.ToString(""X8"")}\}")]
public struct BitArray
{
public int Value { get; private set; }
public bool this[int index]
{
get => (Value & (1 << index)) != 0;
set
{
if (value) Value |= 1 << index;
else Value &= ~(1 << index);
}
}
public bool this[Index index]
{
get => this[index.GetOffset(32)];
set => this[index.GetOffset(32)] = value;
}
public BitArray(int value) => Value = value;
public override string ToString() => Value.ToString();
public static BitArray Parse(string s) => int.Parse(s);
public static implicit operator BitArray(int v) => new BitArray(v);
public static explicit operator int(BitArray v) => v.Value;
public static BitArray operator ++(BitArray v) => v.Value + 1;
public static BitArray operator --(BitArray v) => v.Value - 1;
}
}
view raw BitArray.cs hosted with ❤ by GitHub

以下、それぞれについて説明していきます。

キャスト演算子

型変換 (キャスト) には暗黙的 (implicit) な型変換と明示的 (explicit) な型変換があり、ユーザー定義型でキャスト演算子を実装するときにいずれかを選べます。
基本的な方針として、情報の消失がない場合は暗黙的な型変換を実装可能である、と考えます。
ただし、一方が他方のインスタンスをラップしている関係の場合には、ラップする操作を暗黙的な型変換として、ラップを解除する操作を明示的な型変換として定義することが多いです。今回の例では、BitArray が int をラップしています。

Parse メソッド

ToString メソッドの逆の操作として、インスタンスの文字列表現からインスタンスを復元するための Parse メソッドを用意することがあります。Parse メソッドを実装する場合は、少なくとも ToString メソッドで得られた文字列をそのまま Parse メソッドで解析でき、元と同等のインスタンスが得られることが望ましいでしょう。

[TestMethod]
public void Parse()
{
BitArray b1 = 65535; // implicit conversion
Assert.AreEqual("65535", b1.ToString());
var b2 = BitArray.Parse(b1.ToString());
Assert.AreEqual(65535, (int)b2); // explicit conversion
Assert.AreEqual(b1, b2);
}
view raw BitArrayTest.cs hosted with ❤ by GitHub

インデクサー

this[] という特殊な形式のプロパティを実装することで、配列のように [] でアクセスできるようになります。
多次元配列と同様に、複数の引数を受け付けることもできます。

この例では setter でビットを書き換えています。
構造体は、基本的には不変 (immutable) な値として扱うことが多いのですが、値を書き換えることもできます。
このインデクサーの setter を実装することで、キーと値を指定するオブジェクト初期化子を利用できるようになります。
他にも、Add メソッドを定義することで、List のように値を追加するコレクション初期化子を利用できます (後付けの拡張メソッドでも可)。

また、C# 8.0 で Index および Range の機能が導入されたため、インデクサーのオーバーロードで Index 型を引数として使えるようにしています。これで b[^i] の形式により、逆順のインデックスでもアクセスできるようになります。
Range 型は、例えば数列の部分和を求めるというケースで使えるでしょう。

[TestMethod]
public void Indexer()
{
BitArray b = 10;
Assert.AreEqual(false, b[0]);
Assert.AreEqual(true, b[1]);
Assert.AreEqual(false, b[2]);
Assert.AreEqual(true, b[3]);
Assert.AreEqual(false, b[4]);
Assert.AreEqual(false, b[5]);
Assert.AreEqual(false, b[^27]); // Index
Assert.AreEqual(10, (int)b);
b[5] = true;
Assert.AreEqual(true, b[5]);
Assert.AreEqual(true, b[^27]); // Index
Assert.AreEqual(42, (int)b);
}
[TestMethod]
public void Initializer()
{
var b = new BitArray
{
[3] = true,
[6] = true,
};
Assert.AreEqual(72, b.Value);
}
view raw BitArrayTest.cs hosted with ❤ by GitHub

インクリメント演算子

インクリメント演算子 ++ およびデクリメント演算子 -- は、引数の型も戻り値の型も定義元と同じでなければなりません (派生型はOK)。

bit 全探索

以上のように実装すると、いわゆる bit 全探索のアルゴリズムが次のようなコードでできます。

[TestMethod]
public void BitSearch()
{
var n = 8;
var n2 = 1 << n; // 256
// bit 全探索
for (BitArray b = 0; b.Value < n2; b++)
{
for (int i = 0; i < n; i++)
{
// b[i] の真偽による何らかの処理
Console.Write(b[i] ? 1 : 0);
}
Console.WriteLine();
}
}
view raw BitArrayTest.cs hosted with ❤ by GitHub

BitSearch

なお、.NET の基本クラスライブラリの BitArray クラスでは new BitArray(new[] { x }) のようにコンストラクターで元の整数を配列で渡し、BitVector32 構造体ではインデクサーで b[i] ではなく b[1 << i] のようにマスクを表す整数を指定する、という違いがあります。

デバッグ時の表示

Visual Studio において、デバッグ時の [ローカル] ウィンドウや [ウォッチ] ウィンドウで値を表示するとき、既定では ToString メソッドを呼び出した結果が利用されます。これを ToString と異なるものにしたい場合、型に [DebuggerDisplay] 属性を追加します。
コンストラクターに指定する文字列では、{} の中にコードを記述することができます。
上の実装例では、整数を 16 進数形式で表示させています。

VS-Debug-Attribute

次回は論理演算子についてです。

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

作成したサンプル

バージョン情報

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

参照

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

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

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

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

    […] 前回の記事では、キャスト演算子、インデクサーなどについて説明しました。 今回は論理演算子を扱います。 […]

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

    […] (4) キャスト演算子、インデクサー […]


コメントを残す

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

WordPress.com ロゴ

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

Google フォト

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

Twitter 画像

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

Facebook の写真

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

%s と連携中

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