【C#】2次元配列から2次元配列を切り出す

タイトルの通りなのですが、絵にするとこんな感じです。

以下のような2次元配列があった時に

f:id:Takachan:20200220221712p:plain

以下のようにある部分を範囲選択して切り取り新しい2次元配列を作成する処理になります。

f:id:Takachan:20200220221727p:plain

以下コードですが最新版なら普通のC#でもUnityでもどちらでも行けます。

実装コード

早速実装例を紹介したいと思います。

ArrayUtilityクラス

まず、2次元配列に対する操作を行うUtilityクラスを以下の通り作成します。

冒頭の図のような処理を行うのは中ほどにあるCutメソッドです。

中心座標を指定してその周囲を切り取るCutByCenterも併せて実装しました。

// 配列に対する汎用機能を提供します。
public static class ArrayUtility
{
    // 指定した2次元配列を複製します。
    public static T[,] Clone<T>(T[,] src)
    {
        int h = src.GetLength(0);
        int w = src.GetLength(1);
        var map = new T[h, w];
        for (int y = 0; y < h; y++)
        {
            for (int x = 0; x < w; x++)
            {
                map[y, x] = src[y, x];
            }
        }
        return map;
    }

    // 指定した2次元配列の位置 (x, y) から (w to h) の範囲を切り取ります。
    // (w, h)の 指定が src の範囲を超える場合 src の範囲内で切り取りを行います。
    public static T[,] Cut<T>(T[,] src, int x, int y, int w, int h)
    {
        // コピー元の配列の大きさ
        int ymax = src.GetLength(0);
        int xmax = src.GetLength(1);

        // 入力値が範囲内に収まるかどうか
        if (x < 0 || x > xmax || y < 0 || y > ymax)
        {
            string msg = $"Parameter is out of range. src[y={ymax},x={xmax}], x={x}, y={y}";
            throw new ArgumentOutOfRangeException(msg);
        }
        if (w == 0 || h == 0)
        {
            throw new ArgumentException($"Invalid parameter. w={w}, h={h}");
        }

        // コピー先の配列の大きさ
        int rh = y + h <= ymax ? h : h - (y + h - ymax);
        int rw = x + w <= xmax ? w : w - (x + w - xmax);
        
        // 元の大きさからコピーする
        var map = new T[rh, rw];
        for (int my = 0, _y = y; my < rh; my++, _y++)
        {
            for (int mx = 0, _x = x; mx < rw; mx++, _x++)
            {
                map[my, mx] = src[_y, _x];
            }
        }

        return map;
    }

    // 2次元配列の中心位置 (cx, cy) から指定した半径 (rw, rh) の範囲を切り取ります。
    // (rw, rh) が src の範囲を超える場合 src の範囲内で切り取りを行います。
    // 
    // 戻り値のタプルには src から切り取った範囲の値が設定されます。
    public static (T[,] map, Rect2Di rect) CutByCenter<T>(T[,] src, int cx, int cy, int rw, int rh)
    {
        int xmax = src.GetLength(1);
        int ymax = src.GetLength(0);

        if (cx < 0 || cx > xmax || cy < 0 || cy > ymax)
        {
            string msg = $"Parameter is out of range. src[y={ymax},x={xmax}], x={cx}, y={cy}";
            throw new ArgumentOutOfRangeException(msg);
        }

        // 切り取る範囲の最大・最小値を取得
        int min_x = cx - rw;
        int max_x = cx + rw;
        int min_y = cy - rh;
        int max_y = cy + rh;
        if (min_x < 0)
        {
            min_x = 0;
        }
        if (max_x >= xmax)
        {
            max_x = xmax - 1;
        }
        if (min_y < 0)
        {
            min_y = 0;
        }
        if (max_y >= ymax)
        {
            max_y = ymax - 1;
        }

        var map = new T[max_y - min_y + 1, max_x - min_x + 1];

        for (int my = 0, _y = min_y; _y <= max_y; my++, _y++)
        {
            for (int mx = 0, _x = min_x; _x <= max_x; mx++, _x++)
            {
                map[my, mx] = src[_y, _x];
            }
        }

        return (map, new Rect2Di(min_x, max_x, min_y, max_y));
    }

    // [デバッグ用] 指定した配列の内容を文字列に変換します。
    public static string TostringByDebug<T>(T[,] src)
    {
        var a = new StringBuilder();
        var b = new StringBuilder();
        int ymax = src.GetLength(0);
        int xmax = src.GetLength(1);
        for (int y = 0; y < ymax; y++)
        {
            for (int x = 0; x < xmax; x++)
            {
                b.Append($" {src[y,x]},");
            }
            a.Append(b.ToString().Trim(' ', ','));
            a.Append(Environment.NewLine);
            b.Clear();
        }
        return a.ToString();
    }
}

// 一時的なデータの入れ物
public readonly struct Rect2Di
{
    public int XMin { get; }
    public int YMin { get; }
    public int XMax { get; }
    public int YMax { get; }
    public Rect2Di(int xmin, int ymin, int xmax, int ymax)
    {
        this.XMin = xmin;
        this.YMin = ymin;
        this.XMax = xmax;
        this.YMax = ymax;
    }
}

使い方

上記実装の使用方法です。

public static void Main(string[] args)
{
    int[,] _m = new int[,]
    {
        { 0, 0, 0, 0, 0, 1, 1, 1, 1, 1 },
        { 1, 1, 1, 1, 1, 2, 2, 2, 2, 2 },
        { 2, 2, 2, 2, 2, 3, 3, 3, 3, 3 },
        { 3, 3, 3, 3, 3, 4, 4, 4, 4, 4 },
        { 4, 4, 4, 4, 4, 5, 5, 5, 5, 5 },
        { 5, 5, 5, 5, 5, 6, 6, 6, 6, 6 },
        { 6, 6, 6, 6, 6, 7, 7, 7, 7, 7 },
        { 7, 7, 7, 7, 7, 8, 8, 8, 8, 8 },
        { 8, 8, 8, 8, 8, 9, 9, 9, 9, 9 },
        { 0, 0, 0, 0, 0, 1, 1, 1, 1, 1 },
    };

    // 左上基準に範囲を切り取る
    var map1 = ArrayUtility.Cut(_m, 0, 0, 5, 5);
    Console.WriteLine(map1.ToStringByDebug());
    Console.WriteLine("");
    // 0, 0, 0, 0, 0
    // 1, 1, 1, 1, 1
    // 2, 2, 2, 2, 2
    // 3, 3, 3, 3, 3
    // 4, 4, 4, 4, 4


    // 中心座標を指定して範囲を切り取る
    var (map, rect) = ArrayUtility.CutByCenter(_m, 4, 4, 2, 2);
    Console.WriteLine(map.ToStringByDebug());
    Console.WriteLine("");
    
    // (4, 4)を中心に上下左右に2ずつ(縦横5x5)の範囲を切り取る
    // 2, 2, 2, 3, 3
    // 3, 3, 3, 4, 4
    // 4, 4, 4, 5, 5
    // 5, 5, 5, 6, 6
    // 6, 6, 6, 7, 7
}

ArrayExtension:拡張メソッド版

上記の処理を2次元配列の拡張メソッドとして定義したいと思います。

コード例は以下の通りです。

先ほどのUtilityの処理を中で呼び出すようにしています。

// 配列に対する拡張機能を提供します。
public static class ArrayExtension
{
    // <see cref="ArrayUtility.Clone{T}(T[,])"/> と同じ機能を持つ拡張メソッド
    public static T[,] Clone<T>(this T[,] src) => ArrayUtility.Clone(src);

    // <see cref="ArrayUtility.Cut{T}(T[,], int, int, int, int)"/> と同じ機能を持つ拡張メソッド
    public static T[,] Cut<T>(this T[,] src, int x, int y, int w, int h)
    {
        ArrayUtility.Cut(src, x, y, w, h);
    }

    // <see cref="ArrayUtility.CutByCenter{T}(T[,], int, int, int, int)"/> と同じ機能を持つ拡張メソッド
    public static (T[,] map, Rect2Di rect) CutByCenter<T>(this T[,] src, int cx, int cy, int rw, int rh)
    {
        ArrayUtility.CutByCenter(src, cx, cy, rw, rh);
    }

