【C#】コレクション(リスト・配列)を安全に外部公開する

この記事は、あるクラスの中で管理しているコレクション(リスト・配列)をクラス外に渡す時に安全な渡し方の考え方や実装の紹介です。

あるクラス内で管理しているリストをクラス外に公開する場合に具体的にどういった危険性があるのか、どうすれば安全なのかを考えていきます。

はじめに

例えば、以下の例のようにあるクラス内でリストを持っていてそれを外部に公開するケースです。

// リストを持っているクラス
public class ListContainer
{
    List<Item> _list = new List<Item>();

    public IReadOnlyCollection<Item> Items => _list; // リストを外部に公開してる

    public ListContainer()
    {
        _list.Add(new() { No = 0 });
        _list.Add(new() { No = 1 });
        _list.Add(new() { No = 2 });
        _list.Add(new() { No = 3 });
    }
}

// リストの要素
public class Item
{
    public int No { get; set; } // 誰でも変更できる
}

IReadOnlyCollection で変更できない旨を外部に表明してはいますが、要素の内容までは保護されない & 以下のようなコードを書くと割と簡単に変更できてしまいます。

ListContainer container = new ();

foreach (var item in container.Items)
{
    item.No = 1; // (1)mutableな参照型の要素だとIReadOnlyCollectionでも変更できる
}

if (container.Items is List<Item> list)
{
    list.Add(new() { No = 4 }); // (2)元の型にアクセスしやすいとキャストして自由に追加削除できる
}

// (3)クラス外で勝手に変更されてクラス内のリストも変わってしまう
foreach (var item in container.Items)
{
    Console.WriteLine(item.No);
    // > 1 // 中身が書き換わっている
    // > 1
    // > 1
    // > 1
    // > 4 // 追加されている
}
// ★想定しない状態になってしまう

こうなると自分のクラス内で管理してるリストが外で変更されて自分のクラス内の処理が想定外の結果になって処理の失敗が発生してしまいます。

リストを外部に渡す時の要件

従って、特に複数人で作業していた場合は自分の作成したコンポーネントは割と想定と違う使われ方をされることが多く、そういった場合に他人の操作で自分のコンポーネントの中身が壊れるようなことは避けたい所です。その場合コレクションを外部公開する時の要件は概ね以下になります。

  • (1) コンポーネント内のコレクションが変更されない
  • (2) コレクションの各要素を変更してほしくない
  • (3) もしクラス外で変更されてもその変更はクラス内に伝わらない

解決方法

先述のそれぞれの項目の解決方法は以下の通りです。

  • (1) コレクションを変更してほしくない
    • リストを複製した上で IEnumerable を使用する
  • (2) コレクションの各要素を変更してほしくない
    • 可能であれば要素クラスを immutable に変更する、そうでなければ完全なコピーを返す
  • (3) もしクラス外で変更されてもその変更はクラス内に伝わらない
    • (1)と(2)が守られていれば自動的に達成可能

少し長くなるので(2)の要素クラスをimmutableにする方法を先に紹介します。基本的に「一度作成したオブジェクトは変更できない」です。

要素クラスのimmutable化

色々実装方法があるのでいくつか紹介します。

// 元の定義
public class Item
{
    public int No { get; set; }
}

// ↓↓↓↓

public class Item
{
    public int No { get; private set; }
    public Item(int no) => No = no;
}

// もしくは

public class Item
{
    public readonly int No;
    public Item(int no) => No = no;
}

// 以下でもほぼ同じ

public readonly struct Item
{
    public readonly int No;
    public Item(int no) => No = no;
}

上記の方法の他に、外部にはインターフェースだけ公開、クラス内では内部してクラスを用いて安全を確保する方法もあります。

public class ListContainer
{
    List<Item> _list = new();

    // ★インターフェースの要素を返すようにする
    public IEnumerable<IItem> Items => _list.ConvertAll(item => item as IItem);

    public ListContainer()
    {
      // ...
    }

    // ★内部で使用するクラス
    private class Item : IItem
    {
        public int No { get; set; }
    }
}

// 外部に公開するインターフェース
public interface IItem
{
    int No { get; }
}

immurable なクラスの場合 struct と大差ないのですが、struct だとオブジェクトのメンバーとして持つケースでコンポーネント内の通常の操作が面倒になるため使い方を考慮しつつ適切な宣言を行います。インターフェースを返す方法は最初のコードからは形が大きく違うので実装が少し大きくなります。これも状況と規模によってどうするか決めましょう。

実装コード

最終的な実装方法は以下の通りです。

要素クラスにコピーを実装しないで yield return するときに新しいオブジェクトを返す事もできます。

public class ListContainer
{
    List<Item> _list = new();

    // ★IEnumerable を返すように修正する
    public IEnumerable<Item> Items
    {
        get
        {
            foreach (var item in _list)
            {
                yield return item.Clone(); // ★全ての要素はコピーを返す
            }
        }
    }

    public ListContainer() { /* ... */ }
}

public class Item
{
    public int No { get; set; }

    // ★オブジェクトの完全なコピーを返すメソッドを追加
    public Item Clone()
    {
        return new() { No = this.No };
    }
}

要素のインターフェースだけを外部公開する場合の実装は以下の通りです。

public class ListContainer
{
    List<Item> _list = new();

    // ★外部公開するのはインターフェース、ConvertAllで新しいリストを作って返す
    public IEnumerable<IItem> Items => _list.ConvertAll(item => item as IItem);

    public ListContainer() { /* ... */ }

    // ★実際に使用する要素クラス(内部クラスで宣言)
    public class Item
    {
        public int No { get; set; }
    }
}

// ★外部に公開する要素クラスのインターフェース
public interface IItem
{
    int No { get; }
}

この場合、以下のようにキャストすれば追加削除できますが ConvertAll を挟んでいるので内部のリストではなく新しい別のリストとして渡しているためクラス内部まで影響はありません。

if (container.Items is List<IItem> list)
{
    list.RemoveAt(0); // こうすると削除できるがクラス内の要素には影響しない
}

独自に IItem のサブクラスを作って追加することもできますが、これもクラスの内部には影響しません。

オブジェクトのクローンの実装はメンバーを含め完全なコピーを作成します。ただしこのコピー処理(深いコピー or ディープコピー等と言います)の実装は割と不具合を作りやすいため、新規実装する場合は特にインターフェースの実装の方が簡単かもしれません。余談ですが、記事中ではコード例を提示していませんがインデクサーなどでリストの個別の要素にアクセスする場合、クラス外にはオブジェクトのコピーを返すのが定石です。

またリフレクションを使うと要素を書き換えられると値が変わってしまうのですが、それを言い始めるとクラス内部のリストを直接操作も可能なのでここでは考慮しないで良いと思います。

