【C#】enumのメンバーに任意の番号を振る

enum は宣言した順に数字が自動で振られています。

以下の例だと Apple=0, Banana=1 といように自動でインクリメントされた数字が割り当てられます。なのでメンバーを途中に追加すると数値が変わってしまいます。

public enum Type
{
   Apple,      // 0
   Banana,     // 1
   Orange,     // 2
   Grape,      // 3
   Watermelon  // 4
}

// 途中に追加すると暗黙の数字がずれる
public enum Type
{
   Apple,      // 0
   Banana,     // 1
   Orange,     // 2
   Cherry,     // 3 ★追加
   Grape,      // 4
   Watermelon  // 5 後ろはずれてしまう
}

enum は途中にメンバーを追加しない、は、割とありますが任意で番号を与えることもできます。

// 各シンボルの数値を明示する
public enum Type
{
   Apple = 0,
   Banana = 1,
   Orange = 2,
   Cherry = 5,
   Grape = 3,
   Watermelon = 4 
}

// 明示しない場合+1される
public enum Type
{
   Apple = 0,
   Banana,     // 2 ★省略すると+1されていく
   Orange,     // 3
   Cherry = 5, // 5
   Grape = 3,  // 3
   Watermelon  // 4 ★自動的に3から+1される
}

一般的に

  • 基本的にメンバーを追加するときは末尾に追加していく
  • 途中にメンバーを追加しない

で、問題が発生する場合は

  • 途中に追加する場合、既存のメンバーの数値を変えないように値を明示して調整する

のような運用が考えられます。

関連リンク

takap-tech.com

【Unity】ExcelのデータをScriptableObjectに取り込む

Excel で管理しているゲームデータを自作のプログラムを使って ScriptableObject にインポートする方法を紹介したいと思います。

これ系は既にライブラリとかアセットが色々と配布されていて、それらを使用したほうが時間の節約になると思いますが、今回はを全て自作してみたいと思います。

ツール類は全て C# で作成し、Excel VBA や Python は使用しません。

