【C#】配列をN個に分割する

文字列をSplitで分割する処理は割と見かけますが、ある配列を複数の配列に分割する処理はあまり見かけたことが無いので書きました。

例えば10個の要素の配列を3個ずつの配列に分割して 3 + 3 + 3 + 1要素の配列に分割する場合の処理方法です。

確認環境

以下の環境で確認してます。

  • VisualStudio2019
  • .NET5, C# 9.0

実行コード

private static T[][] SplitArray<T>(T[] list, int maxLen)
{
    int len = list.Length / maxLen;
    if (list.Length % maxLen != 0)
    {
        len++;
    }

    var retList = new T[len][];
    for (int i = 0, p = 0, q =0; i < list.Length; i++, p++)
    {
        if (p >= maxLen)
        {
            p = 0;
            q++;
        }

        if (p == 0)
        {
            retList[q] = 
                (list.Length - i - maxLen < 0) ? 
                    new T[list.Length - i] : 
                    new T[maxLen];
        }

        retList[q][p] = list[i];
    }

    return retList;
}

動作確認

private static void Main(string[] args)
{
    // データ生成
    IEnumerable<string> f(int count)
    {
        for (int i = 0; i < count; i++)
        {
            yield return i.ToString("D5");
        }
    }

    // 分割
    var items = SplitArray(f(10).ToArray(), 3);
    if (items is null)
    {
        return;
    }

    // 結果出力
    for (int i = 0; i < items.Length; i++)
    {
        for (int j = 0; j < items[i].Length; j++)
        {
            Console.WriteLine($"[{i}][{j}]={items[i][j]}");
            // [0][0]=00000
            // [0][1]=00001
            // [0][2]=00002
            // [1][0]=00003
            // [1][1]=00004
            // [1][2]=00005
            // [2][0]=00006
            // [2][1]=00007
            // [2][2]=00008
            // [3][0]=00009

            // 3 + 3 + 3 + 1 に分割されている
        }
    }
}

書いてて思いましたが、事前にデータ構造を整理するような用途以外では使わないかもしれませんね。

短いですが以上です。

【C#】リストや配列をクラス外に安全に公開する方法

この記事は、あるクラスの中で管理しているリストや配列をクラス外に公開して参照してもらう時に安全に公開する方法や考え方や実装の紹介です。

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

はじめに

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

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

    // リストを読み取り専用で外部に公開してるつもり
    public IReadOnlyCollection<Item> Items => _list;

    public ListContainer()
    {
        _list.Add(new Item() { No = 0 });
        _list.Add(new Item() { No = 1 });
        _list.Add(new Item() { No = 2 });
        _list.Add(new Item() { 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) 配列やリスト内の各要素の内容は変更できない
    • 可能であれば要素クラスを immutable にする。無理な場合はオブジェクトのコピーを返す
  • (2) 配列やリスト自体が変更されない(追加されたり削除されたり)
    • IReadOnlyCollection を正しく使用する
  • (3) もしクラス外で変更されてもその変更はクラス内に伝わらない
    • (1)と(2)が守られていれば自動的に達成可能

要素を変更できないようにする

immutable 化するとか言いますが、一度作成したら後で内容を変更できないようします。

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

// 元の定義
public class Item
{
    public int No { get; set; } // 変更可能
}

// ↓↓↓↓

// 読み取り専用プロパティ
public class Item
{
    public int No { get; private set; }
    public Item(int no) => No = no;
}

// もしくはreadonlyフィールド
public class 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 だとオブジェクトのメンバーとして持つケースでコンポーネント内の通常の操作が面倒になるため使い方を考慮しつつ適切な宣言を行います。インターフェースを返す方法は最初のコードからは形が大きく違うので実装が少し大きくなります。これも状況と規模によってどうするか決めましょう。

配列やリストを変更されないようにする

外部に公開するリストは IReadOnlyCollection で公開しますが公開する方法を押さえておけば大丈夫です。

以下のように IReadOnlyCollection のインスタンスをそれぞれの方法で作成して実装します。配列の場合サイズを変更したら割り当て直します。

public class Sample
{
    // 外部に公開する読み取り専用リスト
    public readonly IReadOnlyCollection<Item> List;
    List<Item> _list = new List<Item>();

    // 配列も同じように公開できる
    public IReadOnlyCollection<Item> ListArray { get; private set; }
    Item[] _itemsArray = new Item[10];

    public Sample()
    {
        // リストをコンストラクタに渡すと作成できる
        List = new ReadOnlyCollection<Item>(_list);

        // 配列はAsReadOnlyでReadOnlyCollectionを作成できる
        ListArray = Array.AsReadOnly(_itemsArray);

        // 配列はリサイズしたら割り当て直す
        Array.Resize(ref _itemsArray, 20);
        ListArray = Array.AsReadOnly(_itemsArray);
    }
}

これで追加や削除できない、内容も変更できないようになりました。

以上です。

【C#】QueueとConcurrentQueueの使い方

C# で キューというデータ構造を扱う Queue<T> クラスと、スレッド排他制御機能付きの ConcurrentQueue<T> クラスの使い方の紹介をしたいと思います。

2つのQueue

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

こういったデータ構造を先入れ先出し(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