最後に内部管理のコレクションが配列の場合「Array」クラスに「ConvertAll」が同様に存在するため以下の通りほぼ同じ実装ができます。

// コレクションが配列の場合以下のように実装できる
Item[] _array = new Item[5];
public IEnumerable<IItem> Items => Array.ConvertAll(_array, item => item as IItem);

Unity の場合は「[SerializeField] Item[] itemList」のような宣言をよく利用するためその場合こちらを使用します。

以上です。

【C#】Queue<T>とConcurrentQueue<T>の使い方

タイトルの通りQueueの使い方の紹介をしたいと思います。

2つのQueue

まず「Queue」ですが一言で言うと、入れたデータが入れた順番に取り出せる入れ物の事を指します。以下のような1本のパイプのイメージです。

f:id:Takachan:20211122235940p:plain

こういったデータ構造を先入れ先出し(First In First Out)を略して FIFO と呼んだりします。C# にはこの FIFO をサポートする Queue<T> というクラスがありその使い方の紹介になります。

また「ConcurrentQueue<T>」はこの Queue が複数のスレッドから同時にアクセスしても安全なスレッドセーフという特徴を持っています。まとめるとこんな感じです。

クラス 説明
Queue<T> 通常の Queue
ConcurrentQueue スレッドセーフな Queue

ちなみに操作方法は「ほぼ」同じです。

尚この記事では System.Linq で定義されている Linq の拡張メソッドについては範囲が広大になるため言及しません。

確認環境

この記事は以下環境で動作を確認しています。

  • VisualSturio2019
  • .NET 5
  • C# 9.0

Queue<T>の使い方

宣言

Queue<T> は完全名が「System.Collections.Generic.Queue<T>」のため最初に以下のように最初に using を宣言します。

// 最初に宣言する
using System.Collections.Generic;

// アセンブリ
// System.Collections.dll
// 
// クラスの宣言
// public class Queue<T> : IEnumerable<T>, IEnumerable, IReadOnlyCollection<T>, ICollection

基本的な操作

生成と値の出し入れは以下の通りです。List<T> と違って FIFO と用途が限定されているため利用できるメソッドが少なめです。

//
// (1)生成
// - - - - - - - - - - - - - - - - - - - -

// Queueのオブジェクトを新規作成
Queue<int> queue = new();

// 初期容量を指定してオブジェクトを新規作成
Queue<int> queue2 = new(256);

//
// (2)値の出し入れ
// - - - - - - - - - - - - - - - - - - - -

// ★★値を入れる

queue.Enqueue(0);
queue.Enqueue(10);
queue.Enqueue(100);
queue.Enqueue(1000);
queue.Enqueue(10000);
// この時点でのqueueの中身=[0, 10, 100, 1000, 10000]

// ★★値を取り出す

int a = queue.Dequeue();
// a=0, queueの中身=[10, 100, 1000, 10000]
int b = queue.Dequeue();
// b=10, queueの中身=[100, 1000, 10000]
int c = queue.Dequeue();
// c=100, queueの中身=[1000, 10000]
int d = queue.Dequeue();
// d=1000, queueの中身=[10000]
int e = queue.Dequeue();
// d=10000, queueの中身=[(カラ)]

int f = queue.Dequeue();
// 空のQueueから更に取り出そうとすると
// System.InvalidOperationException: 'Queue empty.' が発生する

// ★★値の安全な取り出し

// ★Queueが空でも例外を出さずに安全に値を取り出す
if (queue.TryDequeue(out int f2))
{
    // 値が取得できればここに入る
}
else if (queue.Count != 0)
{
    int f3 = queue.Dequeue();  // この処理方法でも同じ結果が得られる
}

//
// (3)その他の操作
// - - - - - - - - - - - - - - - - - - - -

// ★指定した要素が存在するか確認する
bool contains = queue.Contains(100);
// > contains=true
// true: 存在する / false: 存在しない

// ★保持しているすべての要素を消去する
queue.Clear();

// ★内部バッファーを縮小する
// 大量の要素を入れた場合最大要素数分+αのメモリ領域が確保されっぱなしになるのを開放できる
queue.TrimExcess();

特殊な操作

値の出し入れ以外に出来る操作は以下の通りです。

// 準備
Queue<int> queue = new();
queue.Enqueue(0);
queue.Enqueue(10);
queue.Enqueue(100);
queue.Enqueue(1000);
queue.Enqueue(10000);

//
// 特殊な操作
// - - - - - - - - - - - - - - - - - - - -

// ★★中身は減らさず先頭の値を取得する

// ★減らさずに取り出す
int a = queue.Peek();
// a=0, queueの中身=[10, 100, 1000, 10000]
int b = queue.Peek();
// b=0, queueの中身=[10, 100, 1000, 10000]
int c = queue.Peek();
// c=0, queueの中身=[10, 100, 1000, 10000]

// ★Queueが空でも例外を出さずに安全に値を取り出す
if (queue.TryPeek(out int f1))
{
    // 値が取得できればここに入る
}
else if (queue.Count != 0)
{
    int f3 = queue.Peek(); // この処理方法でも同じ結果が得られる
}

// (2)内容物を配列に変換する
int[] queueArray = queue.ToArray();
// queueArray=[10, 100, 1000, 10000]

// (3)Queue内の全要素を列挙する(取り出さない)
foreach (int item in queue)
{
    Console.WriteLine(item); // 10 > 100 > 1000...
}

ConcurrentQueue<T>の使い方

ConcurrentQueue ですが Queue と「ほぼ」同じです。ただ取り出すときの「Dequeue」と「Peek」が存在せず取り出すときは「TryDequeue」と「TryPeek」のみが存在します。これはこの Queue を使用するときは常に自分以外のスレッドから値が取り出されて、直前までは値があったのに自分が取り出すときに存在しないことがあるため「安全に中身を取り出す」ために Try~ 系で取り出すことになります。

宣言

ConcurrentQueue<T> は完全名が「System.Collections.Generic.ConcurrentQueue<T>」のため最初に以下のように最初に using を宣言します。

// 最初に宣言する
using System.Collections.Generic;

// アセンブリ
// System.Collections.dll
// 
// クラスの宣言
// public class ConcurrentQueue<T> :
//     IProducerConsumerCollection<T>, IEnumerable<T>,
//     IEnumerable, ICollection, IReadOnlyCollection<T>

なんか色々インターフェースを継承していますが、スレッドセーフですよーの目印の「IProducerConsumerCollection」を継承しています。まぁでもこれに大した意味はないです。

各種操作

Queue とほぼ同じなのでざっくり以下の通りになります。