    // <see cref="ArrayUtility.TostringByDebug{T}(T[,])"/> と同じ機能を持つ拡張メソッド
    public static string ToStringByDebug<T>(this T[,] src)
    {
        ArrayUtility.TostringByDebug(src);
    }
}

使い方

上記実装例の使用方法です。

ほぼ同じですが、2次元配列のメソッドとして使用できるようになっています。多分こっちの方がすっきりすると思います。

public static void Main(string[] args)
{
    int[,] _m = new int[,]
    {
        { 0, 0, 0, 0, 0, 1, 1, 1, 1, 1 },
        { 1, 1, 1, 1, 1, 2, 2, 2, 2, 2 },
        { 2, 2, 2, 2, 2, 3, 3, 3, 3, 3 },
        { 3, 3, 3, 3, 3, 4, 4, 4, 4, 4 },
        { 4, 4, 4, 4, 4, 5, 5, 5, 5, 5 },
        { 5, 5, 5, 5, 5, 6, 6, 6, 6, 6 },
        { 6, 6, 6, 6, 6, 7, 7, 7, 7, 7 },
        { 7, 7, 7, 7, 7, 8, 8, 8, 8, 8 },
        { 8, 8, 8, 8, 8, 9, 9, 9, 9, 9 },
        { 0, 0, 0, 0, 0, 1, 1, 1, 1, 1 },
    };

    // 左上基準に範囲を切り取る
    var map1 = _m.Cut(0, 0, 5, 5);
    Console.WriteLine(map1.ToStringByDebug());
    Console.WriteLine("");
    // 0, 0, 0, 0, 0
    // 1, 1, 1, 1, 1
    // 2, 2, 2, 2, 2
    // 3, 3, 3, 3, 3
    // 4, 4, 4, 4, 4


    // 中心座標を指定して範囲を切り取る
    var (map, rect) = _m.CutByCenter(4, 4, 2, 2);
    Console.WriteLine(map.ToStringByDebug());
    Console.WriteLine("");
    
    // (4, 4)を中心に上下左右に2ずつ(縦横5x5)の範囲を切り取る
    // 2, 2, 2, 3, 3
    // 3, 3, 3, 4, 4
    // 4, 4, 4, 5, 5
    // 5, 5, 5, 6, 6
    // 6, 6, 6, 7, 7
}

こちらも最初のコードと同じように切り取ることができました。

コードばかりになってしまいましたが以上です。

C#でオブジェクト初期化子を使った時の丸カッコの有無

C#には「オブジェクト初期化子」というインスタンス作成時に使用できる初期化方法があります。

例えば以下のようなクラスがあった場合、公開されているプロパティに対して初期化と同時に値が設定できます。

// クラス宣言
public class Point2Di
{
    public int X { get; set; }
    public int Y { get; set; }
}

// オブジェクト初期化子による初期化
var p = new Point2Di
{
    X = 10,
    Y = 20 // プロパティに対して作成と同時に値の設定ができる
};

また配列やコレクションの初期化を行うことができます。

var list = new List<int>
{
   1, 2, 3, 4, 5
};

var array = new int[] { 1, 3, 5, 7 };

この機能でリストの初期化やコンストラクターに引数が含まれないフィールドを作成と同時に初期化することができます。

で、この初期化子ですが以下のように2種類書き方があります。

// (1) オブジェクト初期化子で初期化。【丸カッコ付き】
var p2 = new Point2Di()
{
    X = -3,
    //Y = -4
};

// (2) オブジェクト初期化子で初期化。【丸カッコ無し】
var p3 = new Point2Di
{
    X = -5,
    //Y = -6,
};

何が言いたいのかと言いうと、このカッコの有無で挙動の違いはありません。

動作確認

動作に差異が無いことを確認するために冒頭のクラスでコンストラクタが呼び出されるとログが出力されるようにコンストラクターを追加します。

public class Point2Di
{
    public int X { get; set; }
    public int Y { get; set; }

    // 呼び出されるとログが出る
    public Point2Di() => Console.WriteLine("ctor");
}

次に以下コードを実行します。

// 確認用のコード

// (1) オブジェクト初期化子で初期化。【丸カッコ付き】
var p1 = new Point2Di()
{
    X = -3,
    //Y = -4
};
// 出力:
//> ctor

// (2) オブジェクト初期化子で初期化。【丸カッコ無し】
var p2 = new Point2Di
{
    X = -5,
    //Y = -6,
};
// 出力:
//> ctor

//Console.WriteLine($"p1({p1.X}, {p1.Y})");
Console.WriteLine($"p1({p1.X}, {p1.Y})");
Console.WriteLine($"p2({p2.X}, {p2.Y})");
// 出力:
//> p1(-3, 0)
//> p2(-5, 0)
//  → 結果は同じ

それぞれで、(1) コンストラクターが呼び出される → (2) メンバーは規定値に初期化される → (3) メンバー初期化子の内容がオブジェクトに設定されるという挙動が同じことが確認できたと思います。

以上です。

C#の配列をインデックス付きforeachする

前置き

C#の配列をインデックス付きでforeachする方法は純粋なC#では2種類あります。シンプルにforで回すか、LinqでSelectするかです(ループの外にインデックスを宣言すればどの方法でも処理できますが、ここではインデックス用の変数(i)が外に見えない形式を指しています)

int[] array = new int[] { 100, 101, 102, 103, 104 };

// 通常のfor文の利用
for (int i = 0; i < array.Length; i++)
{
    Console.WriteLine($"[{i}] = {array[i]}");
    // > 出力:
    // > [0] = 100
    // > [1] = 101
    // > [2] = 102
    // > [3] = 103
    // > [4] = 104
}

また、配列にインデックス付きのforeachするならLinqでも以下のように記述すればできます。

int[] array = new int[] { 100, 101, 102, 103, 104 };

// Tupleを使ったインデックス付きforeachの記述例
foreach ((int item, int i) in array.Select((x, i) => (x, i)))
{
    result.Add(item);
}

ですが何度も上記の処理を毎回記述すると結構面倒です。

また上記記述方法以外でループの外でインデックス用の変数を用いれば上記手法に囚われずループ中にインデックスが利用できます。

しかし、宣言したインデックスの変数スコープがループ外に見える事が邪魔なケースもあり、出来ればインデックス用の変数はループ外部から見えないほうがいいです。

またLinqでSelectすると処理が結構遅い事もあり、わざわざ配列を選択した局面で遅いアルゴリズムを選択することも少し気になります。PC環境では気になることはそれほど無いですがモバイルだと影響が結構出るかもしれません。

そこで拡張メソッドの仕組みを利用してインデックス付きのforeachを実現したいと思います。

確認環境

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

  • C# 7.3
  • .Net Core3.2
  • VisualStudio2019

ArrayExtensionクラス

まず、ArrayExtensionクラスを宣言して以下のように拡張メソッドを定義します。

配列を引数にとり内部でforを回しています。

/// <summary>
/// 配列に対する拡張機能を定義します。
/// </summary>
public static class ArrayExtension
{
    /// <summary>
    /// 指定したコレクションに対してインデックス付き foreach を実行します。途中で中断できません。
    /// 
    /// Func:
    ///   T1 : Tuple(i:インデックス, item:配列要素)
    /// </summary>
    public static void ForEach<T>(this T[] array, Action<(int i/*index*/, T item)> func)
    {
        for (int i = 0; i < array.Length; i++)
        {
            func((i, array[i]));
        }
    }

    /// <summary>
    /// 指定した配列に対してインデックス付き foreach を実行します。途中で中断可能版。
    /// 
    /// Func:
    ///   T1 : Tuple(i:インデックス, item:配列要素)
    ///   TRet : ループを継続するかどうかのフラグ, true:継続する / false:ループを終了
    ///   
    /// 戻り値:
    ///   true : 全要素に対して処理完了 / false : 途中でループ終了
    /// </summary>
    public static bool ForEach<T>(this T[] array, Func<(int i/*index*/, T item), bool> func)
    {
        for (int i = 0; i < array.Length; i++)
        {
            if (!func((i, array[i])))
            {
                return false; // 中断終了
            }
        }
        return true;
    }
}