確認環境

  • Windows11 + VisualStudio2022
  • Unity 2022.3.5f1
  • .NET 6(コンソールアプリ作成用
    • ExcelDataReader 3.6.0
    • ExcelDataReader.DataSet 3.6.0
    • Newtonsoft.Json 13.0.3

エディター上で動作を確認しています。

Excelからデータを取り込む流れ

Excel データを Unity に取り込む手順は以下の通りです。

  1. エクセルでデータを作成する
  2. Unity上でデータ構造を定義する
  3. エクセルを ExcelDataReader で JSON ファイルに変換する
  4. Unity上からJSONファイルを読み込む

実装手順

エクセルでデータを作成する

まず Excel でデータを作成します。見た目はこんな感じで Excel を作成しておきます。

保存先 D:\Master\ItemData.json

キー アイテム名 重さ 説明
Item01 アイテム1 0.01 1 1 アイテム1です
Item02 アイテム2 0.02 1 2 アイテム2です
Item03 アイテム3 0.03 2 1 アイテム3です
Item04 アイテム4 0.04 2 2 アイテム4です
Item05 アイテム5 0.05 1 1 アイテム5です
Item06 アイテム6 0.06 1 2 アイテム6です
Item07 アイテム7 0.07 2 1 アイテム7です
Item08 アイテム8 0.08 2 2 アイテム8です
Item09 アイテム9 0.09 1 1 アイテム9です

Unity上でデータ構造を定義する

Unity 上にエクセルデータに対応するデータ構造を ScriptableObject で実装します。

これをマスターデータとしてプロジェクトにアセットとして新規作成します。

// ItemMaster.cs

using System.Collections.Generic;
using System.Collections.ObjectModel;
using UnityEngine;

namespace Takap
{
    // マスターデータ保持用のオブジェクト
    [CreateAssetMenu(menuName = "MyData/ItemMaster")]
    public class ItemMaster : ScriptableObject
    {
        [SerializeField] List<ItemData> _itemList;
        public ReadOnlyCollection<ItemData> ItemList
            => new ReadOnlyCollection<ItemData>(_itemList);
    }
}

// ItemData.cs

using System;
using UnityEngine;

namespace Takap
{
    [Serializable]
    public class ItemData
    {
        [SerializeField] string _key = "";
        [SerializeField] string _name = "";
        [SerializeField] float _weight;
        [SerializeField] int _high;
        [SerializeField] int _width;
        [SerializeField] string _text;

        public string Key => _key;
        public string Name => _name;
        public float Weight => _weight;
        public int High => _high;
        public int Width => _width;
        public string Text => _text;

        public ItemData(string key, string name, float weight, int high, int width, string text)
        {
            _key = key;
            _name = name;
            _weight = weight;
            _high = high;
            _width = width;
            _text = text;
        }
    }
}

上記コードを追加するとプロジェクトのコンテキストメニューに以下のように MyData > ItemMaster という項目が追加されるのでそれを選択して、「ItemMaster」という名前でアセットを作成します。

作成すると以下のような状態になっていると思います。

ExcelをExcelDataReaderでJSONファイルに変換する

Excel を JSON に変換するために外部にコンソールアプリを作成します。このプログラムに Excel をドラッグ & ドロップするとJSONが指定したパスに出力されるようにします。

VisualStudio 上からコンソールアプリのプロジェクトを新規作成し .NET 6 を選択してコンソールアプリを新規作成します。

NuGet から以下の 3つをインストールしておきます。

  • ExcelDataReader
  • ExcelDataReader.DataSet
  • Newtonsoft.Json

そして、先ほど Unity 上に実装した ItemData.cs の内容をコンソールアプリ内にコピーし、コンソールアプリを以下のように実装します。

注意:1点制限があって ItemData クラスの読み書きしたい対象に JsonProperty 属性を追加しています。

using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Text;
using ExcelDataReader;
using Newtonsoft.Json;

namespace MasterDataToJson
{
    internal class Program
    {
        static readonly ExcelToJson _excelToJson = new();

        static void Main(string[] args)
        {
            // パスを引数で受け取る
            string excelFilePath = args[0];
            _excelToJson.ExecExcelToJson(excelFilePath);
        }
    }

    // ExcelをJSONに変換するためのクラス
    public class ExcelToJson
    {
        static readonly JsonSerializer _jsonSerializer = new();

        public void ExecExcelToJson(string filePath)
        {
            using FileStream stream = 
                    File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);

            // 拡張子がxlsx(新しいほう)のファイルの読み取りを指定
            // 古いxlsはCreateBinaryReaderメソッドを使う
            using IExcelDataReader reader = 
                    ExcelReaderFactory.CreateOpenXmlReader(stream, JpEncode());

            DataTable sheet = reader.AsDataSet().Tables[0];
            ItemData[] data = ReadContents(sheet).ToArray(); // オブジェクトに変換
            foreach (var item in data)
            {
                Console.WriteLine($"{item.Key}");
            }

            using TextWriter tw = new StreamWriter(GetOutputPath(sheet), false, Encoding.UTF8);
            _jsonSerializer.Serialize(tw, data);
        }

        // エンコードの設定を追加
        private static ExcelReaderConfiguration JpEncode()
        {
            Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
            return new() { FallbackEncoding = Encoding.GetEncoding("Shift_JIS") };
        }

        // 出力先パスの取得
        private string GetOutputPath(DataTable sheet) => sheet.Rows[0][1].ToString();

        // Excelのシートの内容をC#のオブジェクトに変換する
        private IEnumerable<ItemData> ReadContents(DataTable sheet)
        {
            for (int i = 3; i < sheet.Rows.Count; i++) // ヘッダーは読み飛ばす
            {
                DataRow row = sheet.Rows[i];
                yield return new ItemData(
                    row[0].ToString(),
                    row[1].ToString(),
                    float.Parse(row[2].ToString()),
                    int.Parse(row[3].ToString()),
                    int.Parse(row[4].ToString()),
                    row[5].ToString()
                    );
                    
            }
        }
    }

    // コピーしてきたクラス
    [Serializable]
    public class ItemData
    {
        // ★読みたい対象にJsonPropertyを追加する
        [SerializeField, JsonProperty] string _key = "";
        [SerializeField, JsonProperty] string _name = "";
        [SerializeField, JsonProperty] float _weight;
        [SerializeField, JsonProperty] int _high;
        [SerializeField, JsonProperty] int _width;
        [SerializeField, JsonProperty] string _text;

        public string Key => _key;
        public string Name => _name;
        public float Weight => _weight;
        public int High => _high;
        public int Width => _width;
        public string Text => _text;

        public ItemData(string key, string name, float weight, int high, int width, string text)
        {
            _key = key;
            _name = name;
            _weight = weight;
            _high = high;
            _width = width;
            _text = text;
        }
    }

    // Unity外だと存在しないので名前だけ追加しておく
    public class SerializeFieldAttribute : Attribute { }
}

このプログラムをコンパイルして、Excel ファイルをこのプログラムにドラッグ & ドロップすると Excel 内で指定した位置にファイルが出力されます。

中身は整形するとこんな感じになってます(余計な要素も出力されていますが参照しないのでこのまま使用します)

Unity上からJSONファイルを読み込む

最後に JSON をUnity 上から読み取るための処理を実装します。

最初に作成した ScriptableObject をまず以下のように修正します。

// ItemMaster.cs

using System.Collections.Generic;
using System.Collections.ObjectModel;
using Takap.Utility;
using UnityEngine;

namespace Takap
{
    [CreateAssetMenu(menuName = "MyData/ItemMaster")]
    public class ItemMaster : ScriptableObject
    {
        [SerializeField] List<ItemData> _itemList;
        public ReadOnlyCollection<ItemData> ItemList
            => new ReadOnlyCollection<ItemData>(_itemList);

        // ★★★追加ここから --->
        [SerializeField] string _filePath;
        public void ReadJson()
        {
            if (string.IsNullOrEmpty(_filePath) || !System.IO.File.Exists(_filePath))
            {
                Debug.Log("JSONファイルが見つかりません。");
                return;
            }
            // 中身を読み取って処理用のメソッドに渡す
            string json = System.IO.File.ReadAllText(_filePath);
            ReadJson(json);
        }