ConcurrentQueue<int> queue = new();

queue.Enqueue(0); // 値を入れるときは同じ
queue.Enqueue(1);

// 値を取り出すときはTryXXXを使う
if (queue.TryDequeue(out int a))
{
    Console.WriteLine($"a={a}"); // 取り出せた時だけ処理する
}
if (queue.TryPeek(out int b))
{
    Console.WriteLine($"b={b}"); // 取り出せた時だけ処理する
}

スレッドセーフとは?

最後に Queue と ConcurrentQueue をマルチスレッドで使用したときの挙動の違いを確認します。まずは以下のコードとコメントを確認してください。

private static void Main(string[] args)
{

    QueueMultiThreadTest();
    ConcurrentQueueMultiThreadTest();
}

// (1) ConcurrentQueueをマルチスレッドで動かす
private static void QueueMultiThreadTest()
{
    Queue<int> queue = new();

    // 0~5までの6個の数字をマルチスレッドでQueueに入れる
    Parallel.For(0, 6, i =>
    {
        queue.Enqueue(i);
        // ★例外が出たり,
        // > System.ArgumentException: 'Destination array was not long enough.
        // Check the destination index, length, and the array's lower bounds.Arg_ParamName_Name'
    });

    foreach (var item in queue)
    {
        Console.WriteLine(item);
        // > 0
        // > 0
        // > 1
        // > 5
        // > 2
        // > 4
        // ★同じ値が複数入ったりする
        // ** 出力が順不同なのは仕様
    }

    Console.WriteLine();

    Queue<int> queue2 = new();
    queue2.Enqueue(0);
    queue2.Enqueue(1);
    queue2.Enqueue(2);
    queue2.Enqueue(3);
    queue2.Enqueue(4);
    queue2.Enqueue(5);

    Parallel.For(0, queue2.Count, i =>
    {
        int cnt = queue2.Dequeue();
        Console.WriteLine(cnt);
        // > 0
        // > 1
        // > 2
        // > 4
        // > 3
        // > 2
        // ★同じ値が複数取得されてることがある
        // ** 出力が順不同なのは仕様
    });
}

// (2) ConcurrentQueueをマルチスレッドで動かす
private static void ConcurrentQueueMultiThreadTest()
{
    ConcurrentQueue<int> queue = new();

    // 0~5までの6個の数字をマルチスレッドでConcurrentQueueに入れる
    Parallel.For(0, 6, i =>
    {
        queue.Enqueue(i); // エラーは出ない
    });

    foreach (var item in queue)
    {
        Console.WriteLine(item);
        // > 0
        // > 2
        // > 1
        // > 3
        // > 4
        // > 5
        // 順不同だが全て操作が完了する
        // ★★同じ値が複数回入ったりしない
    }

    Console.WriteLine();

    ConcurrentQueue<int> queue2 = new();
    queue2.Enqueue(0);
    queue2.Enqueue(1);
    queue2.Enqueue(2);
    queue2.Enqueue(3);
    queue2.Enqueue(4);
    queue2.Enqueue(5);

    Parallel.For(0, queue2.Count, i =>
    {
        if (queue2.TryDequeue(out int cnt)) // Try系で取り出す
        {
            Console.WriteLine(cnt);
        }
        // > 0
        // > 1
        // > 3
        // > 2
        // > 4
        // > 5
        // 順不同だが正常に処理が完了する
        // ★★同じ値が複数回取れたりはしない
    });
}

コード中のコメントに書きましたが Queue はマルチスレッドで使用すると内容が滅茶苦茶になります。途中で例外が出ることもあります。逆に ConcurrentQueue は内容に一貫性がある状態を保っています。複数のスレッドから同時に操作しようとしたときに安全かそうでないかが確認できました。

最後に

Queueでできない事

余談ですが Queue でできない事を以下に紹介します。

FIFO で最初に入れたものが最初に取れるという順序が保証できなくなる操作は提供されていません。

//
// Queueでできない事
// - - - - - - - - - - - - - - - - - - -

// ★★初期値を指定してオブジェクトの初期化はできない
Queue<int> queue = new Queue<int>()
{
    0, 10, 100
    // CS1061:
    // 'Queue<int>' に 'Add' の定義が含まれておらず、型 'Queue<int>'
    // の最初の引数を受け付けるアクセス可能な拡張メソッド 'Add' が見つかりませんでした。
};

// ★★インデックスアクセスはできない
int a = queue[1];
// CS0021:
// 角かっこ[] 付きインデックスを 'Queue<int>' 型の式に適用することはできません

// ★★ソートはできない
queue.Sort();
// CS1061:
// 'Queue<int>' に 'Sort' の定義が含まれておらず、型 'Queue<int>'
// の最初の引数を受け付けるアクセス可能な拡張メソッド 'Sort' が見つかりませんでした。

List<T>との使い分け

Queue の機能ですが基本的に List<T> でも同じことができます。List の部分的な機能が Queue と言ってもいいかもしれません。

// 以下のようにすると Queue と同じことができる
List<int> list = new()
{
    0, 10, 100
};

// 末尾に追加
list.Add(1000); 

// 先頭から取り出し & 削除
int i = list[0];
list.RemoveAt(0);

さて、List でも同じようなことができるのに Queue を使用する意義ですが、「ここは FIFO で順序を保証します」という設計意図が保証できます。この制限を他人に強制できてクラス自体余計な操作ができなので List のような柔軟な操作を禁止できます。

この制限により間に値を挿入したり先頭に値を追加することができません。List クラスは柔軟で動的な操作が可能かつ Linq も組み合わせると多様な機能が提供されている反面、自由すぎて実装意図を読み取るのは結構難しいケースがあるため実装の意図の明確化として有用なのではないかと思います。

ただ、最初は Queue で操作を制限していても途中で割り込み挿入が入ったり、ソートが必要等で仕様が変わると結局 List になってしまう事も多いので純粋な Queue が最後まで維持される事があまりないのも印象的です。

以上です。

C#でビット操作を簡単に行うラッパークラスの紹介

例えばC#でハードウェアに近い I/O を扱う場合、1チャンネルが ushort の1ブロックの読み書きが要求されていて、各ポートはビットごとに割り当てられているなんてケースが割とありますがいちいち出力ポートの設定を読んでビット操作をしてまた書き込むなどの操作はまぁ一部のユースケース以外に許されないと思います。

というか出力側は自分で値を保持していて管理値を送信するのが基本だと思います。

そういった場面でいちいちビット演算しているのはなかなか効率が悪いので「あるビット幅の値を操作を簡単に行えるクラス」というのを考えてみました。

確認環境