使用方法

上記クラスの使い方は以下の通りです。

配列のインスタンスに対してForEachを使えるように機能が拡張されます。

メソッドに渡すデリゲートにインデックスと配列要素を格納したTupleが渡されるのでループ中はこれを利用します。

public static void Main(string[] args)
{
    // 配列に対する操作
    int[] array = new int[] { 100, 101, 102, 103, 104 };

    // 全部の要素を列挙して処理を行う
    array.ForEach(p =>
    {
        // p.i にインデックス
        // p.item に配列要素が入ってくる
        Console.WriteLine($"[{p.i}] = {p.item}");
    });
    // > 出力:
    // > [0] = 100
    // > [1] = 101
    // > [2] = 102
    // > [3] = 103
    // > [4] = 104

    // ある条件になったらループを途中終了する
    bool ret1 = array.ForEach(p =>
    {
        if (p.item > 102)
        {
            Console.WriteLine($"[{p.i}]で処理を中断");
            return false;
        }
        Console.WriteLine($"[{p.i}] = {p.item}");
        return true;
    });
    // > 出力:
    // > [0] = 100
    // > [1] = 101
    // > [2] = 102
    // [3]で処理を中断
}

上記メソッドはforで記述したときの処理時間を1としたときにForEachメソッドは1.07倍程度のコストで使用できます。

LinqでSelectすると3.1倍程度なので視認性を天秤にかけてまぁ許せる速度かなと思います。

という訳なのでもしよかったら使用してみてください。

.NET Core3.xとUnityでAES暗号化を利用する

タイトルの環境でAES暗号化をしてみたので実装方法の紹介をしたいと思います。

AESの実装方法はサンプルがネットに結構転がっているので簡単に説明するだけでいきます。

いちおう、以前 Cocos2d-x 上で AES 暗号化ライブラリを実装しましたのですが、C++に比べてC#だと実装量が少量 & 環境依存をそこまで考えて処理を分けないで済むので簡単な記述ができます。

確認環境

確認環境は以下の通り

  • Unity 2019.2.17f
  • .NET Core3.1
  • Visual Studio2019
  • Windows 10

(UnityはEditorとWindowsで動作確認済み。Android/iOSの実機動作は未確認、たぶん動くの精神)

AesCypherクラス:AES暗号化の実装

AES暗号化は繰り返し使用する & 少し使い方があったのでAesCypherクラスにライブラリ化しました。

バイト配列のいわゆるバイナリデータを暗号化したバイナリデータに変換と復元を行えます。

サポートする形式は特に指定しなければAES-256, CBC形式で現在ほぼ最強水準のセキュリティとなります。

// AesCypher.cs

using System.Security.Cryptography;

/// <summary>
/// AES暗号化クラス
/// </summary>
public static class AesCypher
{
    /// <summary>
    /// 指定したパラメーターで文字列を暗号化します。
    /// </summary>
    public static byte[] Encrypt(EnctyptionInfo info, byte[] buffer)
    {
        using (var aes = CreateEngine(info))
        {
            ICryptoTransform e = aes.CreateEncryptor();
            return e.TransformFinalBlock(buffer, 0, buffer.Length);
        }
    }

    /// <summary>
    /// 指定したパラメーターで文字列を復号化します。
    /// </summary>
    public static byte[] Decrypt(EnctyptionInfo info, byte[] buffer)
    {
        using (var aes = CreateEngine(info))
        {
            ICryptoTransform e = aes.CreateDecryptor();
            return e.TransformFinalBlock(buffer, 0, buffer.Length);
        }
    }

    /// <summary>
    /// 暗号化を行うためのエンジンを生成します。
    /// </summary>
    public static AesManaged CreateEngine(EnctyptionInfo info)
    {
        return new AesManaged
        {
            KeySize = info.KeySize,
            BlockSize = info.BlockSize,
            Mode = info.Mode,
            IV = info.IV,
            Key = info.Key,
            Padding = info.Padding
        };
    }
}

上記で使用する引数情報を設定するためのデータコンテナです。

// EnctyptionInfo.cs

using System.Security.Cryptography;

/// <summary>
/// AES暗号化を行うときに指定するパラメータを格納するコンテナを表します。
/// </summary>
public class EnctyptionInfo
{
    /// <summary>
    /// 暗号化キーサイズを設定または取得します。256bit固定
    /// </summary>
    public int KeySize { get; } = 256;

    /// <summary>
    /// 暗号化ブロックサイズを設定または取得します。128bit固定
    /// </summary>
    public int BlockSize { get; } = 128;

    /// <summary>
    /// 暗号化モードを取得します。CBC固定。
    /// </summary>
    public CipherMode Mode { get; } = CipherMode.CBC;

    /// <summary>
    /// 初期ベクトルを設定または取得します。
    /// </summary>
    public byte[] IV { get; set; }

    /// <summary>
    /// 暗号化キーを設定または取得します。
    /// </summary>
    public byte[] Key { get; set; }

    /// <summary>
    /// パディングモードを設定または取得します。
    /// </summary>
    public PaddingMode Padding { get; } = PaddingMode.PKCS7;
}

使い方

先にEnctyptionInfoクラスのプロパティにキーと初期ベクトルを設定してAesCypherに引数として指定します。

暗号化・復元のメソッドの第1パラメータに上記コンテナ、第2パラメータに暗号化したいコンテンツのバイナリを指定します。

実際はバイトデータをオブジェクトに相互に変換する処理が必要ですがここでは説明は割愛します。

大抵の場合、JSONで文字列やバイナリのシリアライズからデータを生成しておいて、GetBytesでそのバイナリを取ったりするケースが多いと思います。

// AppMain.cs

using System;
using System.Collections.Generic;
using System.Text;

internal static void Main(string[] args)
{
    // Key(= キー用の)32byte(256bit)のバイナリ
    var key = new List<byte>()
    {
        0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00
    };

    // IV(= 初期ベクトル用)の16byte(128bit)のバイナリ
    var iv = new List<byte>()
    {
        0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00,
        0x00
    };

    // 重要:
    // 以下のような文字列から生成すると数値の範囲が狭くなって
    // セキュリティ強度が低下するので必ず上記のようなバイナリの数列を指定してください
    // 
    // 悪い例:
    // byte[] _key = Encoding.ASCII.GetBytes("12345678901234567890123456789012");
    // byte[] _iv = Encoding.ASCII.GetBytes("1234567890");

    // 暗号化の初期情報を設定
    //  → 基本的にKeyとIVだけ設定すればOK
    var encInfo = new EnctyptionInfo
    {
        Key = key.ToArray(),
        IV = iv.ToArray(),
    };

    // 暗号化対象の文字列
    string message = "ああいいううええおおかかききくくけけここ";

    // 暗号化前のデータのバイナリ
    byte[] plane = Encoding.UTF8.GetBytes(message);
    ShowBytes(plane);
    // > 0xE3 0x81 0x82 0xE3 0x81 0x82 0xE3 0x81 0x84 0xE3...(60byte)

    // 文字列を暗号化
    byte[] encrypted = AesCypher.Encrypt(encInfo, plane);
    ShowBytes(encrypted);
    // > 0x90 0xD1 0xEC 0xFE 0x6D 0xD5 0x29 0x79 0x98 0xFD...(64byte)

    // 暗号化を解除
    byte[] decrypt = AesCypher.Decrypt(encInfo, encrypted); // 暗号化と同じ情報で復号化
    ShowBytes(decrypt);
    // > 0xE3 0x81 0x82 0xE3 0x81 0x82 0xE3 0x81 0x84 0xE3...(60byte)

    // 文字列に戻す
    string message2 = Encoding.UTF8.GetString(decrypt);
    Console.WriteLine(message2);
}

// バイト配列の内容をコンソールに表示する
public static void ShowBytes(byte[] bs)
{
    Array.ForEach(bs, b => Console.Write($"0x{b:X2} "));
    Console.WriteLine("");
}