        // JSON文字列を受け取ってScriptableObjectを更新する
        public void ReadJson(string json)
        {
            ItemData[] items = JsonHelper.FromJson<ItemData>(json);
            _itemList.Clear();
            _itemList.AddRange(items);

#if UNITY_EDITOR
            UnityEditor.EditorUtility.SetDirty(this);
            UnityEditor.AssetDatabase.SaveAssets();
#endif
            Debug.Log("インポートが完了しました。");
        }
        // <--- ★★★追加ここまで
    }
}

JsonHelper.FromJson はメソッドですがルート要素が配列のデータを扱うためのライブラリで以下のリンクから取得して記事中のコードをプロジェクトに取り込んでおいてください。

takap-tech.com

次に、ScriptableObject に Editor 拡張を追加して読み取りボタンを追加して読み取りを実行します。

// ItemMasterEditor.cs

#if UNITY_EDITOR

using UnityEditor;
using UnityEngine;

namespace Takap
{
    [CustomEditor(typeof(ItemMaster))]
    public class ItemMasterEditor : UnityEditor.Editor
    {
        public override void OnInspectorGUI()
        {
            // 通常のインスペクターを表示
            DrawDefaultInspector();

            // 対象のクラスのインスタンスを取得
            ItemMaster targetClass = (ItemMaster)target;

            // ボタンを追加
            if (GUILayout.Button("JSONの読み取り"))
            {
                targetClass.ReadJson();
            }
        }
    }
}
#endif

れこをビルドすると以下のようにJSONの読み取り実行ボタンと FilePath が表示されるので FilePath に Excel に入力したファイルパスを指定します。

これで実行を押すとコンソールに「インポートが完了しました」というメッセージが表示されて、ItemMaster が以下の通り更新されていると思います。

これで Excel のデータを ScriptableObject に取り込むことができました。

この ItemMaster をインスペクターから GameObject に設定するなどしてゲーム中で使用することができます。

記事中の実装はサンプルなので機能がチープです。各自で使いやすいように機能は追加していきましょう。

最後に

この記事で紹介した内容は基本的な処理の流れのサンプルとなります。アイデア次第でもっと便利にすることもできると思います。

例えば、Excel も Unity 内に格納して処理を Unity 内で完結するようにしたり、Excel の更新を監視し、変更があったら JSON ファイルを生成してGit に Push まで自動でする事もできると思います。

JSON さえあればそのデータを ScriptableObject に取り込めるのでデータの管理は JSON にエクスポートさえできれば Excel 以外でも扱えます。Editor の実行前に変更があったらマスターを更新して実行なども考えられると思います。

全て自作は、配布されているツールの他人が考えた仕組みを覚える必要が無くて、ツールでは対応できないワークフローやデータ形式も柔軟に対応することができるのがよい所だと思います。

各自で機能を追加してどんどん便利にしていきましょう。

【C#】正規表現で小数文字列をマッチする

小数文字列は書き方が複数ありますが、それらを正規表現でマッチさせたりマッチした文字列を取り出す方法を紹介したいと思います。

小数の表記方法の確認

小数は以下のように表現されます。

  • 123.456 / +123.456
  • -123.456
  • .456 / +.456
  • -.456
  • 1.23e5 / 1.23E5 / 1.23e+5 / 1.23e-5 / 1.23E05

ルールとして

  • 通常のルール
    • 整数部分が無く小数点から始まるときがある
    • 先頭にプラスもしくはマイナスが付くときがある
    • 上記2つのルールは共存する
    • 整数部が指定されたら小数部は省略可能、小数部があれば整数は省略可能
    • 省略表記は数値 + ドット もしくは ドット + 数値となり両方省略(ドットのみ)は不可
  • 指数表記のルール
    • e は大文字/小文字両方使用できる
    • e の後ろには符号(+-)が付くときと付かない時がある
    • eが指定されたら後ろには必ず有効な数値が指定される

が挙げられます。

正規表現の実装

C#実装としての正規表現としては以下の通りです。

// 小数をマッチするための正規表現
string pattern = @"^[+-]?(\d+.\d*|\d*.\d+)([eE][+-]?\d+)?$";

// --- 部分ごとの説明 ---

// ★(1)先頭の符号の指定
[+-]?

// ★(2)数値の指定
// 123. とか .456 のような形式を想定
(\d+.\d*|\d*.\d+)

// ★(3)指数の指定
([eE][+-]?\d+)?

C# で実装すると以下のようになります。

const string Pattern = @"^[+-]?(\d+.\d*|\d*.\d+)([eE][+-]?\d+)?$";
static string[] values = new string[]
{
    // 通常の数値
    "123.456",
    "+123.456",
    "-123.456",
    "123.",
    "+123.",
    ".456",
    "-.456",
    // 指数表記
    "123.456e12",
    "123.456E12",
    "123.456e-12",
    "123.456e012",
    "123.456e-012",
    "+123.456e12",
    "-123.456e12",
    "123.e12",
    "+123.e12",
    ".456e12",
    "-.456e12",
    // 一致しない
    ".",
    "+a.12",
    "123.456f0-1"
};