確認環境は以下の通りです。

  • VisualSturio2019
  • .NET 5.0
  • Windows10, コンソールプロジェクト

実装コード

さっそく実装コードの紹介です。

使い方

まずは使い方からです。ビット操作を行うクラス「BitManager」があます。そこに初期値を設定したインスタンスを作成し、「SetBit」「GetBit」 で各ビットを操作し、「ToUInt」や「ToULong」などでプリミティブな型に戻すことができます。

static void Main(string[] args)
{
    // 二進数で 1010 1010 1010 1010
    ulong value = 0xAAAA;

    // Long型の管理オブジェクトを作成
    BitManager bm = BitManager.Parse(value);

    // 内容を文字列に変換
    Console.WriteLine(bm.ToString());
    // > 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 1010 1010 1010 1010

    // 3ビット目を1に変更
    bm.SetBit(2, true);
    // 2ビット目を0に変更
    bm.SetBitInt(1, 0);

    // 内容を文字列に変換
    Console.WriteLine(bm.ToString());
    // > 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 1010 1010 1010 1100

    var v = bm.ToULong();
    Console.WriteLine("0x" + v.ToString("X16"));
    // > 0x000000000000AAAC
}

BitManager

「BitManager」の実装コードです。ユースケースによって作成した BitManager の値をプリミティブ型などで作成済みのオブジェクトの値を上書きしたいケースがあると思いますが、今回作成したクラスではサポートしていません。

/// <summary>
/// 任意のビット数を管理するためのクラスです。
/// </summary>
public class BitManager
{
    //
    // Fields
    // - - - - - - - - - - - - - - - - - - - -

    // true: 1 / false: 0 として bool で管理する
    private readonly bool[] bits;

    //
    // props
    // - - - - - - - - - - - - - - - - - - - -

    /// <summary>
    /// 現在管理中のビット数を取得します。
    /// </summary>
    public int BitLength => this.bits.Length;

    /// <summary>
    /// 管理ビット数を指定してオブジェクトを作成します。
    /// </summary>
    public BitManager(int bitCount) => this.bits = new bool[bitCount];

    /// <summary>
    /// 既定のサイズを指定してオブジェクトを作成します。
    /// </summary>
    public BitManager(BitSize size) : this(size.Value) { }

    //
    // Public Methods
    // - - - - - - - - - - - - - - - - - - - -

    // 指定したビットを設定または取得する
    public bool this[int index] { get => this.GetBit(index); set => this.SetBit(index, value); }

    public void SetBit(int bit, bool value) => this.bits[bit] = value; // チェックしない
    public void SetBitInt(int bit, int value) => this.bits[bit] = value != 0;
    public bool GetBit(int bit) => this.bits[bit];
    public int GetBitInt(int bit) => this.bits[bit] ? 1 : 0;

    // 任意のプリミティブ型から BitManager を作成する
    public static BitManager Parse(ulong value) 
        => ParseCommon(new BitManager(BitSize.Long), value);
    public static BitManager Parse(uint value)
        => ParseCommon(new BitManager(BitSize.Int), value);
    public static BitManager Parse(ushort value)
        => ParseCommon(new BitManager(BitSize.Short), value);
    public static BitManager Parse(byte value)
        => ParseCommon(new BitManager(BitSize.Byte), value);

    // 現在の管理値を指定したプリミティブ型へ変換する
    public ulong ToULong() => ToValue(BitSize.Long);
    public uint ToUInt() => (uint)ToValue(BitSize.Int);
    public ushort ToUShort() => (ushort)ToValue(BitSize.Short);
    public byte ToByte() => (byte)ToValue(BitSize.Byte);

    // 全てのビットを列挙する
    public int[] GetAllBitsInt()
    {
        var ret = new int[this.bits.Length];
        for (int i = 0; i < ret.Length; i++)
        {
            ret[i] = this.bits[i] ? 1 : 0;
        }
        return ret;
    }
    public bool[] GetAllBitsBool()
    {
        var ret = new bool[this.bits.Length];
        Array.Copy(this.bits, ret, ret.Length);
        return ret;
    }

    // 2進数で4ビット区切りで出力
    public override string ToString()
    {
        List<bool> list = new (this.bits); 
        list.Reverse();
        int i = 0;
        StringBuilder sb = new StringBuilder();
        list.ForEach(n =>
        {
            if (i++ == 4)
            {
                i = 1;
                sb.Append(' ');
            }
            sb.Append(n ? 1 : 0);
        });
        return sb.ToString();
    }

    //
    // Private Methods
    // - - - - - - - - - - - - - - - - - - - -

    private static BitManager ParseCommon(BitManager bm, ulong value)
    {
        for (int i = 0; i < bm.BitLength; i++)
        {
            ulong mask = 1UL << i;
            if (mask > value)
            {
                break;
            }

            bool bit = (value & mask) != 0;
            bm[i] = bit;
        }
        return bm;
    }

    private ulong ToValue(BitSize size)
    {
        ulong value = 0;
        for (int i = 0; i < size.Value; i++)
        {
            if (bits[i] == false)
            {
                continue;
            }

            value += 1UL << i;
        }
        return value;
    }
}

ビット操作は bool を int で受け付けています。一部効率の悪い処理が含まれるのは把握しています。処理効率を高めたい場合は必要に応じて各自コードを修正した方がいいかもしれません。

以上です。

ディレクトリの中から最新の更新時刻のファイルを取得する

タイトルの通りですが、あるディレクトリ(フォルダ)の中から最新の更新時刻、つまり一番最後に内容を更新したファイルを 1件だけ取得する実装例です。

最近似たような処理を何度も書いた気がするので記事にしてみました。

確認環境

この記事は以下の環境で動作確認を行っています。

  • ViauslStudio 2019(16.10.4)
  • .NET Core 5 + C# 9.0

実装コード

まずは実装コードの紹介です。

DirectoryUtil クラス

最新のものを取得するだけでも良かったのですが逆の動作をする処理も実装しています。各々の説明は以下の通りです。

メソッド名 説明
GetLatestFile ディレクトリの中から最新の更新時刻のファイルを1件取得する
GetOldestFile ディレクトリの中から一番古い更新時刻のファイルを1件取得する
using System;
using System.IO;

namespace Takap.Utility
{
    public static class DirectoryUtil
    {
        /// <summary>
        /// 指定したディレクトリの中から更新時刻が一番新しいファイルを取得します。
        /// </summary>
        /// <returns>
        /// 最新のファイルパス。
        /// ただしディレクトリにファイルが1つも無ければ空文字を返す。
        /// </returns>
        public static string GetLatestFile(string dir)
        {
            return core(dir, (path, file)
                => File.GetLastWriteTime(path) > File.GetLastWriteTime(file));
        }