C#だと簡潔に処理が記述できるのでとても良いです。

コメントでも記載していますが、特にリリース時はKEYとIVはは文字列リテラルからGetByteで生成はやめましょう。バイト列の場合も容易に値を知られないように別途仕組みが必要です。

C#のスクリプトにリテラルで書いたりするとせっかく暗号化してもバレバレで一瞬で暗号化が突破されてしまうので気を付けましょう。

関連記事

takap-tech.com

以上です。

C#の標準機能でデータを圧縮・展開する

C#には結構昔から標準機能として「DeflateStream」と「GZipStream」というデータを圧縮する2種類のライブラリが実装されています。

MSDNに説明が書いてあるのですが読んでもよくわからないと思うので簡単にまとめてみました。

名前 説明
Deflate 可逆圧縮アルゴリズムの名前
GZipStream Defrate圧縮 + ヘッダーとフッターを付与したもの

両方とも中身の実装は「zlib ライブラリ」を使用しているみたいです。 「.NET Framework 4.5以降は」と書いてありますがまぁ大抵の場合、標準的な実装が利用できそうです。

なので、Deflateで圧縮すると純粋なデータ圧縮、GZipStreamで圧縮するとDelfateの圧縮データ + ヘッダー(+フッダー)になります。

使い分けの方針ですが、Deflateだとヘッダーなどがない只のバイナリデータなので元が何のデータが分からなくなるので身元が明らかなローカルシステム内のストレージや、エンドポイント間のデータ転送などで限定的に使用します。GZipはファイルに保存したりするときに.zipや.gzipなど拡張子を付けて保存したり間をあけて生成者と利用者が異なるケースで使用することが多いと思います。

当方説明以外でも説明しているページがあったのですが掲載コードがやや微妙だったので余計なことをせず簡潔なライブラリをC#で実装してみました。

確認環境

確認環境は以下の通り

  • .NET Core 3.1
  • Unity 2019.2.17f1
  • Visual Studio2019
  • Windows 10

(UnityはEditorとWindowsで動作確認済み。Android/iOSの実機動作は未確認、たぶん動くの精神)

DataOperationクラス:圧縮・解凍ライブラリ

文字列を直接圧縮・解凍する方法と、バイナリと圧縮・解凍する処理を提供するクラスを"DataOperation"クラスにまとめました。

// DataOperation.cs

using System.Text;
using System.IO;
using System.IO.Compression;

/// <summary>
/// デフレートアルゴリズムを使用したデータ圧縮・解凍昨日を定義します。
/// </summary>
public static class DataOperation
{
    /// <summary>
    /// 文字列を圧縮しバイナリ列として返します。
    /// </summary>
    public static byte[] CompressFromStr(string message) => Compress(Encoding.UTF8.GetBytes(message));

    /// <summary>
    /// バイナリを圧縮します。
    /// </summary>
    public static byte[] Compress(byte[] src)
    {
        using (var ms = new MemoryStream())
        {
            using (var ds = new DeflateStream(ms, CompressionMode.Compress, true/*msは*/))
            {
                ds.Write(src, 0, src.Length);
            }

            // 圧縮した内容をbyte配列にして取り出す
            ms.Position = 0;
            byte[] comp = new byte[ms.Length];
            ms.Read(comp, 0, comp.Length);
            return comp;
        }
    }

    /// <summary>
    /// 圧縮データを文字列として復元します。
    /// </summary>
    public static string DecompressToStr(byte[] src) => Encoding.UTF8.GetString(Decompress(src));

    /// <summary>
    /// 圧縮済みのバイト列を解凍します。
    /// </summary>
    public static byte[] Decompress(byte[] src)
    {
        using (var ms = new MemoryStream(src))
        using (var ds = new DeflateStream(ms, CompressionMode.Decompress))
        {
            using (var dest = new MemoryStream())
            {
                ds.CopyTo(dest);

                dest.Position = 0;
                byte[] decomp = new byte[dest.Length];
                dest.Read(decomp, 0, decomp.Length);
                return decomp;
            }
        }
    }
}

"DeflateStream" の個所を "GZipStream" に変更すればGZipStreamが同じように使用できます。

用途によると思うので選べるように汎用性を作りこんでもいいのかもしれません。

使い方

上記のクラスの使い方は以下のとおりです。プレーンテキストなら大抵半分よりは小さくなる場合が多いです(データが10バイトのように短すぎると逆にサイズが大きくなります)

// AppMain.cs

using System;
using System.Text;

internal static void Main(string[] args)
{
    // 圧縮対象の文字列
    string message = "ああいいううええおおかかききくくけけここ";

    // 圧縮しない場合の文字のバイト配列
    byte[] planeBytes = Encoding.UTF8.GetBytes(message);
    ShowBytes(planeBytes);
    // > 0xE3 0x81 0x82 0xE3 0x81... 60byte

    // --- ★圧縮する ---

    // 文字列を圧縮する
    byte[] compressed = DataOperation.CompressFromStr(message);
    ShowBytes(compressed);
    // > 0x7B 0xDC 0xD8 0xF4 0x18... 30byte(半分に減ってる

    // バイト配列を圧縮
    byte[] compressed2 = DataOperation.Compress(planeBytes);
    ShowBytes(compressed2);
    // > 0x7B 0xDC 0xD8 0xF4 0x18... 30byte(半分に減ってる

    // --- ★元に戻す ---

    // 圧縮した文字列を元に戻す
    string restore = DataOperation.DecompressToStr(compressed);
    Console.WriteLine(restore);
    // > ああいいううええおおかかききくくけけここ

    // バイト配列を元に戻す
    string restore2 = Encoding.UTF8.GetString(DataOperation.Decompress(compressed2));
    Console.WriteLine(restore2);
    // > ああいいううええおおかかききくくけけここ
}

// バイト配列の内容をコンソールに表示する
public static void ShowBytes(byte[] bs)
{
    Array.ForEach(bs, b => Console.Write($"0x{b:X2} "));
    Console.WriteLine("");
}

それぞれ変換 → 元に戻す操作ができていると思います。

大した事ないので短めですが以上です。

参考資料

dobon.net: GZIPやデフレートでファイルを圧縮する

Gushwell's Dev Notes: データの圧縮と展開

MSDN: DeflateStream クラス

C#で属性を利用して処理に制約の説明を追加する

属性とは

C#に属性(Attribute)という機能があり、これを付ける事でクラスやメンバーに情報を追加することができます。

.NET で使用されている有名なものでは、デバッグ時だけコンパイルされる"Conditionat"属性や、廃止予定を予告するための"Obsolete"属性が有名です。

これらは付与するとIDEや言語と連携して何らかの機能が提供されたりします(例えばメッセージが表示されたり、実装が追加されたり etc...)

また属性は自作することもできます。自作の属性は付与しても特に何が起きるわけではないので純粋な情報追加という意味になります。

もちろん外部ツールでアセンブリからメタ情報を読み取ってCI時にチェックするという使用方法もあります。最終的にかなり手間がかかるので面倒なため限られた人しかやっていません。

後から特別な方法で付与した属性の値は取り出すことができるのでこれを利用してenumを拡張するのような使い方をするのが一番よくある利用のされかたかと思います。

自作の属性でプロパティやメソッドに説明を付与する

で、アイデアレベルになりますが、の属性を使ってメソッドや属性に説明を追加したいと思います。

コメントだと自由記述過ぎるのため、特定の属性が付与されていることで制約を表すことに使えないかなと。

ImplementAttribute:インターフェースの実装を明示する

C#は言語仕様上インターフェースを具象クラスに実装した場合、メソッドにoverrideキーワードをつけません。

"abstract" や "virtual" メソッドは実装した場合 "overide" キーワード指定が必須で後からコードを見たときに区別が一瞬つかないときがあります。またAPIコメントがインターフェースと実装でコピペされているのも結構無駄な感じなのでImplementAttributeを作成してoverrideキーワードの代わりにしたいと思います。

属性の宣言は以下の通りです。付与して説明するだけなのでコンストラクターで引数を指定するだけで値の保存などはしません。