private static void Check()
{
    foreach (var value in values)
    {
        bool isMatch = Regex.IsMatch(value, Pattern);
        Console.WriteLine($"{isMatch},{value}");
        // True,123.456
        // True,+123.456
        // True,-123.456
        // True,123.
        // True,+123.
        // True,.456
        // True,-.456
        // True,123.456e12
        // True,123.456E12
        // True,123.456e-12
        // True,123.456e012
        // True,123.456e-012
        // True,+123.456e12
        // True,-123.456e12
        // True,123.e12
        // True,+123.e12
        // True,.456e12
        // True,-.456e12
        // False,.
        // False,+a.12
        // False,123.456f0-1
    }
}

部分的に切り出して先頭から(^)末尾まで($)で一致させていますがこれを省略すると "+a.12" や "123.456f0-1" も一致判定となりますが、実際にマッチしている部分は ".12" と "123.456"となります。

この、一致している小数部分を取り出す方法は以下の通りです。

// 小数の部分をグループ化して取り出す文字列
const string Pattern = @"(?<GRP>[+-]?(\d+.\d*|\d*.\d+)([eE][+-]?\d+)?)";

// 検査対象の文字列リスト
static string[] values = new string[]
{
    //...省略...
};

// チェック実行
private static void Check()
{
    foreach (var value in values)
    {
        Match m = Regex.Match(value, Pattern);
        if (m.Success)
        {
            string matchString = m.Groups["GRP"].Value;
            Console.WriteLine($"\"{value}\" -> {matchString}");
        }
        else
        {
            Console.WriteLine($"{value} is not match");
        }
        > "123.456"  -> 123.456
        > "+123.456" -> +123.456
        > "-123.456" -> -123.456
        > "123."  -> 123.
        > "+123." -> +123.
        > ".456"  -> .456
        > "-.456" -> -.456
        > "123.456e12"   -> 123.456e12
        > "123.456E12"   -> 123.456E12
        > "123.456e-12"  -> 123.456e-12
        > "123.456e012"  -> 123.456e012
        > "123.456e-012" -> 123.456e-012
        > "+123.456e12"  -> +123.456e12
        > "-123.456e12"  -> -123.456e12
        > "123.e12"  -> 123.e12
        > "+123.e12" -> +123.e12
        > ".456e12"  -> .456e12
        > "-.456e12" -> -.456e12
        > . is not match
        > "+a.12"       -> .12
        > "123.456f0-1" -> 123.456
    }
}

この取り出した文字列を double.Parse(matchString) とすると浮動小数として取得できます。

【C#】インスタンス作成速度の比較

C# で数通りあるインスタンス作成方法の処理速度の比較です。

前提条件・環境

環境は以下の通り

  • Visual Studio 2019 + .NET Core 3.1
  • Windows10, Releaseビルドをコンソールから実行

動作検証

検証は BenchmarkDotNet を使用して検証は以下の通り実行します。

// Program.cs

internal class Program
{
    private static void Main(string[] args)
    {
        var summary = BenchmarkRunner.Run<CreateInstance1>();
        // もしくは以下を使う
        //var summary = BenchmarkRunner.Run<CreateInstance2>();
    }
}

試験に使用するクラスの定義は以下の通りです。

// MyClass.cs

public class MyClass
{
    string _str;
    int _value;
    public MyClass() { }
    public MyClass(string str, int value) => (_str, _value) = (str, value);
    public override string ToString() => $"{_str}={_value}";
}

引数無しのインスタンス作成

引数無しの場合以下の方法でインスタンスを作成できます。

  • Case1: newして作成
  • Case2: ジェネリックで作成
  • Case3: Activatorで作成(ジェネリックを使用)
  • Case4: Activatorで作成(Type渡しを使用)

検証コード

[MemoryDiagnoser]
[RankColumn]
public class CreateInstance1
{
    [Benchmark]
    public void Case1()
    {
        _list1.Clear();
        for (int i = 0; i < 1000; i++)
        {
            var obj = new MyClass();
            _list1.Add(obj);
        }
    }
    List<MyClass> _list1 = new List<MyClass>();

    [Benchmark]
    public void Case2()
    {
        _list2.Clear();
        for (int i = 0; i < 1000; i++)
        {
            var obj = Create<MyClass>();
            _list2.Add(obj);
        }
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static T Create<T>() where T : class, new() => new T();
    List<MyClass> _list2 = new List<MyClass>();

    [Benchmark]
    public void Case3()
    {
        _list3.Clear();
        for (int i = 0; i < 1000; i++)
        {
            var obj = Activator.CreateInstance<MyClass>();
            _list3.Add(obj);
        }
    }
    List<MyClass> _list3 = new List<MyClass>();

    [Benchmark]
    public void Case4()
    {
        _list4.Clear();
        for (int i = 0; i < 1000; i++)
        {
            var obj = (MyClass)Activator.CreateInstance(_type);
            _list4.Add(obj);
        }
    }
    Type _type = typeof(MyClass);
    List<MyClass> _list4 = new List<MyClass>();
}

実行結果

| Method |      Mean |     Error |    StdDev | Rank |   Gen0 |   Gen1 | Allocated |
|------- |----------:|----------:|----------:|-----:|-------:|-------:|----------:|
|  Case1 |  5.246 us | 0.0726 us | 0.0607 us |    1 | 7.6447 | 0.0076 |  31.25 KB |
|  Case2 | 31.587 us | 0.2984 us | 0.2646 us |    2 | 7.6294 |      - |  31.25 KB |
|  Case3 | 31.233 us | 0.4671 us | 0.4369 us |    2 | 7.6294 |      - |  31.25 KB |
|  Case4 | 33.266 us | 0.5065 us | 0.4737 us |    3 | 7.6294 | 0.0305 |  31.25 KB |
  • Case2~4 Case1に比べて6倍遅い
  • Case4 は Case2,3 に比べて微妙に遅い

引数ありのインスタンス作成

引数ありの場合はジェネリックが使用できないので以下の 2通りのインスタンス作成方法があります。