        /// <summary>
        /// 指定したディレクトリの中から更新時刻が一番古いファイルを取得します。
        /// </summary>
        /// <returns>
        /// 最新のファイルパス。
        /// ただしディレクトリにファイルが1つも無ければ空文字を返す。
        /// </returns>
        public static string GetOldestFile(string dir)
        {
            return core(dir, (path, file) 
                => File.GetLastWriteTime(file) > File.GetLastWriteTime(path));
        }

        private static string core(string dir, Func<string, string, bool> compare)
        {
            if (!Directory.Exists(dir))
                throw new DirectoryNotFoundException($"Directory not found. path={dir}");

            string file = "";
            foreach (string path in Directory.GetFiles(dir))
            {
                if (string.IsNullOrEmpty(file))
                {
                    file = path;
                }
                else
                {
                    if (compare(path, file))
                    {
                        file = path;
                    }
                }
            }
            return file;
        }
    }
}

使い方

上記処理の使い方は以下の通りです。唯一ディレクトリが空の場合、戻り値の string が共通して空文字列になるためチェックしてからパスを参照します。

internal class AppMain
{
    public static void Main(string[] args)
    {
        string filePath = FileUtil.GetLatestFile(@"D:\Sample");
        
        // ディレクトリが空だとファイルパスが取れないのでチェックする
        if(!string.IsNullOrEmpty(filePath))
        {
            Console.WriteLine(filePath); // 一番新しいファイルのパス
        }
    }
}

以上です。

2つのファイルの内容が同じかチェックする

C#で内容を含めて2つのファイルの内容が同じかどうかをチェックする方法です。

処理の流れは、2つのファイルをファイルサイズで比較した後、内容を1バイトずつ比較しています。

FileStreamで1バイトづつ比較しているのでメモリ使用量が少なく比較的高速に動作します。逆に一度ファイルの中身を全部 string で読みだして比較するとかするとメモリは大量に使用するは動作は低速だで大きいファイルの内容を比較するにはこの方法が一番よさそうです。

確認環境

この記事は以下の環境で動作確認を行っています。

  • ViauslStudio 2019(16.10.4)
  • .NET Core 5 + C# 9.0

実装コード

さっそく実装コードの紹介です。

FileUtilクラス

FileUtil クラス内に static メソッドとして IsSameContents を定義しています。

using System;
using System.IO;

public static class FileUtil
{
    /// <summary>
    /// 指定した2つのファイルの内容が同じかを確認します。
    /// </summary>
    public static bool ContentsEqual(string path1, string path2)
    {
        if (path1 == path2)
        {
            return true;
        }

        FileStream fs1 = null;
        FileStream fs2 = null;
        try
        {
            fs1 = new FileStream(path1, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
            fs2 = new FileStream(path2, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);

            if (fs1.Length != fs2.Length)
            {
                return false;
            }

            int file1byte;
            int file2byte;
            do
            {
                file1byte = fs1.ReadByte();
                file2byte = fs2.ReadByte();
            }
            while ((file1byte == file2byte) && (file1byte != -1));
            return (file1byte - file2byte) == 0;
        }
        finally
        {
            using (fs1)
            { }
            using (fs2)
            { }
        }
    }
}

使い方

上記の処理の使い方です。

特に難しい事はないです。メソッドに 2 つのファイルのファイルパスを引数に指定して結果を bool 値で確認します。

static void Main(string[] args)
{
    string file1 = "foo.txt";
    string file2 = "bat.txt";

    bool result = FileUtil.IsSameContents(file1, file2);
    // true: 同じ内容 / false: 異なる内容
}

参考

stackoverflow.com

【Unity】上下左右を表すThickness型を作成する

C#のGUI表現技術ののXAMLには上下左右を表す Thickness 型というものがありますが Unity にはありません(ないですよね?

なので今回はこの Thickness 型を作成して特定の操作を簡単にしたいと思います。GUIの上下端を表したり

確認環境

今回実装・確認を行う環境は以下の通りです。

  • Unity 2020.3.14f1
  • VisualStudio 2019

Editor上のみで確認しています。

実装コード

ではさっそく実装の紹介です。

Thicknessクラス

まずは上下左右を表す Thickness クラスの実装です。以前も紹介している ValueObject のテンプレートを改造して immutable になるように作成します。この方は JsonUtility などでシリアライズできるように readobly フィールドを使用しないようにしています。

using System;
using UnityEngine;

[Serializable]
public /*readonly*/ struct Thickness : IEquatable<Thickness>
{
    // JsonUtility でシリアライズする事を考慮

    // Fields
    //public readonly float Left;
    //public readonly float Top;
    //public readonly float Right;
    //public readonly float Bottom;
#pragma warning disable IDE0044
    [SerializeField] private float left;
    [SerializeField] private float top;
    [SerializeField] private float right;
    [SerializeField] private float bottom;
#pragma warning restore IDE0044

    // Props
    public float Left => left;
    public float Top => top;
    public float Right => right;
    public float Bottom => bottom;

    public Thickness(float l, float t, float r, float b)
    {
        this.left = l;
        this.top = t;
        this.right = r;
        this.bottom = b;
    }

    // 演算子のオーバーライド
    public static bool operator ==(in Thickness a, in Thickness b) => Equals(a, b);
    public static bool operator !=(in Thickness a, in Thickness b) => !Equals(a, b);

    // 4つの組み合わせからHSVオブジェクトを作成する
    public static implicit operator Thickness((float l, float t, float r, float b) thickness)
    {
        return new Thickness(thickness.l, thickness.t, thickness.r, thickness.b);
    }

    // 等値比較演算子の実装
    public readonly override bool Equals(object obj) => (obj is Thickness _obj) && this.Equals(_obj);

    // IEquatable<T> の implement
    public readonly bool Equals(Thickness other)
    {
        // 個別に記述する
        return ReferenceEquals(this, other) ||
               this.left == other.left &&
               this.top == other.top &&
               this.right == other.right &&
               this.bottom == other.bottom;
    }

    public readonly override int GetHashCode()
    {
        unchecked
        {
            var hashCode = this.left.GetHashCode();
            hashCode = (hashCode * 397) ^ this.top.GetHashCode();
            hashCode = (hashCode * 397) ^ this.right.GetHashCode();
            hashCode = (hashCode * 397) ^ this.bottom.GetHashCode();
            return hashCode;
        }
    }
}

RectTransformExtensionクラス

上記の型をGUIと連携させるために以下のような拡張メソッドを定義します。

using System;
using UnityEngine;

public static class RectTransformExtension
{
    /// <summary>
    /// このオブジェクトを <see cref="RectTransform"/> を仮定して 
    /// <see cref="GetLocalSize(RectTransform)"/> に処理を転送します。
    /// </summary>
    public static Thickness GetLocalSize(this Transform t)
    {
        if (t is RectTransform rt)
        {
            return GetLocalSize(rt);
        }
        throw new InvalidOperationException($"t is not RectTransform.");
    }