/// <summary>
/// インターフェースを実装していることを表します。
/// 付与するだけで特にシステム上の意味はありません。
/// </summary>
[AttributeUsage(AttributeTargets.Method | 
                AttributeTargets.Property, 
                Inherited = false, AllowMultiple = false)]
public sealed class ImplementAttribute : Attribute
{
    public ImplementAttribute(string name) { }
    public ImplementAttribute(Type type) { }
}

上記属性の使用方法です。

// Sample.cs

using System;

public class Sample : IDisposable
{
    // インターフェースを実装したメソッドに以下のように付与する
    [Implement(nameof(IDisposable.Dispose))]
    public void Dispose()
    {
        // hoge
    }
}

後から見たときにインターフェースを実装していることを属性で明示できたような気がします。

ReqireWithNoCheckAttribute:指定が必須だけどチェックしない

パラメーターとして渡した値が渡した先でチェックされるのかされないのかを属性によって明示します。

チェックしないけど必須パラメーター(パフォーマンス上の都合など)や、プロパティに何か指定しないと後の処理が動かなくなるなどを属性で明示します。

// ReqireAttribute.cs

/// <summary>
/// 値の設定がインスタンス生成時や以降の処理実行時に必須かどうかを表します。
/// (未設定の場合、以降の操作で未チェックの例外が発生する可能性を表す)
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, Inherited = false, AllowMultiple = true)]
public sealed class ReqireAttribute : Attribute
{
    /// <summary>
    /// 設定が必須のパラメーターの旨を明示してオブジェクトを作成します。
    /// </summary>
    public ReqireAttribute() { }

    /// <summary>
    /// 必須パラメーターがメソッド内でチェックされるかどうかを指定してオブジェクトを作成します。
    /// true  : メソッド内でチェックあり
    /// false : パラメーターは渡した先でチェックされない。未検査でパラメーターの仕様で落ちる可能性がある。
    /// </summary>
    public ReqireAttribute(bool inspection) { }
}

上記属性の使用方法です。

// Sample.cs

public class Sample
{
    // 引数がチェックされないで使用される旨を属性で明示する
    public void Execute([Reqire(inspection: false)] Around around)
    {
        // aroundオブジェクトにnullチェックをしないでいきなりアクセスする
        around.Foo();
    }

    // 有意の値をプロパティに設定する必要がある旨を表す。
    [Reqire]
    public string Message { get; set; } = "";

    public void Foo()
    {
        // このメソッド内でMessageプロパティを検査せずに使用する
        if (this.Message == "Foo")
        {
            // hoge
        }
    }
}

呼び出すときにパラメータやプロパティの内容に注意が必要な旨を属性で表明した気になれます。

これも付与して説明するだけでシステム上何か特別な作用があるわけではないのですが、他人のコードにこれがついてると多少ありがたい時があります。

SideEffectAttribute:呼び出すと副作用が発生する

プロパティに値を設定すると連動してほかのプロパティが変化する(クソ設計臭がすごいですが…)や、呼び出すとオブジェクトの状態が変化するメソッドなどに付与して副作用が発生することを属性を使って明示します。

// SideEffectAttribute.cs

using System;

/// <summary>
/// 呼び出しに副作用があることを明示します。
/// 付与するだけで特にシステム上の意味はありません。
/// </summary>
[AttributeUsage(AttributeTargets.Method | 
                AttributeTargets.Property | 
                AttributeTargets.Parameter, 
                Inherited = false, AllowMultiple = true)]
public sealed class SideEffectAttribute : Attribute
{
    public string[] Names { get; private set; }

    /// <summary>
    /// 副作用のある対象を列挙してオブジェクトを新規作成します。
    /// </summary>
    /// <param name="names"></param>
    public SideEffectAttribute(params string[] names) => this.Names = names;
}

上記属性の使用方法です。

// Sample.cs

public class Sample
{
    public int Count { get; private set; }

    // 呼び出すとCountプロパティに影響があることを指定する
    [SideEffect(nameof(Count))]
    public void Execute()
    {
        this.Count++;
    }
}

呼び出すと関係ない個所に影響があることを属性の引数で説明文でも良いですし、変数名でも良いので指定して属性を付与します。

副作用が無いのが一番いいですが理想通りに実装できるケースの方が少ないので他のドキュメントで説明するよりは属性で示した方が後で幸せになれると思います。

DoNotChangeAttribute:値を変えてはいけない

リリース後にリテラルやEnumなどを変更してはいけないことを明示します。

既定値のあるSerializeAttributeやDataContract、SerializeFieldが付与されているプロパティの名称の変更の禁止は当然ですが、定数リテラルとかEnumを数字として扱っている場合、変更されると境界面や境界外で影響が発生するものに付与し変更を禁止する旨を明示します。

/// <summary>
/// フィールド、プロパティ、パラメータの設定値が変更禁止な事を表します。
/// 特にリリースした後に変更してはいけない定数などに付与します。
/// 目印として付与するだけでシステム上の意味は特にありません。
/// </summary>
[AttributeUsage(AttributeTargets.Field | 
                           AttributeTargets.Property | AttributeTargets.Parameter, Inherited = false, AllowMultiple = true)]
public sealed class DoNotChangeAttribute : Attribute
{
    /// <summary>
    /// 既定の初期値でオブジェクトを生成します。
    /// </summary>
    public DoNotChangeAttribute()
    {
    }

    /// <summary>
    /// 禁止の理由や対象を説明する文字列を指定してオブジェクトを生成します。
    /// </summary>
    public DoNotChangeAttribute(string message)
    {
    }
}

上記属性の使用方法です。

// Sample.cs

using System;
using System.Runtime.Serialization;
using UnityEngine;

[DoNotChange("並び順及び名称変更NG")]
[DataContract]
public enum Hoge
{
    [EnumMember] AAA,
    [EnumMember] BBB,
    [EnumMember] CCC,
}

public class Sample
{
    [DoNotChange("名称変更禁止")]
    [SerializeField]
    public GameObject Sprite;
}

名前を指定してない "DataMember" のプロパティの名前を他人が変更してデシリアライズに失敗とか最高にあるあるなので付けておいた方が身を守れます。

あとはSerializeFieldの名前をリリース後に変えてしまって参照が外れたまま気が付かないとか割とよくありそうです。

Enumをintにキャストしてるちょっとアレなコードの中ほどに定義を一個追加してシステムがおかしくなるとかも稀によくあるのでコードに書いておいた方がいいです。

実装を直した方がいいと思いますがそういう訳にもいかない場合、属性を付与して防衛しておくのもアリかなと。。

最後に

ConstAttribte とかやり始めるとキリがないのに実効性が無いものもたくさん出てくると思いますが個人的にあって幸せになれそうな4つを紹介しました。

属性は実装に一切影なく宣言に追加できるのでこういった自己説明的な属性があってもいいのではないかなと思います。

やりすぎると属性だらけになってしまう(というかそうなったら設計直した方がいいですが)のでご利用は計画的に。

また、頑張ればCI時にインスペクションで指摘が出せる可能性もあるのでよかったら使ってみてください。

以上です。

C#で文字列にSQLのIN句のようなメソッドを追加する

SQLにあるIN句をC#の文字列に適用し、リストに格納された文字列がある文字列に一致するかどうかを判定する処理をstringに追加したいと思います。

例えば"ABC123"という文字列の中に"AB", "12"という文字が含まれているかという処理は以下のように書けば判定できます。

string str = "ABC123";

// ★判定方法(1)
if(str == "AB" || str == "12")
{
    // 見つかった
}
else
{
    // 見つからなかった
}

// ★判定方法(2)
List<string> keys = new List<string>()
{
    "AB", "12"
};
foreach(string item in keys)
{
    if(str == item)
    {
        // みつかった
    }
}

(1)の場合、対象が個数が増えていくと重複したコードが増えるし(2)だと見つからないときに何かしら追加が必要です。

どうせなら1行でかけたらいいのに…という実装アイデアです。

コード例

早速ですがコード例です。

stringクラスにInメソッドを拡張メソッドとして定義します。

IN句なので完全一致判定となります。