  • newして作成
  • Activatorで作成

検証コード

[MemoryDiagnoser]
[RankColumn]
public class CreateInstance2
{
    [Benchmark]
    public void Case1()
    {
        _list1.Clear();
        string str = "asdf";
        int value = 100;
        for (int i = 0; i < 1000; i++)
        {
            var obj = new MyClass(str, value);
            _list1.Add(obj);
        }
    }
    List<MyClass> _list1 = new List<MyClass>();

    [Benchmark]
    public void Case2()
    {
        _list2.Clear();
        string str = "asdf";
        int value = 100;
        for (int i = 0; i < 1000; i++)
        {
            var obj = (MyClass)Activator.CreateInstance(_type, str, value);
            _list2.Add(obj);
        }
    }
    Type _type = typeof(MyClass);
    List<MyClass> _list2 = new List<MyClass>();
}

実行結果

| Method |       Mean |     Error |    StdDev | Rank |    Gen0 |   Gen1 | Allocated |
|------- |-----------:|----------:|----------:|-----:|--------:|-------:|----------:|
|  Case1 |   6.012 us | 0.1196 us | 0.2575 us |    1 |  1.9073 | 0.0992 |  31.25 KB |
|  Case2 | 311.745 us | 5.9345 us | 6.5962 us |    2 | 26.3672 | 1.4648 |  437.5 KB |
  • Case1 と 2で50倍以上速度差がある

かなり昔から Activator.CreateInstanceは 遅いと言われていました、実際かなり遅いので通常は使用することはないと思います。

引数無しのほうはまぁまぁの速度でしたが引数ありの CreateInstance はかなり厳しいですね。

なぜか Allocated の項目が引数なしに比べて 15倍も増えてます。

【C#】Windowsで使用できるファイル名かチェックする

C# で Windows 上で使用できるファイル名かどうかをチェックする実装です。

using System.IO;
using System.Text.RegularExpressions;

public class WindowsFileSystem
{
    // 使用禁止文字
    static readonly char[] invalidChars = Path.GetInvalidFileNameChars();

    // 指定したファイルパスが使用できるかどうか?
    // true: 使える / false: 使えない
    public bool CanUseFilePath(string path)
    {
        if (string.IsNullOrWhiteSpace(path)) return false;

        // 推定ファイル名の部分を取得する
        int index = path.LastIndexOf('\\');
        string word = index == -1 ? path : path.Substring(index + 1, path.Length - index - 1);
        return CanUseFileName(word);
    }

    // 指定した文字列がファイル名 or フォルダ名に使用できるかどうか?
    // true: 使える / false: 使えない
    public bool CanUseFileName(string name)
    {
        if (string.IsNullOrWhiteSpace(name)) return false;

        // 拡張子より前を選択
        int index = name.IndexOf('.');
        string target = index == -1 ? name : name.Substring(0, index);

        // 対象に不正な文字が含まれているか?
        foreach (char c in target)
        {
            foreach (var p in invalidChars)
            {
                if (c == p) return false;
            }
        }

        // 予約語が含まれていないか?
        return !Regex.IsMatch(target,
            @"^((COM|LPT)[1-9]|CON|PRN|AUX|NUL)$", RegexOptions.IgnoreCase);
    }
}

長さについてはチェックしていません。

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

private static void Main(string[] args)
{
    WindowsFileSystem windows = new();
    bool isOK = windows.CanUseFilePath(@"d:\hoge\sample.txt");
    if (isOK)
    {
        // 使える
    }
    else
    {
        // 使えない
    }
}

【C#】NuGetパッケージを再インストールする

環境

  • Windows10
  • VisualStudio 2019

状況

以下でパッケージのキャッシュをクリアしたらエラーが出て「発行」できなくなった。

ツール > オプション > NuGet パッケージ マネージャー > すべての NuGet キャッシュをクリア

次に、Webプロジェクトを発行すると以下エラーが出て完了しない。

warning MSB3106
アセンブリの厳密な名前 "${DLLのパス}"
は、見つからなかったパスであるか、形式が正しくない完全アセンブリ名です。

error MSB3030:
ファイル "${DLLのパス}"
は見つからなかったためコピーできません。

手順(1)

以下手順でパッケージを復元する。