    /// <summary>
    /// このオブジェクトの中央を(アンカーなどの設定に関わらず固定で)ゼロとしてローカルサイズを取得します。
    /// </summary>
    /// <remarks>
    /// RectTransform.localPostion と対応関係がある数値になる。
    /// </remarks>
    public static Thickness GetLocalSize(this RectTransform rt)
    {
        UnityEngine.Rect rect = rt.rect;
        float hw = rect.x / 2f;
        float hh = rect.y / 2f;
        return new Thickness(-hw, hh, hw, -hh);
    }
}

使い方

使用方法は以下の通りです。

public void Foo(Image img)
{
    Thickness th = img.transform.GetLocalSize(); // 上下左右の境界の位置を取得する
    float top = th.Top;
    float bottom = th.Bottom;
    float left = th.Left;
    float right = th.Right;
}

こうすれば少しはコードの記述が簡単になるかと思います。

関連記事

takap-tech.com

takap-tech.com

【Unity】Mathf.LerpとInverseLerpの覚書

使うときは頻繁に使うし使わないと全然使わないのでなかなか覚えられない Mathf.Leap と InverseLLeap の挙動のメモです。

Mathf.Lerp

リファレンスの説明は以下の通り。

// [a, b] の範囲内で補間する値 value を生成する線形パラメーター t を計算します
float t = Mathf.Leap(float a, float b, float value)
// a: 開始値
// b: 終了値
// value: 開始と終了の間の値

value = 0 ~ 1.0 の範囲の割合(%)を指定すると、対応す値 t が取得できる。具体的には以下の通り。

a b value t memo
0 100.0 -0.1 0 ★最小値より下は最小値
0 100.0 0 0 0~100までの間で0%の値=0
0 100.0 0.5 50.0 0~100までの間で50%の値=50
0 100.0 1.0 100.0 0~100までの間で100%の値=100
0 100.0 1.1 100.0 ★最大値より上は最大値

必ずしも a < b でなくてもよい。a > b でも正常動作する。値が増えると減少するみたいな場合 a > b にして数値を入れ替えると b - t せずに済む。

Mathf.InverseLeap

リファレンスの冒頭の説明が Mathf.Leap と同じ…

// [a, b] の範囲内で補間する値 value を生成する線形パラメーター t を計算します
float t = Mathf.InverseLerp(float a, float b, float value)
// a: 開始値
// b: 終了値
// value: 開始と終了の間の値

value = 0 ~ 100 の範囲の具体値を指定すると対応する割合 t が取れる。Inverse なので value と t が入れ替わる。具体的には以下の通り。

a b value t memo
0 100.0 -1 0 ★最小値より下は最小値
0 100.0 0 0 0~100までの間で0は0%
0 100.0 50 0.5 0~100までの間で50は50%
0 100.0 100 1.0 0~100までの間で100は100%
0 100.0 101 1.0 ★最大値より上は最大値

こちらも必ずしも a < b でなくてもよい。a > b でも正常動作する。逆数を取りたいときは 1.0 - t よりも a < b に入れ替えたほうがよい。

範囲を変更する

上記のメソッドを組み合わせて 0 ~ 100 の範囲で 25 だった数値の割合を維持しながら、0 ~ 1000 の範囲でいくつか(=この場合250)を取得するメソッドを作成したいと思います。

public class MathfUtil
{
    // 割合を維持しながら範家を変更する
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static float ChangeRange(float curMin, float curMax, float value,
            float newMin, float newMax)
    {
        if (value >= curMax)
        {
            return newMax;
        }

        if (value <= curMin)
        {
            return newMin;
        }
        return Mathf.Lerp(newMin, newMax, Mathf.InverseLerp(curMin, curMax, value));
    }
}

// 使い方

// 0 - 100 で 25 を 0 - 1000 の範囲だと 250
float t1 = MathfUtil.ChangeRange(0, 100, 25, 0, 1000);
// t1 = 250

// 0 - 255 で 180 を 0 - 100 の範囲だと 70.588
float t2 = MathfUtil.ChangeRange(0, 256, 180, 0, 100);
// t2 = 70.5882339

ちなみに「計算量が多い」場合で「範囲が毎回固定」の場合、あらかじめ係数を以下のように

float factor = (curMax - curMin) / (newMax - newMin);
// 例えば0~4096を0~255に直した場合
// factor = 0.062255859375

先に計算しておいて

if (value >= curMax)
{
    return newMax;
}
if (value <= curMin)
{
    return newMin;
}
int newValue = (int)Mathf.Round(value * factor);

としたほうが処理速度が100倍以上早くなるのであらかじめ係数が分からない場合などで使用すると幸せになれます(割り算は遅いです)

中身の処理

Mathf に実装されているメソッドの実際の実装は以下の通りです。

// Mathf.cs

public static float Lerp(float a, float b, float t)
{
    return a + (b - a) * Clamp01(t);
}

public static float InverseLerp(float a, float b, float value)
{
    if (a != b)
    {
        return Clamp01((value - a) / (b - a));
    }
    return 0f;
}

public static float LerpUnclamped(float a, float b, float t)
{
    return a + (b - a) * t;
}

public static float Clamp01(float value)
{
    if (value < 0f)
    {
        return 0f;
    }
    if (value > 1f)
    {
        return 1f;
    }
    return value;
}

【C#】インターネット ショートカットを普通のショートカットに変換する

Windows 上にあるインターネットショートカットを普通のショートカットに変換するプログラムです。

既定のブラウザに関わらず指定したブラウザ(Chorome)で強制的に開くように変換します。

using IWshRuntimeLibrary;
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;

namespace ConsoleApp1
{
    internal class Program
    {
        public static void Main(string[] args)
        {
            createShortcut(args[0], getUrl(args[0]));
        }

        /// <summary>
        /// 指定したインターネットショートカットからURLを取得します。
        /// </summary>
        private static string getUrl(string path)
        {
            string[] lines = System.IO.File.ReadAllLines(path);
            if (lines[0] != "[InternetShortcut]")
            {
                throw new NotSupportedException("1行目がショートカットではありません。");
            }

            for (int i = 1; i < lines.Length; i++)
            {
                if (lines[i].StartsWith("URL"))
                {
                    return lines[i].Split('=')[1];
                }
            }

            throw new InvalidDataException("URLタグが見つかりませんでした。");
        }