// StringExtension.cs

/// <summary>
/// <see cref="string"/>  クラスの拡張メソッドを定義します。
/// </summary>
public static class StringExtension
{
    // ★パターン(1)
    // 指定した文字列中に複数の要素の中から1つでも一致するものが含まれているかどうかを確認する
    public static bool In(this string str, params string[] items)
    {
        return items.Contains(str); // 逆にすれば Linq で判定できる (完全一致判定)
    }
    public static bool In(this string str, bool ignoreCase, params string[] items)
    {
        return items.Contains(str, comparer); // 逆にする + 大文字小文字を区別しない (完全一致反転)
    }
    
    // ★パターン(2)
    // 指定した文字列中に複数の要素の中から1つでも一致するものが含まれているかどうかを確認する
    public static bool In(this string str, IEnumerable<string> items)
    {
        return items.Contains(str);
    }
    public static bool In(this string str, bool ignoreCase, IEnumerable<string> items)
    {
        return ignoreCase ? items.Contains(str, comparer) : In(str, items);
    }

    // 大文字/小文字を区別しない実装
    private static readonly Exp comparer = new Exp();
    private class Exp : IEqualityComparer<string>
    {
        public bool Equals(string x, string y)
        {
            return string.Compare(x, y, true) == 0;
        }
        public int GetHashCode(string obj)
        {
            return obj.GetHashCode();
        }
    }
}

使い方

上記クラスの使い方は以下の通りです。

リストと配列、IEnumerableが全て指定できます。Spanでも実装できますが…

大文字と小文字を区別しない場合、第1引数はtrueにします。

static void Main(string[] args)
{
    string str = "ABC123";

    // パターン(1)でチェック その1
    bool contains = str.In(false, "XX", "99", "AB", "12");
    if (contains)
    {
        // 見つかった
    }
    else
    {
        // 見つからない
    }

    // パターン(1)でチェック その2
    string[] items = new string[] { "XX", "99", "AB", "12" };
    contains = str.In(false, items);
    if (contains)
    {
        // 見つかった
    }
    else
    {
        // 見つからない
    }


    // パターン(2)でチェック
    IList<string> list = new List<string>() { "XX", "99", "AB", "12" };
    contains = str.In(false, list);
    if (contains)
    {
        // 見つかった
    }
    else
    {
        // 見つからない
    }

    // パター(2)でチェック
    IEnumerable<string> f()
    {
        yield return "XX";
        yield return "99";
        yield return "AB";
        yield return "12";
    }

    contains = str.In(false, f());
    if (contains)
    {
        // 見つかった
    }
    else
    {
        // 見つからない
    }
}

これで複数の候補の中から完全一致するものがあるかどうかをstringクラスで判定できます。

Containsを拡張する

余談ですが、string.Contains を複数の文字列の中から部分一致するものがあるか判定する処理も記載しておきます。

public static class StringExtension
{
    public static bool Contains(this string str, params string[] keywords)
    {
        foreach (var word in keywords)
        {
            if (str.Contains(word)) return true;
        }
        return false;
    }
}

こうしておくと指定したキーワードの中に部分一致するものがあれば true を返すようになります。こっちのほうがよく使うかもしれません。

C#の共有メモリで簡単にオブジェクトを共有する方法

C#を使ってプロセス間でデータ共有をする際にオブジェクトを共有する方法です。

プロセス間でオブジェクトを共有したい場合、大抵の場合構造体を定義してメモリに書き込めばすれば良いとネットに書いてあります。ですが、普段クラスで扱っているデータをその時だけ構造体にするのは結構手間です。

また、オブジェクトを構造体で定義するのも手間です。定義の方法もC#ではすごくマイナーな相互運用で構造体を記述する知識が必要なのでもっと簡単にオブジェクトを共有できる方法を紹介したいと思います。

考え方

考え方は以下の通りです。

// ◆サーバー側
//
// C#のオブジェクトを作成
//   ↓
// オブジェクトをJsonにシリアライズ
//   ↓
// Jsonを共有メモリに展開

// ◆クライアント側
//
// Jsonを共有メモリから読み取る
//   ↓
// Jsonをオブジェクトにデシリアライズ
//   ↓
// C#のオブジェクトの使用

昨今のWebのプロトコル風にJSONを利用します。この方法だとバイナリ配列のやり取りと違ってオーバーヘッドが発生します。その代わり非常に扱いやすいです。

C++と連携してネイティブから読み出した場合も割と簡単に読み出せると思います。

準備

データを共有するための実装を作先に準備します。

共有するクラスの準備

まず共有したい型の定義です。後でシリアライズするのでクラスに以下のように[DataContract]および、[DataMember]属性を目印として付与します。既存クラスの場合も属性の追加だけで大丈夫です。

using System.Runtime.Serialization;

[DataContract]
public class Item
{
    [DataMember(Name = "id")]
    public int ID { get; set; }

    [DataMember(Name = "rate")]
    public double Rate { get; set; }

    [DataMember(Name = "name")]
    public string Name { get; set; }

    public override string ToString()
    {
        return $"{nameof(this.ID)}={ID}, {nameof(this.Rate)}={this.Rate}, {nameof(this.Name)}={this.Name}";
    }
}

JSONを読み書きするクラス

C#のライブラリにJSONを扱えるクラスがあるのでそれを使用してオブジェクト ⇔ JSON文字列の相互変換を定義します。

public static class JsonUtility
{
    // 任意のオブジェクトを JSON メッセージへシリアライズします。
    public static string Serialize(object graph)
    {
        using (var stream = new MemoryStream())
        {
            var serializer = new DataContractJsonSerializer(graph.GetType());
            serializer.WriteObject(stream, graph);
            return Encoding.UTF8.GetString(stream.ToArray());
        }
    }

    // Jsonメッセージをオブジェクトへデシリアライズします。
    public static T Deserialize<T>(string message)
    {
        using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(message)))
        {
            //var setting = new DataContractJsonSerializerSettings()
            //{
            //    UseSimpleDictionaryFormat = true,
            //};
            var serializer = new DataContractJsonSerializer(typeof(T)/*, setting*/);
            return (T)serializer.ReadObject(stream);
        }
    }
}

使用方法

上記のクラスを使って実際にオブジェクトを共有します。

サーバー側

サーバー側ではItemオブジェクトを作成してJSONに変換後に共有メモリにデータを展開します。

// サーバー側実装
public static void Main(string[] args)
{
    // 共有するオブジェクト
    var item = new Item
    {
        ID = 1,
        Rate = 2.553,
        Name = "sample1234567890",
    };

    var sharedMemory = MemoryMappedFile.CreateNew("share", 1024 * 1024 * 5); // 5MB分領域を確保
    using (MemoryMappedViewAccessor view = sharedMemory.CreateViewAccessor())
    {
        string jsonStr = JsonUtility.Serialize(item);
        char[] jsonCharArray = jsonStr.ToCharArray(0, jsonStr.Length); // C#のcharは2byte

        // 受け取り手はサイズが分からないので先頭にサイズを書いておく
        view.Write(0, jsonCharArray.Length);

        // Json文字列を共有メモリに書き込み
        view.WriteArray(sizeof(int), jsonCharArray, 0, jsonCharArray.Length);
    }

    Console.ReadLine(); // プロセスが終了するとデータが消えるのでここで止めておく

    using (sharedMemory) { }
}

読み取り側は共有メモリに実際にどれくらいデータが書き込まれているかわかりません。

そのため、共有メモリの先頭4バイトの領域にデータサイズを書き込んでいます。

クライアント側

クライアント側の実装です。

共有メモリからサイズを読んでJSONの読み取り → オブジェクトにデシリアライズを行います。

public static void Main(string[] args)
{
    string json = ReadDataByJson();

    // 読み取ったデータをデシリアライズしてオブジェクトに戻す
    var item = JsonUtility.Deserialize<Item>(json);

    Console.WriteLine($"{item}");
    // > ID=1, Rate=2.553, Name=sample1234567890
}