  • VisualStudio を閉じる
  • 隠しファイルを表示 > .vs ファイルを削除
  • VisualStudio を立ち上げてパッケージマネージャーコンソールを表示
// 以下を入力する
update-package -reinstall

PC のディスクの容量が少なくなったからキャッシュ削除したら動かなくなって焦りました。再インストールは IDE の NuGet パッケージの管理 からは実行できないのでパッケージマネージャーコンソールからコマンドを入力して実行する必要があります。

手順(2)

以下フォルダにパッケージが見つからないといわれる場合はこれかもしれません。

%userprofile%\.nuget\packages\${ライブラリ名}\${バージョン}
  • 新しいソシューションとプロジェクトを .NET Frameowrk 4.7.2 とか 4.8 で作成
  • NuGet パッケージマネージャーを平出 ${ライブラリ名}\${バージョン} を選択しインストール

これでキャッシュフォルダにパッケージが配置されます。既存のプロジェクトで既に別のバージョンをダウンロード済みだとうまくいかないかもしれません。

プロジェクトは .NET Core や .NET だとではなく .NET Framework を選択したほうがよさそう。

【C#】BenchmarkDotNetを使って処理時間を計測する方法

C# の処理の実行時間の測定やメモリ使用量、GCの量を正確に計測できるライブラリ「BenchmarkDotNet」の最速セットアップ & 使用方法の紹介したいと思います。実行時間を測定するために Stopwatch クラスを使用する方法もありますが、このライブラリを使ったほうがより正確に実行時間を計測できます。

確認環境

  • VisualStudio 2022
  • .NET6
  • Windows11

VisualStudio の IDE 上で操作しています。

セットアップ手順

手順は概ね以下の通りです。

  1. NuGet から BenchmarkDotNet をインストールする
  2. テストしたい処理を書く、クラス、メソッドに属性を付ける
  3. Release ビルドで実行する

で、IDE 上から Release ビルドしてそのまま実行してもいいですし、いったん Relese フォルダ内の exe をコンソールから実行してもいいです。コンソールから実行したほうが計測が安定すると思います。

計測コードの書き方

using BenchmarkDotNet.Running; // 追加する

internal class Program
{
    private static void Main(string[] args)
    {
        BenchmarkRunner.Run<Test>(); // メインにテストしたいクラスを指定する
    }
}

// ★計測対象のクラスに追加する
[MemoryDiagnoser]
[RankColumn]
public class Test
{
    // ★事前のデータ初期化など最初のセットアップ
    [GlobalSetup]
    public void Setup()
    {
        // フィールド変数の初期化とかリストデータ作成etc...
    }

    // ★対象のメソッドに属性を追加する
    [Benchmark]
    public void Case1()
    {
        // 計測したい実装を書く
    }

    [Benchmark]
    public void Case2()
    {
        // 計測したい実装を書く
    }
}

各属性の説明です。

  • MemoryDiagnoser:結果にメモリ使用量の統計を出力するように指定します。
  • RankColumn:結果に処理速度の順位を出力するように指定します。
  • Benchmark:計測したいメソッドに追加します。

これで実行すると、以下のように結果がサマリーで表示されます。

| Method |     Mean |    Error |   StdDev | Rank |   Gen0 | Allocated |
|------- |---------:|---------:|---------:|-----:|-------:|----------:|
|  Case1 | 17.31 us | 0.083 us | 0.074 us |    1 | 1.3733 |   5.62 KB |
|  Case2 | 18.73 us | 0.108 us | 0.101 us |    2 | 2.1973 |   9.01 KB |

結果の見方は以下の通りです。

* Method:計測対象のメソッド名
* Mean:計測対象のメソッドの処理時間
* Error:99%の確率で母平均が含まれるような範囲
* StdDev:すべての測定値の標準偏差です。平均値からのデータの散らばりの程度
* Rank:Mean の順位
* Gen0:1000回処理ごとのGC(世代0)の回収数(オブジェクトの数?)
* Allocated:一回ごとのメモリ割り当て量

結局色々オプションがあっても、Mean と Allocated くらいしか見ないのでこれで使い方は十分だと思います。

よく、Benchmark のみ指定して計測している方法がネットで見られますが MemoryDiagnoser は付けておいたほうが良いです。通常は確認しにくい、メモリ使用量の項目が出るようになります。

【C#】ExceptionのMessageを後から変更する

C# でいちど作成した Exception の Message プロパティは get のみで後からメッセージを変更することができません。

今回は一度作成した Exception オブジェクトの Message を変更したいと思います。

確認環境