        private static void createShortcut(string path, string url)
        {
            // 起動するプログラム(=Chorome)
            string programPath = @"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe";

            // ショートカットの生成先
            string dir = Path.GetDirectoryName(path);
            string name = Path.GetFileNameWithoutExtension(path);
            string destPath = Path.Combine(dir, name + ".lnk");

            using (var gen = new ShortcutGenerator())
            {
                // (1) リンク先:起動するプログラムのパス
                IWshShortcut info = gen.GetInfo(destPath);
                info.TargetPath = programPath;
                // (2) 引数
                info.Arguments = url;
                // (3) 作業フォルダ
                info.WorkingDirectory = Path.GetDirectoryName(programPath);
                // (4) 実行時の大きさ 1が通常、3が最大化、7が最小化
                info.WindowStyle = 1;
                // (5)アイコンのパス 自分のEXEファイルのインデックス0のアイコン
                info.IconLocation = 
                    @"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" + ",0";
                gen.Save(info);
            }
        }
    }

    /// <summary>
    /// ショートカットを作成するためのクラス
    /// </summary>
    public class ShortcutGenerator : IDisposable
    {
        //
        // Fields
        // - - - - - - - - - - - - - - - - - - - -

        // ショートカット生成用のCOMオブジェクト
        private WshShell shell = new WshShell();
        // 生成したCOMコンテナの数
        private List<IWshShortcut> shortcutList = new List<IWshShortcut>();
        // Dispose したかどうかのフラグ
        // true : Dispose済み / false : まだ
        private bool isDisposed;

        //
        // Constructors
        // - - - - - - - - - - - - - - - - - - - -

        /// <summary>
        /// オブジェクトを破棄します。
        /// </summary>
        ~ShortcutGenerator()
        {
            this.Dispose();
        }

        //
        // Public Methods
        // - - - - - - - - - - - - - - - - - - - -

        /// <summary>
        /// ショートカットへ与えるデータを格納するオブジェクトを生成します。
        /// </summary>
        public IWshShortcut GetInfo(string path)
        {
            var info = (IWshShortcut)shell.CreateShortcut(path);
            this.shortcutList.Add(info);
            return info;
        }

        /// <summary>
        /// 既存のショートカットをロードします。
        /// </summary>
        public IWshShortcut Load(string path)
        {
            var info = (IWshShortcut)shell.CreateShortcut(path);
            info.Load(path);
            this.shortcutList.Add(info);
            return info;
        }

        /// <summary>
        /// ショートカットを生成します。
        /// </summary>
        public void Save(IWshShortcut info) => info.Save();