// 共有メモリからデータを読み取る
public static string ReadDataByJson()
{
    using (var sharedMemory = MemoryMappedFile.OpenExisting("share"))
    using (MemoryMappedViewAccessor view = sharedMemory.CreateViewAccessor())
    {
        // 共有メモリに先頭に書き込まれている配列サイズを取得する
        int size = view.ReadInt32(0);
        char[] jsonCharArray = new char[size];

        // 共有メモリからデータを取得する
        view.ReadArray(sizeof(int), jsonCharArray, 0, jsonCharArray.Length);

        // 扱いやすいように文字列にして返す
        return new string(jsonCharArray);
    }
}

関連記事

C#のMemoryMappedFileで作成した共有メモリをC++(ネイティブ)から利用する

C#でMemoryMappedFileを使って作成した共有メモリをVC++(あるいはC言語)から利用する方法です。

条件

  • .NET Framework
  • VC++
  • Windows限定(WINAPIを利用)

.NET Core & Linuxとかでは全然使えないのでご了承ください。

C#側のコード

まずはこんな感じでC#上で共有メモリを作成してデータを登録しておきます。

string mapName = "Global\\hoge.map";
int length = 100;

// 共有メモリの作成
var mapFile = MemoryMappedFile.CreateNew(mapName, length/*byte*/);

// アクセス権限を誰でもアクセス可能に変更する
MemoryMappedFileSecurity permission = info.MapFile.GetAccessControl();
permission.AddAccessRule(new AccessRule<MemoryMappedFileRights>("Everyone", 
    MemoryMappedFileRights.FullControl, AccessControlType.Allow));
info.MapFile.SetAccessControl(permission);

// 書き込むデータの作成
byte[] array = new byte[100];
for(int i = 0; i < length; i++)
{
    array[i] = i;
}

// データを書き込む
using (MemoryMappedViewAccessor accesor = MemoryMappedFile.OpenExisting(mapName).CreateViewAccessor())
{
    accesor.WriteArray(0, array, 0, array.Length); // 0~99までのデータを書き込んでる
}

このAddAccessRuleは.NETのほかの権限設定系でも頻出するイデオムなので覚えておいて損はないと思います。xxxRightsをAddAcessRuleするみたいな。

C++側のコード

Win32APIを使って既存の共有メモリを読み取ります。いつものアレです。

#include <Windows.h>

// C#で作成した共有メモリの名前
LPCWSTR lpName = L"Global\\hoge.map";
HANDLE hMap = OpenFileMapping(FILE_MAP_WRITE, true, lpName);
if (hMap == nullptr)
{
    return -1;
}

// Win32APIで共有メモリを開く
LPVOID lvptr = MapViewOfFile(hMap, FILE_MAP_WRITE, 0, 0, 100);
unsigned char* bytes = reinterpret_cast<unsigned char*>(lvptr);
if (bytes == nullptr)
{
    return -2;
}

for (int i = 0; i < 100; i+=4)
{
    std::cout << "[" << i << "]" << (int)bytes[i] << std::endl;
}

// 出力結果
// > [0] 0
// > [1] 1
// > ...
// > [98] 98
// > [99] 99

// 開放する
UnmapViewOfFile(lvptr);
CloseHandle(hMap);

クソ雑エラー処理ですがだいたいこんな感じです。

C#側でintとかfloat型で登録した場合C++側でデータサイズを合わせてforを回すようにしてください。(intの場合4バイト幅など

そこはかとなくメモリ上にビッグエンディアンで配置されているような気がしますが、きっと何か癖のようなものがあるのだと思いますが相互運用はbyte単位でしかしないので詳しくは調べていません。

以上です。

C#のMemoryMappedFile(共有メモリー)でエラーが出たときの対処法

Windows上でサービスなどのシステム権限やAdminisratorsなどの高い権限でMemoryMappedFile使って共有メモリを作成し、一般ユーザー権限のような権限レベルの異なるプロセスから共有メモリをOpenExistingしようとする場合に出るであろうエラーの対象方法です。

同じ権限上で動くプロセス間では発生しない(=VisualStudioで編集時は気が付きにくい)ので気が付くのが遅れがちなので参考になればと思い書きました。

クライアント側でFileNotFoundExceptionが発生する

サービスで作成したMemoryMappedFileをクライアント側でOpenExisting開こうとしたときに以下のようにFileNotFoundExceptionが発生した場合です。

// こんなエラーが発生する
System.IO.FileNotFoundException: 指定されたファイルが見つかりません。
   場所 System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
   場所 System.IO.MemoryMappedFiles.MemoryMappedFile.OpenCore(String mapName, HandleInheritability inheritability, Int32 desiredAccessRights, Boolean createOrOpen)
   場所 System.IO.MemoryMappedFiles.MemoryMappedFile.OpenExisting(String mapName, MemoryMappedFileRights desiredAccessRights, HandleInheritability inheritability)
   場所 ...以下自分のコード

この例外が発生した場合、CreateNew する時のmapNameの頭に"Global\"を付けます。

異なる権限レベルで作成された共有メモリーはデフォルトでは見えません。

// こんな風に先頭にGlobalつけないと下位権限には不可視になる
MemoryMappedFile.CreateNew("Global\\home.map", 1024 * 1024 * 12);

ちなみにこうしてしまうと管理者権限でしか立ち上がらなくなるのでVisualStudioを管理者権限で起動しないといけなくなります。

割と作業に差し支えるため、デバッグ時は以下のように切り分けしたほうがいいかもしれません。

string mapName = "Global\\hoge.map";
if(isDebugMode()) // デバッグもーとの時はGlobalプレフィックスを付けない
{
    mapName = "hoge.map";
}

クライアント側でUnauthorizedAccessExceptionが発生する

サービスで作成したMemoryMappedFileをクライアント側でOpenExisting開こうとしたときに以下のようにUnauthorizedAccessExceptionが発生した場合です。

// こんなエラーが発生する
System.UnauthorizedAccessException: パスへのアクセスは拒否されました。
   場所 System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
   場所 System.IO.MemoryMappedFiles.MemoryMappedFile.OpenCore(String mapName, HandleInheritability inheritability, Int32 desiredAccessRights, Boolean createOrOpen)
   場所 System.IO.MemoryMappedFiles.MemoryMappedFile.OpenExisting(String mapName, MemoryMappedFileRights desiredAccessRights, HandleInheritability inheritability)
   場所 ...以下自分のコード

これはサーバー側で適切な権限を付与できていない場合に発生します。

MemoryMappedFile.CreateNewした後に設定が必要です。

// 事前に宣言しておく
using System.Security.AccessControl;

// ...中略...

var map = MemoryMappedFile.CreateNew("Global\\home.map", 1024 * 1024 * 12);

// 既定の権限設定を取得する
MemoryMappedFileSecurity permission = map.GetAccessControl();

// Everyone(= 誰でも)アクセスできるようにする
permission.AddAccessRule(
  new AccessRule<MemoryMappedFileRights>("Everyone", 
    MemoryMappedFileRights.FullControl, AccessControlType.Allow));

// 権限を設定しなおす
map.SetAccessControl(permission);

サンプルなのでEveryone設定してますが、権限がガバなので使用時は適切なユーザーを指定してください。

これでプロセスを起動すると共有メモリを共有できるようになります。

【C#】リストから要素をランダムにN個取得する

今回は、リストから要素をN個取得する実装方法の紹介をしたいと思います。

いちど選んだ要素はもう使わない + データを取り出した後に元のリストが変化しないように実装していきます。

確認環境