  • VisualStudio 2022
  • .NET 6
  • IDE上からDebug実行で確認

実装の説明

リフレクションを使って Exception オブジェクト内のフィールドを書き換えます。

public static void ChangeMessage(Exception ex, string newMessage)
{
    BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Instance;
    FieldInfo fi = ex.GetType().GetField("_message", flags);
    fi.SetValue(ex, newMessage); // これで書き換わる
}

Message プロパティが内部的に private のフィールド変数 _message を使用しているためこれを強制的に書き換えています。

で、何でこんな事してるかというと、ジェネリックで例外オブジェクトを作成しようとするとあれ、設定できなくないと思ってこんなことしていました。

public static T CreateException<T>(string message) where T : MyException, new()
{
    var ex = new T(); // 引数が指定できない
    BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Instance;
    var fieldInfo = typeof(Exception).GetField("_message", flags);
    fieldInfo.SetValue(ex, message); // これで設定する
    return ex;
}

で、最近まで思ってましたが Activator.CreateInstance を使用すればこんなことしないで済むみたいです。あらら。

public static T CreateException<T>(string message) where T : MyException, new()
{
    return (T)Activator.CreateInstance(typeof(T), message);
}

【C#】オブジェクトにクラスが継承されているかどうかを調べる

あるオブジェクトに特定のクラスやインターフェースが継承されているかどうかを調べる処理方法は以下の通りです。

public static bool IsInherited1<T>(object obj) where T : class
{
    return (obj as T) != null; // ★(1) asで変換できるかどうか調べる
}

public static bool IsInherited2<T>(object obj) where T : class
{
    return obj is T; // ★(2) is演算子でチェックする
}

public static bool IsInherited3<T>(object obj) where T : class
{
    return typeof(T).IsAssignableFrom(obj.GetType()); // ★(3) 型情報を用いてチェックする
}

確認コード

上記の確認用コードは以下の通り

// クラスの定義
public interface IBase { }
public class Base : IBase { }
public class DerivedA : Base { }
public class DerivedB : DerivedA { }
public class Sample { } // これだけ関係ない

private static void Main(string[] args)
{
    DerivedB obj = new DerivedB();

    // asで変換できるかどうか調べる
    bool a = IsInherited1<IBase>(obj);
    bool b = IsInherited1<Base>(obj);
    bool c = IsInherited1<DerivedA>(obj);
    bool d = IsInherited1<Sample>(obj);
    Console.WriteLine($"a={a}, b={b}, c={c}, d={d}");
    //> a=True, b=True, c=True, d=False

    // is演算子でチェックする
    a = IsInherited2<IBase>(obj);
    b = IsInherited2<Base>(obj);
    c = IsInherited2<DerivedA>(obj);
    d = IsInherited2<Sample>(obj);
    Console.WriteLine($"a={a}, b={b}, c={c}, d={d}");
    //> a=True, b=True, c=True, d=False

    // 型情報を用いてチェックする
    a = IsInherited3<IBase>(obj);
    b = IsInherited3<Base>(obj);
    c = IsInherited3<DerivedA>(obj);
    d = IsInherited3<Sample>(obj);
    Console.WriteLine($"a={a}, b={b}, c={c}, d={d}");
    //> a=True, b=True, c=True, d=False
}

また、変換できるか確認して変換結果を得たい時は以下のように実装します。

// 変換できる場合trueが帰り結果がresultに格納される
public static bool TryIsInherited<T>(object obj, out T result) where T : class
{
    return (result = (obj as T)) != null;
}

【C#】ジェネリックが推論できないコード2選

C# の型推論は大抵の場合型を推定してくれてジェネリックの型も推論で省略することができます。

特にジェネリックパラメータを推論によって省略できるため、メソッド使用時に型を書く手間がなくなり、コードの見た目もかなりすっきりできます。が、ごく一部で推論ができないケースがあるのでその紹介になります。

以下の2つの例は、言語仕様でどうしてもジェネリックの型を明示しないといけません。回避できないのであきらめましょう。

// ★★(1) 戻り値からは推論できない
int result = Sample.Foo();
// CS0411
// メソッド 'Sample.Foo<T>()' の型引数を使い方から推論することはできません。
// 型引数を明示的に指定してください。
//
// int result = Sample.Foo<int>(); ← 明示する必要がある
//

// ★★(2) 引数中のジェネリックなデリゲートからは推論できない
int a = 10;
Sample.Foo(value => Console.WriteLine(value));
// CS0411
// 'Sample.Foo<T>(Action<T>)' の型引数を使い方から推論することはできません。
// 型引数を明示的に指定してください。
//
// Sample.Foo<int>(value => Console.WriteLine(value)); ← 明示する必要がある。
// 
// 但し、事前に型に入れておけばエラーは出ない(記述が長くなるので無駄)
// Action<int> action = value => Console.WriteLine(value);
// Sample.Foo(action);
// 

【Unity】デリゲートを指定するときにGCAllocを減らす

あるメソッドの引数にデリゲートが必要な場合、GC Allocationを減らす方法の紹介です。

以下状況での GC Alloc が発生する話になります。

public static class SampleUtility
{
    // デリゲートを引数に取るメソッド
    public int Execute(Action<int> method) { ... }
}

public class MyObject : MonoBehaviour
{
    private void Update()
    {
        // ★GCAllocが発生しない(初回のみ発生)
        SampleUtility.Execute(StaticMethod);

        // ★GCAllocが発生!!!(毎回発生する)
        SampleUtility.Execute(InstanceMethod);
    }

    // 引数で渡すメソッド(1)
    private static void StaticMethod(int value) { ... }

    // 引数で渡すメソッド(2)
    private static void InstanceMethod(int value) { ... }
}

上記の例のコメントの通り

  • インスタンスメソッドを渡すと毎回GC Alloc発生する
  • static メソッドの時はGC Allocが発生しない

となります。

インスタンスメソッドの時だけ GCAlloc が常に発生します。

確認環境