        /// <summary>
        /// <see cref="IDisposable"/> の実装。
        /// </summary>
        public void Dispose()
        {
            if (isDisposed)
            {
                return;
            }
            isDisposed = true;

            for (int i = 0; i < this.shortcutList.Count; i++)
            {
                Marshal.FinalReleaseComObject(shortcutList[i]);
            }
            this.shortcutList.Clear();
            this.shortcutList = null;

            Marshal.FinalReleaseComObject(shell);
            this.shell = null;

            GC.SuppressFinalize(this);
        }
    }
}

【C#】リストのジェネリックを親クラスに変換する

List<T> の T を親クラスやインターフェースに変換したいこと無いですか?継承関係があって安全に変換できるならジェネリックの型は親クラスに互換してても良さそうですが List の T では認められていません。この操作はできないので代替案の話になります。

たとえば以下のような定義はだと型が違うエラーになります。

// <T> がこんな感じに宣言されている

// インターフェースの宣言
public interface ISample
{
    int A { get; set; }
}

// 実際の実装クラス
public class Sample : ISample
{
    public int A { get; set; }
    public int B { get; set; }
}

以下のように親クラスなジェネリックの引数には指定できません。

public static void Main(params string[] args)
{
    // 実装クラスでリストのジェネリックを宣言する
    var list = new List<Sample>();

    // CS1503 引数 1: は
    //  'System.Collections.Generic.List<ConsoleApp26.Derived>' から
    //  'System.Collections.Generic.List<ConsoleApp26.Base>' へ変換することはできません
    Foo(list);

    // これは呼び出せる
    Bar(list);
}

// エラー
public static void Foo(List<ISample> list)
{
    // any
}

// OK
public static void Bar(IEnumerable<ISample> list)
{
    // any
}

こういう時は、IEnumerable で渡すことができます。

また、どうしても渡したい場合以下のように Cast → ToList すれば渡せます、ただしこれ、新しく別のリストを作成し渡しているため、渡した先でリストにAdd/Removeしても呼び出し元のリストは変化しません(恐らく「そうじゃないんだよな」というケースが多いと思いますが…)

// 無理やり同じ型に変換する
Foo(list.Cast<ISample>().ToList());

なので、そういった用途が想定される場合、毎回中身をキャストする、もしくは、最初からインターフェースや親クラスでリストを宣言する、が最終的な答えかと思います。

// 左辺で受けるときにこうやってキャストできるためこれを利用する
foreach(ISample s in list)
{
   // ...

// 最初から T をインターフェースや親のクラスで宣言する
var list = new List<ISample>();

以上です。

【C++/CLI】std::functionにマネージドメソッドをバインドする

std::function にメソッドを関連付ける時は std::bind を使用しますが C++/CLI でマネージドメソッドを std::bind 渡したい場合の実装方法の紹介です。

C++11 以降で関数ポインタの代わりに std::function でコールバック呼び出しされるような局面でマネージクラスのメソッドを std::function に渡してネイティブ側から呼び出してもらいたいケースがあると思いますが std::bind にマネージドメソッドを指定すると以下のようにエラーが発生してしまいます。

// ネイティブ側の定義
class Native
{
public:

    void foo(std::function<void(int)> func) // コールバックがstd::functionなネイティブメソッド
    {
        func(999);
    };
}

// マネージド側の定義
public ref class Managed
{
public:

    Managed(Native* lib)
    {
        auto func_1 = std::bind(this->WhatBind, gcroot<Managed^>(this), std::placeholders::_1);
        // この式は指定不可能
        // E2071 pointer-to-member は マネージド クラスでは無効です

        auto func_2 = []()
        {
            this->Callback();
        };
        // マネージドなのでラムダも無理
        // E2093 マネージド クラスのメンバー関数ではローカルラムダは使用できません
        
        lib->foo(func_1) // ★★★渡せない
    }
    
    void WhatBind(int arg1) { /* ... */ }
}

言語仕様的に上無理なので マネージドメソッドは std::bind できません。そこで以下のようにフリー関数を1層経由させます。こうすることで制限を回避できます。

// ネイティブ側の定義
class Native
{
public:

    void foo(std::function func); // コールバックがstd::function
}

// マネージド側の定義
public ref class Managed
{
public:

    Managed(Native* lib);
    
    void WhatBind() { /* ... */ }
}

// 迂回用のネイティブ関数の定義
static void Proxy(Managed^ managed, int arg1)
{
    managed->WhatBind(arg1); // マネージドメソッドの呼び出し
}

Managed::Managed(Native* lib)
{
    // ★★★これならbindを作成できる
    auto func = std::bind(Proxy, gcroot<Managed^>(this), std::placeholders::_1);
    lib->foo(func);
}

以上です。

参考

How to use boost::bind in C++/CLI to bind a member of a managed class

https://stackoverflow.com/questions/163757/how-to-use-boostbind-in-c-cli-to-bind-a-member-of-a-managed-class

【C++/CLI】Action<T1, T2>, Func<..>がエラーになる

C++/CLI で Action は使用できるのに Action<T1, T2> 以降が「E2154 ジェネリック クラス "System::Action" の引数が多すぎます」でエラーになる場合の対処方法です。

ソリューションエクスプローラー > 該当のプロジェクト > 参照 > System.Core を追加

どうやら定義場所が違うみたいです。初期状態だと参照に入ってないためエラーになります。

// mscorlibで定義されている
Action
Action<T1>

// System.Coreで定義されている
Action<T1, T2...>
Func<...>

.NET 4.x 以降でこのエラーが出る場合構文ミスってないか確認します。

property Action<int, int> Func;
// ここでエラーが出るのは「^」の付け忘れ
// 正しくは
// property Action<int, int>^ Func;

メモ:

.NET 4.x 系でもビルドは通るんだけどインテリセンスでは赤い波線でエラー表示が出る場合は System.Core を追加してプロジェクトのコンテキストメニューから「ソリューションの再スキャン」すると治る一時的に収まる。

参照

https://stackoverflow.com/questions/2193808/c-cli-use-of-action-and-func-unsupported

【C#】少し変わった拡張メソッドを作成する

C#では既存のクラスにメソッドを追加できる「拡張メソッド」という機能があります。

今回はこの拡張メソッドの少々変わった使い方の紹介です。

確認環境

今回の確認環境は以下の通りです。

  • .NET Core5(C# 9.0)
  • VisualStudio 2019
  • Windows 10

拡張メソッドの基本

まずは基本的な書き方の説明です。

以下のように「this」をつけてメソッドを宣言します。

public static class IntExtension
{
    // int 型に PlusOne というメソッドを追加する
    // 拡張したい型を先頭に持ってきて this をつける
    public static int PlusOne(this int value) => value + 1; // 今の値に+1した値を返す
    
    // 1. 追加したい型を第一引数に指定する
    // 2. 引数の宣言の先頭に this を指定する
}

そうすると以下のように使用できるようになります。

public static void Foo()
{
    int value = 1;
    int value2 = value.PlusOne(); // 上記で追加したメソッドが使用できる
    // value2 = 2
}

少し変わった使い方

さて、タイトルの通り拡張メソッドの少し変わった使い方の紹介です。

Object に拡張メソッドを定義する

Object 型に拡張メソッドを定義するとすべての型で拡張メソッドが呼び出せるようになります。

以下のようにリフレクションで対象オブジェクトの private フィールド値を強制的に書き換える処理は汎用性があるかもしれません。インテリセンスに毎回出てくるようになるので少し邪魔かも?

public static class ObjectExtension
{
    // 指定したオブジェクトのprivateフィールドの値を強制定期に変更する
    public static void SetField(this object self, string name, object value)
    {
        self.GetType()
            .GetField(name, BindingFlags.InvokeMethod | 
                            BindingFlags.NonPublic | 
                            BindingFlags.Instance)
            .SetValue(self, value);
    }
}

// 他人が作ったクラスのprivateフィールドを強制的に書き換えられる
public static Foo()
{
    int i = 1;
    i.SetField("id", 10); // この例では無意味だけどこんな感じで使える

    double d = 2.0;
    d.SetField("v", 3.0);
}

ジェネリックやタプルと組み合わせる

通常ジェネリックの拡張メソッドはジェネリックで指定します。

// 通常ジェネリックの方を指定するときは拡張メソッドもジェネリックにする = 'T'
public static bool Foo<T>(this List<T> self, T value) { ... }

// 上記のように宣言するとどのListの型でも使用できるようになる
public static void Foo()
{
    List<int> intList = new();
    bool ret = intList.Foo(100);

    List<double> doubleList = new();
    ret = doubleList.Foo(100.0);
}

ただし特定の型を指定することもできます。

// List<int>の時にしか使用できない拡張メソッドの宣言
public static bool Foo(this List<int> self, int value) { ... }

// 今度はdouble型では使用できなくなる
public static void Foo()
{
    List<int> intList = new();
    bool ret = intList.Foo(100);

    List<double> doubleList = new();
    ret = doubleList.Foo(100.0); // エラーになる

    // エラー CS1929 'List<double>' に 'Foo' の定義が含まれておらず、最も適している
    // 拡張メソッド オーバーロード
    // 'SampleExtension.Foo(List<int>, int)' には 'List<int>' 型のレシーバーが必要です

}

で、このジェネリックはタプルが指定できるので以下のように特殊な値の時にしか使用できない拡張メソッドが定義できます。

以下例ではタプルでintが2つの組み合わせの時にしか使用できない拡張メソッドの定義です。

public static class SampleExtension
{
    // タプルでintが2つの組み合わせの時にしか使用できない拡張メソッド
    public static bool Foo(this List<(int, int)> self, int value)
    {
        foreach (var (a, b) in self)
        {
            if (value == a || value == b)
            {
                return true; // どちらか一方に一致すればtrue
            }
        }
        return false;
    }
}

public static void Foo()
{
    List<(int, int)> intList = new()
    {
        ( 0,  0),
        (10, 20),
        (30, 40),
    };
    bool ret = intList.Foo(100); // 個の組み合わせのタプルの時だけ使える

    List<(int, double)> doubleList = new()
    {
        (0, 1.1),
        (2, 3.3),
        (4, 5.5),
    };
    ret = doubleList.Foo(100.0); // こっちはエラーになる
    
    // エラー CS1929 'List<(int, double)>' に 'Foo' の定義が含まれておらず、
    // 最も適している拡張メソッド オーバーロード 
    // 'SampleExtension.Foo(List<(int, int)>, int)' には 'List<(int, int)>' 型のレシーバーが必要です
}

このジェネリックにタプルを使用する手法は局所的なデータの組み合わせにいちいちクラスを定義しなくても組み合わせを表現できるため実装テクニックとして覚えていても損はないと思います。