実装環境は以下の通りです。

  • Visual Studio 2022 + .NET Core3.1(C#6.0)
  • Unity 2022.3.5f1

使い方

まずは使うとどうなるか紹介します。

要素をランダムにN個取得する GetRandomN メソッドと、リストの内容を全てランダムに取得する Shuffle メソッドの 2つがあります。

// テスト用のデータの作成。100~199までの整数のリスト
IList<int> source = Enumerable.Range(100, 100).ToList();

// ランダムに5つ値を取得する
foreach (int value in source.GetRandomN(5))
{
    Console.WriteLine(value);
    // 144
    // 191
    // 178
    // 175
    // 159
}

// 全部の値をランダムに取得する
foreach (var value in source.Shuffle())
{
    Console.WriteLine(value);
    // 126
    // 192
    // 104
    // 182
    // 180
    // ... (以下略
}

RandomUtilクラス

ランダムに要素を選択する処理を実装しているクラスです。

// RandomUtil.cs

using System;
using System.Collections.Generic;
using System.Linq;

public static class IListExtensions
{
    // 指定したリスト内からランダムでN個のデータを取得する
    public static IEnumerable<T> GetRandomN<T>(this IList<T> collection, int n)
    {
        if (n > collection.Count)
        {
            throw new ArgumentOutOfRangeException("リストの要素数よりnが大きいです。");
        }

        var indexList = new List<int>(collection.Count);
        for (int p = 0; p < collection.Count; p++) indexList.Add(p);

        var random = new Random();
        for (int i = 0; i < n; i++)
        {
            int index = random.Next(0, indexList.Count);
            int value = indexList[index];
            indexList.RemoveAt(index);
            yield return collection[value];
        }
    }

    // リストの内容をランダムに全て取得する
    public static IEnumerable<T> Shuffle<T>(this IList<T> collection)
    {
        return collection.GetRandomN(collection.Count);
    }
}

もし Unity 環境の場合以下のように GetRandomN メソッド書き換えると Unity 用の処理になります。

// Unityの場合以下のように書き換える
public static IEnumerable<T> GetRandomN<T>(this IList<T> collection, int n)
{
    if (n > collection.Count)
    {
        throw new ArgumentOutOfRangeException("リストの要素数よりnが大きいです。");
    }

    var indexList = new List<int>(collection.Count);
    for (int p = 0; p < collection.Count; p++) indexList.Add(p);

    for (int i = 0; i < n; i++)
    {
        int index = UnityEngine.Random.Range(0, indexList.Count); // ★ここ
        int value = indexList[index];
        indexList.RemoveAt(index);
        yield return collection[value];
    }
}

関連記事

takap-tech.com

C#でDictionaryのキーに複数のキーを設定する

DictionaryのKeyに指定するオブジェクトを工夫することで複数のキーを指定できるようにしたいと思います。ただし、検索する見かける Tuple を使用した複数の値の組み合わせを Dictionary のキーに指定する方法はが見づらい & 値の意味が不明瞭化するためなるべく避けたいところです。

従って回はキーに複数の値を格納する自作クラスを作成して利用する方法を紹介したいと思います。

// Tuple をキーにする実装
Dictionary<(int, int), string> _table = new Dictionary<(int, int), string>();

// こうすると、、、

// インデックスアクセスが面倒
_table[(0, 1)] = "sample string.";

// メソッドが面倒
if(_table.ContainsKey((0, 1))) { ...

// 引数が面倒
public string Foo((int key1, int key2) key) { ...

// ★★★というか全体的に (int, int) が頻繁に出てくるが何を表してるのか意味が分からない

タプルはあくまで一時的に値を組み合わせるものであって、システムの長い期間存在するオブジェクトには使用するべきではありません。意味のある 2つの値が組み合わさった存在に、実質名前がついてない状態なのでこれがシステムの色々な場所で出現すると後から見たときに分かりにくくて追うのが大変になってしまいます。

Dictionaryがキーを識別する方法

その前に、Dictionaryのキーの識別方法、すなわちどのようにキーが同値かを判断するかはMSDNによると以下の通りです。

  1. Object.GetHashCode() で値が同じかどうか判定する
  2. 同じ場合 Object.Equals() 本当に同じかどうか判定する

このように2段階の確認で同じ場合同じ場所に値を格納するようになります。

従って

  1. オブジェクトに同じ値が設定されていたら同じハッシュ値を返す
  2. Equalsは内容が同じであればtrueを返す

という条件を持つ複数のフィールドからなるオブジェクトをキーに指定すれば複数の条件をキーに持たせることができたとえ言えます。

コード例

まず確認です。キーに用いるためのクラスを用意します。

// MyKey.cs

// Dictionaryのキーに使用するオブジェクト
public class MyKey
{
    public int Key1 { get; set; }
    public int Key2 { get; set; }
    public string Key3 { get; set; } // 何個かフィールドを持っている
    
    public override string ToString()
    {
        return $"{nameof(this.Key1)}={this.Key1}, " +
               $"{nameof(this.Key2)}={this.Key2}, " +
               $"{nameof(this.Key3)}={this.Key3}";
    }
}

上記クラスで次のコードを実行します。

// Program.cs
internal static void Main(string[] args)
{
    // 値を保持するテーブル
    var table = new Dictionary<MyKey, int>();

    MyKey latestKey = null;

    for (int i = 0; i < 5; i++)
    {
        var key = new MyKey()
        {
            Key1 = 0,
            Key2 = 1,
            Key3 = "a",
        };

        Console.WriteLine($"key.GetHashCode(), {key.Equals(latestKey)}");
        // > 43495525, False
        // > 55915408, False
        // > 33476626, False
        // > 32854180, False
        // > 27252167, False
        latestKey = key;
        table[key] = i;
    }
    
    foreach (var item in table)
    {
        Console.WriteLine($"[{item.Key}] => [{item.Value}]");
        // > [Key1=0, Key2=1, Key3=a] => [0]
        // > [Key1=0, Key2=1, Key3=a] => [1]
        // > [Key1=0, Key2=1, Key3=a] => [2]
        // > [Key1=0, Key2=1, Key3=a] => [3]
        // > [Key1=0, Key2=1, Key3=a] => [4]
    }
}

結果はコメントの通りですが、通常インスタンスが異なるとGetHashCodeが上記のようにひとつづつ異なっていて、Equalsもfalseを返します。Dictionaryには5つの値が格納されます。

そこで、MyKeyクラスを冒頭の条件に一致するよう、以下の通り変更します。

public class MyKey
{
    public int Key1 { get; set; }
    public int Key2 { get; set; }
    public string Key3 { get; set; }

    // ★★★同じ値で同じハッシュを返すコードを追加する
    public override int GetHashCode()
    {
        return this.Key1 ^ this.Key2 ^ this.Key3.GetHashCode();
    }

    // ★★★内容が同じであればtrueを返すコードを追加する
    public override bool Equals(object obj)
    {
        if (obj == null || !(obj is MyKey key))
        {
            return false;
        }

        return this.Key1 == key.Key1 &&
               this.Key2 == key.Key2 &&
               this.Key3 == key.Key3;
    }

    public override string ToString()
    {
        return $"{nameof(this.Key1)}={this.Key1}, " +
               $"{nameof(this.Key2)}={this.Key2}, " +
               $"{nameof(this.Key3)}={this.Key3}";
    }
}

この状態で、先ほどと同じコードを実行するとDictionaryの値の保持のされ方が変化します。

// Program.cs
internal static void Main(string[] args)
{
    // 値を保持するテーブル
    var table = new Dictionary<MyKey, int>();

    MyKey latestKey = null;

    for (int i = 0; i < 5; i++)
    {
        var key = new MyKey()
        {
            Key1 = 0,
            Key2 = 1,
            Key3 = "a",
        };

        Console.WriteLine($"{key.GetHashCode()}, {key.Equals(latestKey)}");
        table[key] = i;
        // > -231358651, False // 最初はノーカン
        // > -231358651, True
        // > -231358651, True
        // > -231358651, True
        // > -231358651, True
    }

    foreach (var item in table)
    {
        Console.WriteLine($"[{item.Key}] => [{item.Value}]");
        // [Key1=0, Key2=1, Key3=a] => [4]
        // 
        // ★★★ 全部同じキーなので最後に設定されたものが上書きされて
        // 1つのみテーブルに記録される
    }
}

同じ内容が設定される限り同じキーだとDictionaryに認識されるようになり、Dictionaryには一つのキーに(あと勝ち上書きになって)最後のオブジェクトだけが残っています。

これで、キーオブジェクトに複数の値を指定する = キーに複数の値を指定するのと実質同じことが実現できました。

関連記事

以下、2重 Dictionary をラップする管理クラスの実装例の紹介です。

takap-tech.com

以上です。