  • Unity 2021.3.24f1

エディター上でプロファイラーを起動して確認しています。

本記事は、Unity を意識して書いていますが通常の .NET であれば共通の問題なのでサーバーサイドやデスクトップでも有効なアプローチです。

解決方法

先にGCAllocを発生させない方法です。

この問題は、スマートな方法が存在しないため回避するコードを自分で記述する必要があります。メソッドの引数にインスタンスメソッドを渡す場合以下のように記述します。

public class MyObject : MonoBehaviour
{
    // ★追加、インスタンスメソッドを保持するための変数
    Action<int> _method;

    private void Update()
    {
        // ★以下の通り書き換える
        SampleUtility.Execute(_method ??= InstanceMethod);
        //  → GCAllocは2回目以降発生しなくなる
    }

    // 引数で渡すメソッド
    private static void InstanceMethod(int value) { ... }
}

この修正で、初回の呼び出し時にデリゲートが生成され、2回目以降の呼び出しでは生成されたデリゲートが再利用されるため、GCAllocが発生しなくなります。

GCAllocが発生する理由

なぜインスタンスメソッドを渡したときだけ、GCAlloc が発生するかですが、最初の例題では概ね以下のようにコードが展開されます。

public class MyObject : MonoBehaviour
{
    // ★自動的にこのクラスが追加される
    private static class AutomaticGeneration
    {
        public static Action<int> __StaticMethod;
    }

    private void Update()
    {
        SampleUtility.Execute(StaticMethod);
        //
        // こんな感じに展開される↓↓↓↓
        //
        SampleUtility.Execute(AutomaticGeneration.__StaticMethod ??
            (AutomaticGeneration.__StaticMethod = new Action<int>(StaticMethod));

        SampleUtility.Execute(InstanceMethod);
        //
        // こんな感じに展開される↓↓↓↓
        //
        SampleUtility.Execute(new Action<int>(InstanceMethod)); // ★知らないうちにnewされてる
    }

    // 引数で渡すメソッド(1)
    private static void StaticMethod(int value) { ... }

    // 引数で渡すメソッド(2)
    private static void InstanceMethod(int value) { ... }
}

static メソッドのほうはコンパイラーが気を利かせてコードを追加して複数回の new を抑制するようになってますが、インスタンスメソッドのほうはそういった展開がされずに毎回デリゲートを new しています。

これが GCAlloc の正体です。なので、インスタンスメソッドの時はコンパイラーが追加しているコードを真似して実装を自分で追加します。

Action<int> _method;

private void Update()
{
    // こう書くのは面倒なので...
    SampleUtility.Execute(_method ?? (_method = new Action<int>(Foo)));

    // このように記述する(結局意味は同じです)
    SampleUtility.Execute(_method ??= new Action<int>(Foo));
}

そのうち気の利いたコードをコンパイラーが生成するようになればこの実装は不要ですが現状このように実装を状況に応じて追記が必要です。

ちなみにラムダ式で包むと効果があるとかありますが、以下のように展開されるので意味ありません(static メソッドは自動的にコード保管されてGCAlloc発生しない、インスタンスメソッドは意味ないとなります)

private void Update()
{
    SampleUtility.Execute(v => Foo(v));
    ↓
    // 結局こうやって展開されて毎回Actionがnewされる
    SampleUtility.Execute(new Action<int>(...));
}

まー、実際 Unity ではインクリメンタルGCを有効化していればこの例程度の実装が多少あって毎フレーム処理があってもほぼ問題起きないです。デスクトップ環境やサーバーサイドだとそもそも存在を認識するほど問題が表面化しません。気になる人は対応しておきましょう。

VisualStudioとC#バージョンの関係性

だんだん状況が変わってきたのでメモ。

2023年3月現在、.NET のバージョンと C#、VisualStudio の対応関係は以下のような関係性になっています。

.NET ver. C# ver. VisualStudio
.NET 7 C# 11 2022
.NET 6 C# 10 2022
.NET 5 C# 9.0 2022, 2019
.NET Core 3.x C#8.0 2022, 2019
.NET Core 2.x C#7.3 2022, 2019, 2017
.NET Framework C#7.3 2022, 2019, 2017, 2015

.NET Framework 以外の .NET 2.x~ 5 はもう軒並みサポート切れてるため、実質安定して使用できるのは .NET6 + C#10 + VisualStudio2022 の組み合わせのみ。

Visual Studio ver. サポート終了日
Visual Studio 2022 n/a
Visual Studio 2019 2029年04月
Visual Studio 2017 2027年04月
Visual Studio 2015 2025年10月

つまり今開発でサポート期間内で使える組み合わせは以下の通り

  • .NET Framework 4.6.2以降 + VisaulStudio 2015
  • .NET Framework 4.6.2以降 + VisaulStudio 2017
  • .NET Framework 4.6.2以降 + VisaulStudio 2019
  • .NET Framework 4.6.2以降 + VisaulStudio 2022
  • .NET 6 + VisaulStudio 2022

ちなみに、以下環境はそろそろやばいので、もし仕事で使ってるなら来年度の予算取りしたほうがいいです。

  • VisualStudio 2015, 2017 はそろそろ 2019 or 2022 に乗り換えるべき
  • .NET Framework 4.6.2 はそろそろ 4.7.2 以降に乗り換えるべき

.NET Core, .NET 系の新しい環境はどんどんライフサイクルが高速化してどんどん進化する、一方で古い環境はそのまま温存されて残るという二極化が進んでいるイメージです。