【Unity】JSONのシリアライズ・デシリアライズの性能比較

Unity環境で使用できる JSON をシリアライズ・デシリアライズできるライブラリとパフォーマンスを調査したいと思います。

はじめに

実際はこのネタ使い古しなので先人よるパフォーマンス計測はいくらでもされているので実際は以下サイトを参考にしたほうが効率がいいと思います…

https://developpaper.com/unity-json-performance-comparison-litjson-newton-softjson-simplejson/

https://github.com/neuecc/Utf8Json

Unity で使用できるJSONライブラリ

標準機能を合わせて以下の4つが有力かと思います。

というか クラス ⇔ JSON を簡単に直接変換できる仕組みを検証します。SimpleJSONはJSONオブジェクト型にパースがされるので高速ですがここでは除外します。だって面倒なんだもん…、そしてutf8jsonはパフォーマンスは素晴らしいですが IL2CPP 環境下で問題が出た時に責任取れないのでここでは計測除外します。扱えるようになったらいつか計測してみたいかなと。

種類 説明
JSON Utility Unity が提供するライブラリ。軽い代わりに制約多数。
DataContractJsonSerializer .NET 標準でかなり昔から使える
System.Text.Json .NET Standard2.1~なので2020以降で使用可能
JSON.NET .NET で有力な 3rd 製ライブラリで機能が豊富

JSON Utility はパフォーマンスが良いのですが、Dictionary 型をシリアル化できない、リストを直接シリアル化できないなどなどほかにもいっぱい色々あるのですが制約が多いです(おそらくサポート範囲がSerializableでインスペクター上で扱えるルール以外全部仕様を落としているという感じです)そこでほかの一般的なライブラリを使用しようとすると制約がない代わりに処理が重いなど一長一短があるようです。そのための参考として比較を行いたいと思います。

System.Text.Json は割と軽くて使いやすいですが現行の Unity の環境で2020を使用している人が多分ほぼいないと思うので検査から除外しています。除外だらけw

レギュレーション

確認環境

  • Unity 2019.4.7f1
  • Windows10
  • VisualStudio2019

補足:

計測はUnityのプロファイラーを使用する

使用するJSON型

以前 DataContractJsonSerializer の使い方で使用した以下のJSON形式を使用します。

このデータを生成して各ライブラリでシリアライズ・デシリアライズしてパフォーマンスを計測していきたいと思います。

[
    {
        "id": 0,
        "name": "Taka",
        "numbers": [
            0,
            1,
            2,
            3
        ],
        "list": [
            0,
            1,
            2,
            3
        ],
        "map": [
            {
                "Key": "key1",
                "Value": "value1"
            },
            {
                "Key": "key2",
                "Value": "value2"
            },
            {
                "Key": "key3",
                "Value": "value3"
            }
        ]
    },
    {
        "id": 1,
        "name": "PG",
        ...// 以下繰り返し
]

使用するクラスの定義

公平を期すためにすべてのテストで以下の定義を使用します。

// 親のクラス
[DataContract]
[Serializable]
[JsonObject]
public class JsonParent
{
    [DataMember(Name = "items")]
    [JsonProperty("items")]
    public List<JsonItem> list = new List<JsonItem>();
}

// 子クラス
[DataContract]
[Serializable]
[JsonObject]
public class JsonItem
{
    [DataMember(Name = "id")]
    [JsonProperty("id")]
    public int id;

    [DataMember(Name = "name")]
    [JsonProperty("name")]
    public string name;

    [DataMember(Name = "numbers")]
    [JsonProperty("numbers")]
    public int[] numbers;

    [DataMember(Name = "list")]
    [JsonProperty("list")]
    public List<float> numberList = new List<float>();

    //[DataMember(Name = "map")]
    //[JsonProperty("map")]
    //public IDictionary<string, string> attributes = new Dictionary<string, string>();
}

測定結果

補足:

公平を期すためにDictionaryの部分はコメントアウトしています。

1件を100回シリアライズ・デイシリアライズ

f:id:Takachan:20200816194119p:plain

シリアライズ
Lib Time GC Alloc 備考
JsonUtility 2.12ms 54.8KB
DataContractJsonSerializer 447.35ms 1.5MB 突出して遅い
JSON.NET 205.67ms 0.7MB
デシリアライズ
Lib Time GC Alloc 備考
JsonUtility 6.7ms 39.2KB
DataContractJsonSerializer 288.6ms 1.9MB 突出して遅い
JSON.NET 81.59ms 415.1KB

10件を100回シリアライズ・デシリアライズ

f:id:Takachan:20200816194716p:plain

シリアライズ
Lib Time GC Alloc 備考
JsonUtility 6.33ms 491.9KB
DataContractJsonSerializer 1885.52ms 7.2MB 突出して遅い、件数増加で悪化
JSON.NET 594.06ms 3.0MB
デシリアライズ
Lib Time GC Alloc 備考
JsonUtility 31.65ms 301.1KB
DataContractJsonSerializer 2006.74ms 4.9MB 突出して遅い、件数増加で悪化
JSON.NET 448.21ms 1.4MB

100件を100回シリアライズ・デシリアライズ

f:id:Takachan:20200816200554p:plain

シリアライズ

| Lib | Time | GC Alloc | 備考 |-|-| | JsonUtility | 45.69ms | 4.8MB | | DataContractJsonSerializer | 11569.97ms | 62.8MB | 突出して遅い、件数増加で悪化 | JSON.NET | 2170.23ms | 22.7MB |

デシリアライズ
Lib Time GC Alloc 備考
JsonUtility 115.29ms 2.9MB
DataContractJsonSerializer 15389.79ms 35.4MB 突出して遅い、件数増加で悪化
JSON.NET 2921.73ms 11.4MB

1000件を1回シリアライズ・デシリアライズ

f:id:Takachan:20200816201054p:plain

シリアライズ
Lib Time GC Alloc 備考
JsonUtility 5.12ms 490.3KB
DataContractJsonSerializer 1320.66ms 6.7MB 突出して遅い
JSON.NET 491.31ms 2.5MB
デシリアライズ
Lib Time GC Alloc 備考
JsonUtility 9.97ms 291.8KB
DataContractJsonSerializer 1529.85ms 3.4MB 突出して遅い
JSON.NET 302.28ms 1.1MB

結論

  • 速度が必要な場所では JsonUtility を使用する
    • 毎フレーム処理するような場合確実にこれを選択する以外ない
  • 汎用性の方が大切な場合 Json.NET を使用する
    • ロード中などの余裕のある時に複雑なデータ処理をするときはこっち
  • DataContractJsonSerializer は論外(ゲームどころかあらゆるケースで使用NG)
    • 絶対に使用しないこと

測定コード

以下のコードを実行してプロファイラーから確認しました。

JsonTestクラス

適当なゲームオブジェクトにアタッチして起動時に検査コードを実行する処理を行います。

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Text;
using UnityEngine;

public class JsonTest : MonoBehaviour
{
    // 生成するデータ量
    [SerializeField] private int DataLength = 10;
    // シリアライズ・デシリアライズの試行回数
    [SerializeField] private int TestCount = 10;

    public void Start()
    {
        // あらかじめデータを生成しておいて
        JsonParent item = DataJenerator.CreateTestData(this.DataLength);

        // 全て同じデータで確認する
        this.RunJsonUtility(item);
        this.RunJsonStd(item);
        this.RunJSOdotNET(item);
    }

    public void RunJsonUtility(JsonParent item)
    {
        for (int i = 0; i < TestCount; i++)
        {
            string jsonStr = JsonUtility.ToJson(item);
            JsonParent _tmp = JsonUtility.FromJson<JsonParent>(jsonStr);

            if (i == 0)
            {
                UnityEngine.Debug.Log($"JsonUtil={jsonStr}");
            }
        }
    }

    public void RunJsonStd(JsonParent item)
    {
        for (int i = 0; i < TestCount; i++)
        {
            string jsonStr = JsonUtilityStd.Serialize(item);
            JsonUtilityStd.Deserialize<JsonParent>(jsonStr);

            if (i == 0)
            {
                UnityEngine.Debug.Log($"JsonUtilityStd={jsonStr}");
            }
        }
    }

    public void RunJSOdotNET(JsonParent item)
    {
        for (int i = 0; i < TestCount; i++)
        {
            string jsonStr = JsonConvert.SerializeObject(item);
            JsonConvert.DeserializeObject<JsonParent>(jsonStr);

            if (i == 0)
            {
                UnityEngine.Debug.Log($"RunJSOdotNET={jsonStr}");
            }
        }
    }
}

DataJeneratorクラス

検査対象のダミーデータを生成します。

public static class DataJenerator
{
    // 指定した件数分のJSONに変換するデータを生成する
    public static JsonParent CreateTestData(int count)
    {
        IEnumerable<JsonItem> f()
        {
            for (int i = 0; i < count; i++)
            {
                var item = new JsonItem()
                {
                    id = UnityEngine.Random.Range(0, int.MaxValue),
                    name = Guid.NewGuid().ToString(),
                    numbers = new int[]
                    {
                        UnityEngine.Random.Range(0, int.MaxValue),
                        UnityEngine.Random.Range(0, int.MaxValue),
                        UnityEngine.Random.Range(0, int.MaxValue),
                        UnityEngine.Random.Range(0, int.MaxValue),
                        UnityEngine.Random.Range(0, int.MaxValue),
                    },
                };

                item.numberList.Add(UnityEngine.Random.Range(0.0f, float.MaxValue));
                item.numberList.Add(UnityEngine.Random.Range(0.0f, float.MaxValue));
                item.numberList.Add(UnityEngine.Random.Range(0.0f, float.MaxValue));
                item.numberList.Add(UnityEngine.Random.Range(0.0f, float.MaxValue));
                item.numberList.Add(UnityEngine.Random.Range(0.0f, float.MaxValue));

                item.attributes[Guid.NewGuid().ToString()] = Guid.NewGuid().ToString();
                item.attributes[Guid.NewGuid().ToString()] = Guid.NewGuid().ToString();
                item.attributes[Guid.NewGuid().ToString()] = Guid.NewGuid().ToString();
                item.attributes[Guid.NewGuid().ToString()] = Guid.NewGuid().ToString();
                item.attributes[Guid.NewGuid().ToString()] = Guid.NewGuid().ToString();

                yield return item;
            }
        }

        return new JsonParent()
        {
            list = f().ToList(),
        };
    }
}

JsonUtilityStdクラス

DataContractJsonSerializer の汎用処理をサポートします。

public static class JsonUtilityStd
{
    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());
        }
    }
    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);
        }
    }
}

参考資料

各ライブラリの特

いちおう各ライブラリの特徴を書いておきます。

  • JSON Utility
    • 長所
      • とにかく動作が軽い・早い
      • Unityで動作保証されている
    • 短所
      • オブジェクトの名前が指定できない
      • 制約が多いのでオブジェクトの変換などを事前にする必要がある
        • プロパティが対象にならない
        • Dictionary型が使えない
        • List型をルートに使用できない
        • DateTime型が変換できない
        • etc...
  • DataContractJsonSerializer
    • 長所
      • ほぼすべてのオブジェクトを変換できる
        • Listを直接変換できる, Dictionaryも余裕で扱える
      • オブジェクト名が指定できる
      • Unityに依存していないので汎用性が高い(Unity外で制作も可能)
      • 雑に使ってもたいてい動く
    • 短所
      • あらかじめクラスを定義する必要がある
      • ルート要素に名前が付けられない
      • 死ぬほど動作が重い(1000倍くらい重い)
        • 汎用性が非常に高い分動作が低速
  • JSON.NET
    • 長所
      • 機能的に一番汎用性が高い
        • "DataContractJsonSerializer" の長所をすべて持っている
          • ルート要素にも名前が付けられる
      • Unityに依存していないので汎用性が高い(Unity外で制作も可能)
      • あらかじめオブジェクトを作成しないでもJsonObjectという汎用的な型で扱える
    • 短所
      • 割と処理は重い

各ライブラリのシリアライズ後の文字列

参考までに各ライブラリで作成したJSONを以下に記載します。ライブラリごとにクセがめちゃくちゃ色々あるのでよく注意したほうがいいです。特に違うシステムと連携したときにパース出来ないとか意図しない形式にシリアル化されているとか、逆に仕様書通りに出力できないとかマジで色々あるので特徴と機能はよく理解しておく必要がありそうです。

Json Utilityu

Dictionary型は無視されています。というかエンジンの仕様で変換できないメンバーが無視して出力しません。

{
    "list": [
        {
            "id": 1447338837,
            "name": "9b3b9b8c-9b5c-4976-8ca6-18906b1322f1",
            "numbers": [
                712401974,
                328545571,
                1454266954,
                629343681,
                1011712660
            ],
            "numberList": [
                1.3944311762943991e+38,
                2.8733568427566508e+38,
                3.3132977216911543e+38,
                3.1354838649369004e+38,
                1.2534365608129303e+37
            ]
        }
    ]
}
DataContractJsonSerializer

Dictionaryは Key - Value で自動的に整形されて出力されます。順序保証がされないので並び順も適当です。

{
    "items": [
        {
            "id": 840029637,
            "list": [
                3.07462284e+38,
                1.84336315e+38,
                2.1545555e+37,
                1.25667769e+37,
                1.46380977e+37
            ],
            "map": [
                {
                    "Key": "f0f37831-501f-452b-a4c4-f7e823f1b71f",
                    "Value": "aa59fcd5-a01b-4c66-a9cd-e183d80e6df6"
                },
                {
                    "Key": "50b95a9c-d3ee-4ea0-a839-2c57599d7612",
                    "Value": "c8cb5110-24aa-4a3f-a9ec-0d035b3bddad"
                },
                {
                    "Key": "e1072425-95ce-4aaf-9bf1-2bd5dede3ff2",
                    "Value": "2c1fee2f-d3e0-41d0-8a4d-314b94b094ef"
                },
                {
                    "Key": "ccbbadb8-20a5-4358-be64-67756f72e893",
                    "Value": "8618a4ad-adb0-4f89-a5f2-db7729c3ef27"
                },
                {
                    "Key": "4d08761c-d9c3-4e9c-87c6-dafa7b8e65df",
                    "Value": "15c67586-5c92-4d13-8f34-5b254ef259fb"
                }
            ],
            "name": "65868ff1-507c-4694-8abc-673bb108a3eb",
            "numbers": [
                2112028652,
                1602046542,
                1331859955,
                1935792680,
                420890169
            ]
        }
    ]
}
Json.NET

一番JSONっぽい出力になります。順序も宣言順を守ってくれます。

{
    "items": [
        {
            "id": 840029637,
            "name": "65868ff1-507c-4694-8abc-673bb108a3eb",
            "numbers": [
                2112028652,
                1602046542,
                1331859955,
                1935792680,
                420890169
            ],
            "list": [
                3.07462284e+38,
                1.84336315e+38,
                2.1545555e+37,
                1.25667769e+37,
                1.46380977e+37
            ],
            "map": {
                "f0f37831-501f-452b-a4c4-f7e823f1b71f": "aa59fcd5-a01b-4c66-a9cd-e183d80e6df6",
                "50b95a9c-d3ee-4ea0-a839-2c57599d7612": "c8cb5110-24aa-4a3f-a9ec-0d035b3bddad",
                "e1072425-95ce-4aaf-9bf1-2bd5dede3ff2": "2c1fee2f-d3e0-41d0-8a4d-314b94b094ef",
                "ccbbadb8-20a5-4358-be64-67756f72e893": "8618a4ad-adb0-4f89-a5f2-db7729c3ef27",
                "4d08761c-d9c3-4e9c-87c6-dafa7b8e65df": "15c67586-5c92-4d13-8f34-5b254ef259fb"
            }
        }
    ]
}

【Unity】モーダルダイアログを実装する(2019.4版)

タイトルには2019.4版と書きましたが特に新機能を使っているわけではないです。新しい環境でもできるよーって感じです。Unity は uGUI というUI作成機能があるのでかなり簡単にダイアログが実装できます。ほとんどコードも書かないで大丈夫です。そこでアプリでよくあるモーダルダイアログの Unity 2019.4 環境で実装してみたので作成手順を紹介したいと思います。

確認環境

  • Unity 2019.4.4f1
  • Windows10
  • VisualStudio2019

補足:

UnityEditor上のみで動作を確認しています。

ダイアログの仕様

仕様は概ね以下の通りです。

  • OKボタン, Cancelボタンを配置
  • ダイアログ表示中は背景が半透明にマスクされる
  • ダイアログ外をタッチするとCancelできる

見た目は大体以下のような感じです。

f:id:Takachan:20200725171900p:plain

ダイアログ外をタッチするとキャンセルできるというのがメインな感じです。

コンポーネント構成

Canvas以下にuGUIのコンポーネントを以下の通り配置することになります。これ以降で配置も含めて手順を説明しますが最終的にこのような形になります。

+ Canvas // UIのルート
  + DialogContainer   // ダイアログを配置する親要素
    + Image_Background   // 背景をマスクするImageコンポーネント
    + Image_DialogBody   // ダイアログ本体のImageコンポーネント
      + Button_OK   // OKボタン
      + Button_Cancel   // Cancelボタン

スクリプトの作成

ボタンが押されたときの動作を行うためのスクリプトが必要なので以下を定義して DialogContainer に追加します。

// OkCancelDialog.cs

using System;
using UnityEngine;

public class OkCancelDialog : MonoBehaviour
{
    public enum DialogResult
    {
        OK,
        Cancel,
    }
    
    // ダイアログが操作されたときに発生するイベント
    public Action<DialogResult> FixDialog { get; set; }
    
    // OKボタンが押されたとき
    public void OnOk()
    {
        this.FixDialog?.Invoke(DialogResult.OK);
        Destroy(this.gameObject);
    }
    
    // Cancelボタンが押されたとき
    public void OnCancel()
    {
        // イベント通知先があれば通知してダイアログを破棄してしまう
        this.FixDialog?.Invoke(DialogResult.Cancel);
        Destroy(this.gameObject);
    }
}

各コンポーネントの設定

DialogContainer の設定

DialogContainer はUI直下に配置します。作成するときにUI上で CreateEmpty を指定することで RectTransform だけを持つゲームオブジェクトとして作成します。よくImageを半透明にして配置するなどの手順がありますが半透明に上書きが発生するとパフォーマンスが悪くなるようなのでやめましょう。

f:id:Takachan:20200725175935p:plain

またこのオブジェクトの RectTransform はアンカーを全方向で Stretch に設定し画面いっぱいに引き伸ばすように設定します。

f:id:Takachan:20200725175548p:plain

画面いっぱいに広がるように余白は上下左右をゼロに設定します。

f:id:Takachan:20200725175713p:plain

そして上記で作成したスクリプトを追加しておきます。

f:id:Takachan:20200725180204p:plain

最後に EventTrigger を追加し、クリックすると先ほど作成したスクリプトの OnCancel メソッドを呼び出すように設定します。

f:id:Takachan:20200725181117p:plain

画面がタッチされたらイベントが発生するようにしたいので Event Trigger コンポーネントの「Add New Event Type」ボンタンを押して PointerClick を選択します。

f:id:Takachan:20200725181315p:plain

以下の図の赤丸の「+」ボタンを押し、クリックされたときの動作を追加します。ヒエラルキーからコンポーネントを下図のようにドラッグして配置し、右側のドロップダウンダイアログから「OkCancelDialog」>「OnCancel ()」の順で呼び出す処理を設定します。

f:id:Takachan:20200725181958p:plain

Image_Background の設定

こちらも DialogContainer の子要素に Image コンポーネントとしてオブジェクトを追加します。

f:id:Takachan:20200725180325p:plain

画像は省略しますが追加後に DialogContainer 同じようにアンカーを Stretch に設定して画面いっぱいに引き伸ばしておきます。そして色を半透明の黒に指定します。

この階層に追加することでUIの後ろの要素にダッチを通過させない、タッチするとこのゲームオブジェクトからイベントが発生するようにできます。

f:id:Takachan:20200725180432p:plain

Image_DialogBody の設定

こちらも DialogContainer の子要素に Image コンポーネントとしてオブジェクトを追加します(画像は省略)ヒエラルキー上の表示は以下のようになります。

f:id:Takachan:20200725180817p:plain

画面上の表示したい位置と大きさに配置し画像を設定します。Scene ウインドウの表示は以下のようになります。

f:id:Takachan:20200725180905p:plain

ボタンの追加

Image_DialogBody の子要素にOKボタンとCancelボタンを2つ配置します。ヒエラルキー上の配置は以下のようになります。

f:id:Takachan:20200725182106p:plain

SceneView 上での見た目はこんな感じです。

f:id:Takachan:20200725182452p:plain

そしてボタンコンポーネントにに配置されている「OnClick ()」に押したときの処理を追加します。追加手順は DialogContainer と同様です。

OKボタンの設定内容

f:id:Takachan:20200725182759p:plain

Cancelボタンの設定内容

f:id:Takachan:20200725182811p:plain

ダイアログのプレハブ化

上記の設定を全て行ったらダイアログをプレハブ化しておきます。以下のようにヒエラルキー上のコンポーネントをプロジェクトにドラッグするとヒエラルキー上のオブジェクトが青くなるのでこれでプレハブ化ができます。

f:id:Takachan:20200725183017p:plain

プレハブ化したらヒエラルキー上からオブジェクトを削除します。

動作確認

テスト用の処理を追加

まず以下のようなコンポーネントを作成します。

// OkCancelDialogTest.cs

using UnityEngine;

public class OkCancelDialogTest : MonoBehaviour
{
    // ダイアログを追加する親のCanvas
    [SerializeField] private Canvas parent = default;
    // 表示するダイアログ
    [SerializeField] private OkCancelDialog dialog = default;

    public void ShowDialog()
    {
        // 生成してCanvasの子要素に設定
        var _dialog = Instantiate(dialog);
        _dialog.transform.SetParent(parent.transform, false);
        // ボタンが押されたときのイベント処理
        _dialog.FixDialog = result => Debug.Log(result);
    }
}

次に画面上にテスト用のボタンを配置して上記スクリプトを追加します。

f:id:Takachan:20200725183939p:plain

スクリプトを追加したらプロジェクトとヒエラルキーからそれぞれ以下のようにオブジェクトをインスペクターにドラッグ&ドロップします。

f:id:Takachan:20200725183853p:plain

次にボタンが押されたときの処理を Button の OnClick() に追加します。

f:id:Takachan:20200725184201p:plain

実行してみる

これで実行すると以下のような動きになります。

f:id:Takachan:20200725184847g:plain

ボタンを押したときと画面外を押したときの動きが反映されていると思います。

少しアニメーションを追加すると以下のようになります。こっちも基本は同じです。

f:id:Takachan:20200725190404g:plain

【はてなブログ】全て記事のURL・タイトル・投稿日を取得する

前回の記事で投稿済みの記事URL一覧をすべて取得するプログラムを書いてみました。

takap-tech.com

今回は、記事のURLのほかに、各記事のタイトルと投稿日時を取得してCSVファイルに出力してみようと思います。

確認環境

  • C# 8.0
  • VisualStudio 2019
  • .NET Core 3.1

実行結果

以下に紹介するプログラムを実行すると以下の通りになります。

はてなブログを使っている自分のサイトにリクエストを送っています。

// 実行するコマンド
> command.exe takap-tech.com d:\url.csv " - PG日誌"

// 実行結果、長いので改行しています。
// url.csv
0001/01/01 00:00,PG日誌
    ,https://takap-tech.com/
0001/01/01 00:00,このブログについて
    ,https://takap-tech.com/about
2020/07/15 00:58,【C言語】コロナ感染拡大で政府がGoToキャンペーンを強行
    ,https://takap-tech.com/entry/2020/07/15/005828
2020/07/09 00:38,【Unity】効果音(SE)の再生方法と音割れ防止について
    ,https://takap-tech.com/entry/2020/07/09/003837
2020/07/09 00:35,【C#】App.config(アプリケーション構成)で設定を読み込む
    ,https://takap-tech.com/entry/2020/07/09/003530
2020/07/09 00:25,"【C#】2,8,10,16進数文字列と数値の相互変換方法まとめ"
    ,https://takap-tech.com/entry/2020/07/09/002557
....

コード

前回記事はURLを取得するだけでしたら今回は Sitemapクラスでサイトマップから記事URL一覧の取得、HtmlPageクラスで各ページ内の情報へアクセスするように変更しています。

private static readonly XNamespace ns = "http://www.sitemaps.org/schemas/sitemap/0.9";

public static void Main(string[] args)
{
    // 書式:
    // > command.exe ${domain-name} ${out-path} [$remove-word}]
    //
    // e.g.
    // > command.exe takap-tech.com d:\url.csv " - PG日誌"

    // コマンドラインからドメイン名を指定(takap-tech.com)
    string domain = args[0];
    string rootUrl = $"https://{domain}/sitemap.xml";

    // 出力先パス
    string outPath = args[1];

    // 表示するときに削除したい単語があれば指定する
    string removeWord = "";
    if (args.Length == 3)
    {
        removeWord = args[2];
    }

    Console.WriteLine($"{DateTime.Now:HH:mm:ss} 開始 >>>");

    int i = 0;
    using var sitemap = new Sitemap();
    using var sw = new StreamWriter(outPath);
    foreach (var page in sitemap.GetContentsAll(rootUrl))
    {
        string line = $"[{i++}],{page.ToString().Replace(removeWord, "")}";
        sw.WriteLine(line);
        Console.WriteLine(line);
    }

    Console.WriteLine($"{DateTime.Now:HH:mm:ss} 終了 <<<");
}

// サイトマップからURLを抽出するクラス
public class Sitemap : IDisposable
{
    private static HttpClient _client;
    public static HttpClient Client => _client ??= new HttpClient();

    public void Dispose()
    {
        using (_client) { }
        GC.SuppressFinalize(this);
    }

    public IEnumerable<HtmlPage> GetContentsAll(string rootUrl)
    {
        foreach (var url in this.GetUrlAll(rootUrl))
        {
            yield return new HtmlPage(url, Client.GetStringAsync(url).Result);
        }
    }

    public IEnumerable<string> GetUrlAll(string rootUrl)
    {
        foreach (var sub in this.GetRootItems(rootUrl))
        {
            foreach (var url in this.GetSubItems(sub))
            {
                yield return url;
            }
        }
    }

    public IEnumerable<string> GetRootItems(string rootUrl)
    {
        string body = Client.GetStringAsync(rootUrl).Result;

        var xml = XDocument.Parse(body);
        var e1 = xml.Element(ns + "sitemapindex");
        var e2 = e1.Elements(ns + "sitemap");

        foreach (var e3 in e2)
        {
            yield return e3.Element(ns + "loc").Value;
        }
    }

    public IEnumerable<string> GetSubItems(string subUrl)
    {
        string body = Client.GetStringAsync(subUrl).Result;

        var xml = XDocument.Parse(body);
        var e1 = xml.Element(ns + "urlset");
        var e2 = e1.Elements(ns + "url");

        foreach (var e3 in e2)
        {
            yield return e3.Element(ns + "loc").Value;
        }
    }
}

// 取得したURLのページを表すクラス
public class HtmlPage
{
    private readonly string _body;

    public string Url { get; private set; }

    public string Title => Regex.Match(this._body, "<title>(?<name>.*)</title>").Groups["name"].Value;

    public DateTime Time
    {
        get
        {
            string timeStr = Regex.Match(this._body,
                "<time data-relative.*>(?<time>.*)</time>").Groups["time"].Value;
            if (DateTime.TryParseExact(timeStr, 
                "yyyy-MM-dd HH:mm", null, System.Globalization.DateTimeStyles.None, out DateTime ret))
            {
                return ret;
            }
            else
            {
                return DateTime.MinValue;
            }
        }
    }

    public HtmlPage(string url, string body)
    {
        this.Url = url;
        this._body = body;
    }

    public override string ToString() =>
        $"\"{this.Time:yyyy/MM/dd HH:mm}\"\t\"{this.Title}\"\t\"{this.Url}\"";
}

App.config(アプリケーション構成ファイル)を利用する

C#の実行形式のファイル(.NET Framework, .NET Core)にはアプリーション固有の設定を XML 形式で記述できるアプリケーション構成ファイルというものが添付できます。このファイルに設定を書いておくと起動時に自動的に読み込まれ以降、この設定を読み出すことができます。

確認環境

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

  • VS2019
  • Windows10
  • .NET 5
  • .NET Core3.1
  • .NET Framework 4.8

System.Configurationのセットアップ

まず、使用を開始する前に .NET 5 などの一部の環境ではパッケージマネージャーからセットアップが必要です。.NET 5系でも Microsoft.WindowsDesktop.App.WindowsForms を使用している場合はセットアップ不要です。

Visual Studio の IDE から 「NuGet パッケージ マネージャー」を開いて「System.Configuration.ConfigurationManager」を導入するか、「パッケージマネージャー コンソール」にて以下を入力して追加のパッケージを導入します。

// URL:
// https://www.nuget.org/packages/System.Configuration.ConfigurationManager/

PM> Install-Package System.Configuration.ConfigurationManager -Version 5.0.0

値の定義方法

.NET Framework の場合、実行形式ファイルを作成するとプロジェクトにデフォルトで "App.config" というものがあるのでそのまま使用します。.NET Core の場合(もしくは存在しないプロジェクトの場合)、最初は存在しないので、プロジェクトのコンテキストメニューから以下の順でプロジェクトにファイルを追加します。

追加 > 新しい項目 > アプリケーション構成ファイル

追加したファイルを開くと以下のような初期状態になっています。

<!-- 追加したXML -->
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
</configuration>

ここに「キー」「値」形式で以下のように値を追加します。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <appSettings>
        <add key="key1" value="abc" />
        <add key="key2" value="1" />
        <add key="key3" value="10.5" />
    </appSettings>
<
//configuration>

App,config には key-value の単純な組み合わせ以外にも独自の形式を定義する事ができます。しかしアクセス方法が独特で覚えるのがかなり面倒なので、そういった場青自分で外部ファイルを作成してデシリアライズしたほうが圧倒的に効率が良です。value に外部定義ファイルのパスを書いて2段階参照するのが定石かと思います。

また、設定値を App.config に書き込んで値を保持してると大抵の場合あとで大変なことになるため App.confg は読み取り専用で key-value の組み合わせ持つくらいの使い方に留めた方が良いでしょう。

もしどうしてもやりたい場合は使い方はこのサイトが詳しいです。ここでは解説しません。

定義した値の読み取り

基本的に、AppSettings セクションは以下のように記述して value を文字列として取得します。

string key1 = ConfigurationManager.AppSettings["key1"];

読み取り用のヘルパーを作成する

この、読み取った値は全て文字列として扱われてしまいますが、valuue が数値の場合、毎回変換するためのコードを書くのは大変なのでユーティリティを作成したいと思います。

使い方

先にユーティリティの使い方を紹介します。

// 定義値を文字列として取得する
int key1 = AppSetting.GetString("key1");

// 定義値をintとして取得する
int key2 = AppSetting.GetInt("key2");

// 定義値を TimeSpan 型として取得する、変換方法はラムダで指定する
TimeSpan span = AppSetting.GetTimeSpan("key4", value => TimeSpan.FromSeconds(value));

// 定義値を DateTime 型として取得する、変換方法はラムダで指定する
DateTime span = 
    AppSetting.GetDateTime("key5",
        str => DateTime.ParseExact(str, "yyyy/MM/dd HH:mm:ss.fff", null));

C#の基本型はすべてサポート + TimeSpan, DateTime も変換方法を指定すれば取得できます。ジェネリックを使用する方法もありますが一度オブジェクトに変換してキャストする関係で少し遅かったので基本型ごとに専用のメソッドを用意して値を取得します。

AppSettingクラス

実際の実装は以下の通りです。コピペでOK。

using System.Configuration;
using System.Globalization;
using System.Runtime.CompilerServices;

/// <summary>
/// App.config の appSetting セクションにアクセスするための汎用機能を提供します。
/// </summary>
public static class AppSetting
{
    #region 見本...
    //
    // 見本:
    // 以下のような App.config 内の appSetting ノードの中身に簡単にアクセスできるようにする
    //
    // <?xml version="1.0" encoding="utf-8" ?>
    // <configuration>
    //
    //     <appSettings>
    //         <add key="key" value="10"/>
    //     </appSettings>
    //
    // </configuration>
    //
    #endregion

    #region 補足...
    //
    // 補足:
    // App.config に任意の値を書き込むこともできるが大抵の場合ロクなことにならないので
    // この値は静的な定数としてアプリは値を読み取り専用で扱うのを推奨
    // 
    // 動的なパラメータこのファイルに記載せずに Settings や外部ファイルで扱う方がよい
    //
    #endregion

    /// <summary>
    /// appSetting セクションをリロードします。
    /// </summary>
    public static void Reload() => ConfigurationManager.RefreshSection("appSettings");

    /// <summary>
    /// appSetting セクション内の指定したキーに対応する値を取得します。
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static string GetString(string key) => ConfigurationManager.AppSettings[key];

    // 以下基本型として値を取得するメソッド

    public static bool GetBool(string key) => bool.Parse(GetString(key));
    public static byte GetByte(string key) => byte.Parse(GetString(key));
    public static sbyte GetSByte(string key) => sbyte.Parse(GetString(key));
    public static char GetChar(string key) => char.Parse(GetString(key));
    public static decimal GetDecimal(string key) => decimal.Parse(GetString(key));
    public static double GetDouble(string key) => double.Parse(GetString(key));
    public static float GetFloat(string key) => float.Parse(GetString(key));
    public static int GetInt(string key) => int.Parse(GetString(key));
    public static uint GetUInt(string key) => uint.Parse(GetString(key));
    public static long GetLong(string key) => long.Parse(GetString(key));
    public static ulong GetULong(string key) => ulong.Parse(GetString(key));
    public static short GetShort(string key) => short.Parse(GetString(key));
    public static ushort GetUShort(string key) => ushort.Parse(GetString(key));

    /// <summary>
    /// 設定値を指定したフォーマットで <see cref="DateTime"/> 型に変換して取得します。
    /// </summary>
    public static DateTime GetDateTime(string key, string format)
    {
        return DateTime.ParseExact(GetString(key), format, CultureInfo.InvariantCulture);
    }

    /// <summary>
    /// 指定値を <see cref="TimeSpan"/> として取得します。変換は conv の処理を使用します。
    /// </summary>
    public static TimeSpan GetTimeSpan(string key, Func<double, TimeSpan> conv)
    {
        return conv(GetDouble(key));
    }
}

以上です。

【C#】2,8,10,16進数文字列と数値の相互変換方法まとめ

数字と文字列にはいろいろ変換方法があるのですが、一覧的に確認できると便利かと思いまとめてみました。

他にもいろいろやり方はありますがこれさえ覚えておけば問題ないと思います。

数値 → 2,8,10,16進数文字列 に変換

数値からN進数の文字列に変換したい場合、各基本型についている ToString() メソッドか Convert.ToString() メソッドを使用します。

元の型 変換先 やり方(1) やり方(2)
数値 2進数の文字列 Convert.ToString ( num , 2 );
数値 8進数の文字列 Convert.ToString ( val , 8 );
数値 10進数の文字列 num.ToString ( num ); Convert.ToString ( num );
数値 16進数の文字列 num.ToString ( "x" ); Convert.ToString ( num , 16 );

使用例は以下の通りです。

public static void Foo()
{
    int inum = 3130;

    // 整数値 → 2進数の文字列に変換
    string str2 = Convert.ToString(inum, 2);
    // > str2 = 110000111010

    // 整数値 → 8進数の文字列に変換
    string str8 = Convert.ToString(inum, 8);
    // > str8 = 6072

    // 整数値 → 10進数の文字列に変換
    string str101 = inum.ToString();
    string str102 = Convert.ToString(inum);
    // > str101 = 3130, str102 = 3130

    // 整数値 → 16進数の文字列に変換
    string str161 = inum.ToString("X"); // "x"はアルファベット部分が小文字, "X"は大文字となる
    string str162 = Convert.ToString(inum, 16);
    // > str161 = C3A, str162 = c3a
}

2,8,10,16進数文字列 → 数値 に変換

Convert クラスにある ToXxxx() メソッドで変換できます。例えば int 型に変換したいときは Convert.ToInt32() メソッドを使用します。

元の型 変換先 やり方
2進数の文字列 int Convert.ToInt32 ( "110000111010" , 2 );
8進数の文字列 int Convert.ToInt32 ( "6072" , 8 );
10進数の文字列 int Convert.ToInt32 ( "3130" );
16進数の文字列 int Convert.ToInt32 ( "0xFFFF" , 16 );

「Convert.ToInt32("0xFFFF", 16);」は文字列の先頭に「0x」が付いていても変換できます。

使用例は以下の通りです。

public static void Foo()
{
    // 文字列を2進数として intに変換
    int inum1 = Convert.ToInt32("110000111010", 2);
    // > inum1 = 3130

    // 文字列を8進数として intに変換
    int inum2 = Convert.ToInt32("6072", 8);
    // > inum2 = 3130

    // 文字列を10進数として intに変換
    int inum3 = Convert.ToInt32("3130");
    // > inum3 = 3130

    // 先頭に0xが付いた16進数文字列を intに変換
    int inum4 = Convert.ToInt32("0xC3A", 16);
    // > inum4 = 3130

    // int 以外にもほぼすべての基本型に変換できる。
    // ただし 16 が指定できるオーバーロードが無いものもある
    byte _b = Convert.ToByte(str, 16);
}

短いですが以上です。

【はてなブログ】投稿記事のURLを一括取得する

とある事情で自分のブログの投稿した記事の全URLのリストアップが必要になったのでリストアップするためのプログラムをC#で書いてみました。

せっかくなのでコードを公開しようと思います。

サイトマップの形式

まずはサイトマップのデータ形式を確認します。はてなブログのサイトマップの形式は2020年7月8日現在以下の通りです。

【サイト全体の概要】sitemap.xml

まずはトップの sitemap.xml です。このURLには各月ごとにまとめられた子サイトマップへのリンク集となっています。

以下のぼくのサイトの例では1か月単位で タグにURLが順番に並んでいます。

<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap>
    <loc>https://takap-tech.com/sitemap_common.xml</loc>
    <lastmod>2020-07-08T01:50:34+09:00</lastmod>
  </sitemap>
  <sitemap>
    <loc>https://takap-tech.com/sitemap_periodical.xml?year=2020&amp;month=7</loc>
    <lastmod>2020-07-08T01:50:34+09:00</lastmod>
  </sitemap>
  <sitemap>
    <loc>https://takap-tech.com/sitemap_periodical.xml?year=2020&amp;month=6</loc>
    <lastmod>2020-07-08T01:50:34+09:00</lastmod>
  </sitemap>
  <sitemap>
  ... 以下繰り返し

【月ごとの記事一覧】sitemap_periodical.xml

sitemap.xml にあったリンク先は各月ごとの投稿記事の一覧のURLです。

ここに具体的な各記事へのURLが記載されているのでここからURLを抽出します。

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://takap-tech.com/entry/2020/06/22/220549</loc>
    <lastmod>2020-06-22T22:13:01+09:00</lastmod>
  </url>
  <url>
    <loc>https://takap-tech.com/entry/2020/06/20/232208</loc>
    <lastmod>2020-06-24T22:26:57+09:00</lastmod>
  </url>
  <url>
  ... 以下繰り返し
</urlset>

各記事のURLをC#で取得する

プログラム言語はC#を使用します。

確認環境

この記事は以下の環境で動作確認しました。

  • C# 8.0
  • VisualStudio 2019
  • .NET Core 3.1

実装コード

C#で記事URLを取得するには HttpClient を使用するのが一番簡単だと思います。

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

まず sitemap.xml の内容を HttpClient で取得し、内容を XDocument でパースします。各月ごとのリンクが取れるのでそれを読みに行って各記事のURLを取得しコンソールに出力します。

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using System.Xml.Linq;
using AngleSharp.Html.Dom;
using AngleSharp.Html.Parser;

internal class AppMain
{
    private static readonly XNamespace ns = "http://www.sitemaps.org/schemas/sitemap/0.9";

    public static void Main(string[] args)
    {
        string domain = args[0]; // コマンドラインからドメイン名を指定
        string rootUrl = $"https://{domain}/sitemap.xml";

        using var client = new HttpClient();

        foreach (var sub in parseRoot(client, rootUrl))
        {
            foreach (var path in parseSub(client, sub))
            {
                Console.WriteLine(path);
                // string title = getTitle(url).Result;
                // Console.WriteLine($"{url}, {title}");
            }
        }
    }

    public static IEnumerable<string> parseRoot(HttpClient client, string url)
    {
        string body = client.GetStringAsync(url).Result;

        var xml = XDocument.Parse(body);
        var e1 = xml.Element(ns + "sitemapindex");
        var e2 = e1.Elements(ns + "sitemap");

        foreach (var e3 in e2)
        {
            yield return e3.Element(ns + "loc").Value;
        }
    }

    public static IEnumerable<string> parseSub(HttpClient client, string url)
    {
        string body = client.GetStringAsync(url).Result;

        var xml = XDocument.Parse(body);
        var e1 = xml.Element(ns + "urlset");
        var e2 = e1.Elements(ns + "url");

        foreach (var e3 in e2)
        {
            yield return e3.Element(ns + "loc").Value;
        }
    }

    // ★★★AngleSharp を利用した記事タイトルの取得
    // using AngleSharp.Html.Dom;
    // using AngleSharp.Html.Parser;
    public static async Task<string> getTitle(string url)
    {
        using var client = new HttpClient();
        using var stream = await client.GetStreamAsync(new Uri(url));
        var parser = new HtmlParser();
        IHtmlDocument doc = await parser.ParseDocumentAsync(stream);
        return doc.Title;
    }
}

実行結果

コマンドラインから以下のようにドメインを指定してプログラムを実行すると以下のように出力されます。

// ドメインを引数にアプリを実行する
> url-listup.exe takap-tech.com

// こんな感じにコンソールに出力される
> https://takap-tech.com/
> https://takap-tech.com/about
> https://takap-tech.com/entry/2020/07/08/015033
> https://takap-tech.com/entry/2020/06/22/220549
> https://takap-tech.com/entry/2020/06/20/232208
> https://takap-tech.com/entry/2020/06/18/002237
> https://takap-tech.com/entry/2020/06/11/003228
> https://takap-tech.com/entry/2020/06/05/005816
> https://takap-tech.com/entry/2020/06/05/001711
> https://takap-tech.com/entry/2020/04/30/152534
> https://takap-tech.com/entry/2020/04/28/211622

【C#】文字列や数値をenum型に変換する

ある任意の文字列や数値から特定のEnumに変換する方法です。

確認環境

  • C#8.0/7.3
  • VisualStudio2019
  • .NET Framwework 4.8/4.7.3
  • Windows10

バージョン依存性があるコードが含まれます。

文字列をEnumに変換する方法

まず enum が以下のように定義されているとします。

// 以下のようにenumが定義されているものとする
public enum EnumSample
{
    Ika = 0,
    Tako,
    Suzuki
}

シンボル名文字列から enum 型への変換

あるEnumのシンボル名からEnum形への変換方法jは以下の通りです。

変換は標準の機能として Enum クラスに Parse 系のメソッドが存在するためこれを利用します。

public static void Foo()
{
    // 変換方法(1) .NET Core の場合
    //
    // 補足:
    // この方法は.NET Core 限定。NET Framework にはメソッドが存在しない
    //
    // 変換できることが確実な場合このように記述する
    // 変換できない場合、「System.ArgumentException: 'Requested value 'sake' was not found.'」
    // 
    var e1 = Enum.Parse<EnumSample>("sake");

    // 変換方法(2) .NET Framework の場合
    //
    // 変換できることが確実な場合このように記述する
    // 変換できない場合、「System.ArgumentException: 'Requested value 'sazae' was not found.'」
    //
    var e2 = (EnumSample)Enum.Parse(typeof(EnumSample), "sazae");

    // 変換方法(2), .NET Core/Framework 共通
    //
    // 変換できるか分からないときにこちらを使用して
    // 変換できたか確認してから使用する。
    //
    if (Enum.TryParse("ika", out EnumSample result))
    {
        // 変換が成功したときに result に結果が入っている
    }
    else
    {
        // 失敗した場合こっち
    }
}

ターゲットプラットフォームが変更される場合、ジェネリック指定できたほうが記述が簡単になるので、上記変換(1), (2)はまとめて以下のように定義していた方が便利かもしれません。

// .NET Framework にジェネリック版の Parse メソッドが存在しないが .NET Core と同じように扱う対応
public static class MyEnum
{
#if NETFRAMEWORK
    public static TEnum Parse<TEnum>(string name) where TEnum : struct
    {
        return (TEnum)Enum.Parse(typeof(TEnum), name);
    }
#else
    public static TEnum Parse<TEnum>(string name) where TEnum : struct
    {
        return Enum.Parse<TEnum>(name);
    }
#endif
}

数字文字列から enum 型への変換

ある文字列変数に "1" などの数値文字列が入っている場合、enum 型に存在しない値でも変換できてしまうため Enum.IsDefined() メソッドで数字に対応する enum のシンボルが定義されているか確認する必要があります。

基本的に先述のシンボル名文字列からの変換と同じですが変換する前に IsDefined を使って存在するかどうかを確認してから変換を行います。

// 変換方法(1) .NET Core の場合
//
// 補足:
// この方法は.NET Core 限定。NET Framework にはメソッドが存在しない
//
// 変換できることが確実な場合このように記述する
// 変換できない場合、「System.ArgumentException: 'Requested value 'sake' was not found.'」
// 
string str1 = "10";
if (Enum.IsDefined(typeof(EnumSample), str1)) // 事前に定義が存在するかどうか確認する
{
    var e1 = Enum.Parse<EnumSample>(str1);
}

// 変換方法(2) .NET Framework の場合
//
// 変換できることが確実な場合このように記述する
// 変換できない場合、「System.ArgumentException: 'Requested value 'sazae' was not found.'」
//
string str2 = "Tako";
if (Enum.IsDefined(typeof(EnumSample), str2))
{
    var e2 = (EnumSample)Enum.Parse(typeof(EnumSample), str2);
}

// 変換方法(2), .NET Core/Framework 共通
//
// 変換できるか分からないときにこちらを使用して
// 変換できたか確認してから使用する。
//
string str3 = "Ika";
if (Enum.IsDefined(typeof(EnumSample), str3) && 
    Enum.TryParse(str3, out EnumSample result))
{
    // 変換が成功したときに result に結果が入っている
}
else
{
    // 失敗した場合こっち
}

数値から enum 型への変換

次は数値から enum 型への変換です。

変換自体は数値を enum 型にキャストするだけで変換可能ですが、こちらも存在しないシンボルの値にも変換できてしまうため事前に IsDefined で確認が必要になります。

// 変換方法(1)、普通にキャストする
int num1 = 0;
if (Enum.IsDefined(typeof(EnumSample), num1)) // 事前に定義が存在するかどうか確認する
{
    var e1 = (EnumSample)num1; // キャストで変換できる
}

// 変換方法(2) TryPaeseを使う
int num2 = 10;
if (Enum.IsDefined(typeof(EnumSample), num2) && 
    Enum.TryParse(num2.ToString(), out EnumSample result))
{
    // 変換が成功したときに result に結果が入っている
}
else
{
    // 失敗した場合こっち
}

相互変換をサポートするクラス

3種類のケースを見てきましたが結局文字列に何が入っているかで書き分ける必要があり、数値の場合とコードが違うなどで面倒なため、全て1つのメソッド解決できるユーティリティを紹介したいと思います。

使い方

先に使い方を紹介します。EnumUtil クラスに Parse, TryParse を実装しています。

//
// (1) Parse メソッドの使い方
//

var e0 = EnumUtil.Parse<EnumSample>("10");
// > 値が存在しないので ArgumentException

var e1 = EnumUtil.Parse<EnumSample>("Tako");
// > e1 = Tako

var e2 = EnumUtil.Parse<EnumSample>(2);
// > e1 = Suzuki

var e3 = EnumUtil.Parse<EnumSample>("2");
// > これはエラーになるので注意!

//
// (2) TryParse メソッドの使い方
//

if (EnumUtil.TryParse("10", out EnumSample result1))
{
    // 変換できたとき
}
else
{
    // 変換できなかったとき
}

// 大文字・小文字を区別したくない時は igoreCase = true で使用する
if (EnumUtil.TryParse("tako", true, out EnumSample result2))
{
    // 変換できたとき
}
else
{
    // 変換できなかったとき
}

EnumUtilクラス

実際の実装です。

.NET Framework でも使用可能なようにジェネリック版の Parse は使用していません。

// 任意の値を enum 型に変換するための機能を定義します
public static class EnumUtil
{
    public static TEnum Parse<TEnum>(object value, bool ignoreCase = false) where TEnum : struct
    {
        if (Enum.IsDefined(typeof(TEnum), value))
        {
            return (TEnum)Enum.Parse(typeof(TEnum), value.ToString(), ignoreCase);
        }
        else
        {
            throw new ArgumentException($"'{value}' is not found.");
        }
    }

    public static bool TryParse<TEnum>(object value, out TEnum result) where TEnum : struct
    {
        result = default;
        return Enum.IsDefined(typeof(TEnum), value) && 
            Enum.TryParse(value.ToString(), out result);
    }

    public static bool TryParse<TEnum>(object value, 
        bool ignoreCase, out TEnum result) where TEnum : struct
    {
        result = default;
        return Enum.IsDefined(typeof(TEnum), value) && 
            Enum.TryParse(value.ToString(), ignoreCase, out result);
    }
}

文字列変数を直接 enum に変換する

ここからは余談で、string 型に拡張メソッドを定義して直接 enum に変換してみます。

使い方

使い方は以下の通りです。

string name1 = "Tako";

// (1) 文字列の変数から直接 Parse して enum を得る
var e1 = name.Parse<EnumSample>();

string name2 = "Same";

// (2) 文字列の変数から直接 TryParse して変換できるか確認後に enum を得る
if(name.TryParse(out EnumSample ret))
{
    // 変換に成功した場合
}
else
{
    // 失敗した場合
}

実装コード:StringExtensionクラス

実装はすごく簡単で先ほどの EnumUtil を間接的に使用します。

public static class StringExtension
{
    // 文字列から TEnum で指定した enum に変換する
    public static TEnum Parse<TEnum>(this string self) where TEnum : struct
    {
        return EnumUtil.Parse<TEnum>(self);
    }

    public static bool TryParse<TEnum>(this string self, out TEnum result) where TEnum : struct
    {
        return EnumUtil.TryParse(self, out result);
    }

    public static bool TryParse<TEnum>(this string self, 
        object value, bool ignoreCase, out TEnum result) where TEnum : struct
    {
        return EnumUtil.TryParse(self, ignoreCase, out result);
    }
}

少し長くなってしまいましたが以上です。

関連リンク

takap-tech.com

【C#】基本型に範囲チェック機能を追加する

はじめに

ある変数が範囲内に収まっていれば新しい値を代入する処理などの「範囲を意識した処理」というものはプログラミングをしているを割と良く出てくる課題です。コードを書くと以下のように記述できます。

// value が 0~10の範囲内なら新しい値を代入する
double value = 10.5;
if (value <= 10 && value >= 0)
{
    value = 21.5;
}

こういった判定の繰り返しの記述で毎回コードを書かないように処理を汎用化したいと思います。この処理を各型の「拡張メソッド」として定義して上記判定文を以下のように簡単化します。

// value が 0~10の範囲内なら新しい値を代入する
value.Assign(21.5, 0, 10);

目次

今回の記事の目次は以下の通りです。

確認環境

  • VisualStudio 2019
  • .NET Core 3.1 (C#8.0)
  • Windows 10

実装した処理一覧

今回実装したメソッドの概要です。

# メソッド名 説明
1 Assign(v, min, max) 変数値が min ~ max の範囲内なら v を代入して true / 範囲外の場合代入せずに false を返す
2 AssignIfOrMore(v, min) 変数値が min 以上なら v を代入して true / 範囲外の場合代入せずに false を返す
3 AssignIfOrLess(v, max) 変数値が min 以下なら v を代入して true / 範囲外の場合代入せずに false を返す
4 IsInRange(min, max) 変数値が min ~ max の範囲内なら true / 範囲外なら false を返す
5 IsInRangeOrMore(min) 変数値が min 以上 true / 範囲外の場合代入せずに false を返す
6 IsInRangeOrLess(max) 変数値が min 以下なら true / 範囲外の場合代入せずに false を返す
7 Clamp(min, max) 変数値が範囲内に収まるように min以下ならmin, max 以上なら max の値を返す
8 ClampSelf(min, max) 変数値が範囲内に収まるように min以下ならmin, max 以上なら max の値を自分自身に設定する

上記メソッドのサポートしている型は以下の通りです。(.NETの基本型は全部サポートしました…大変だった、、、

# 型名 サポート状況
1 byte サポート済み
2 sbyte サポート済み
3 decimal サポート済み
4 double サポート済み
5 float サポート済み
6 int サポート済み
7 uint サポート済み
8 long サポート済み
9 ulong サポート済み
10 short サポート済み
11 ushort サポート済み

使い方

前述のメソッドの使い方は以下の通りです。

Assign(v, min, max):現在値が範囲内なら新しい値を代入する

// (1) 
int value_1 = 10;
bool ret_1 = value_1.Assign(30, 0, 10);
// > ret_1=true, value_1=30, 0~10の範囲外だったので新しい値を代入

int value_2 = 10;
bool ret_2 = value_2.Assign(30, 0, 5);
// > ret_2=flase, value_2=10, 0~5の範囲外なので変化しない

AssignIfOrMore(v, min):現在値が min 以上なら値を代入する

int value_1 = 10;
bool ret_1 = value_1.AssignIfOrMore(30, 0);
// > ret_1=true, value_1=30, 現在値が0以上だったので新しい値を代入

int value_2 = 10;
bool ret_2 = value_2.AssignIfOrMore(30, 20);
// > ret_2=flase, value_2=10, 現在値が20以上ではないので代入しない

AssignIfOrLess(v, max):現在値が min 以下なら値を代入する

int value_1 = 10;
bool ret_1 = value_1.AssignIfOrLess(30, 100);
// > ret_1=true, value_1=100, 現在値が100以下だったので新しい値を代入

int value_2 = 10;
bool ret_2 = value_2.AssignIfOrLess(30, 5);
// > ret_2=flase, value_2=10, 現在値が5以下ではないので代入しない

IsInRange(min, max):変数値が min ~ max の範囲内か確認する

int value_1 = 50;
bool ret_1 = value_1.IsInRange(0, 100);
// > ret_1=true, 変数値が0~100の範囲内なのでtrue

int value_2 = 200;
bool ret_2 = value_2.IsInRange(0, 100);
// > ret_2=flase, 変数値が0~100の範囲外なのでfalse

IsInRangeOrMore(min): 変数値が min 以上か確認する

int value_1 = 100;
bool ret_1 = value_1.IsInRangeOrMore(50);
// > ret_1=true, 変数値が50以上なのでtrue

int value_2 = 100;
bool ret_2 = value_2.IsInRangeOrMore(200);
// > ret_2=flase, 変数値が200以上ではないのでfalse

IsInRangeOrLess(max):変数値が min 以下か確認する

int value_1 = 50;
bool ret_1 = value_1.IsInRangeOrLess(100);
// > ret_1=true, 変数値が100以下なのでtrue

int value_2 = 50;
bool ret_2 = value_2.IsInRangeOrLess(30);
// > ret_2=flase, 変数値が30以下ではないのでfalse

Clamp(min, max):範囲内に収まる値を返す

int value_1 = 100;
int ret_1 = value_1.Clamp(20, 30);
// > ret_1=30, 30以上なので範囲内の最大値30を返す

int value_2 = 10;
int ret_2 = value_2.Clamp(20, 30);
// > ret_2=20, 20以下なので範囲内の最小値20を返す

ClampSelf(min, max):変数値を範囲内に収める

int value_1 = 100;
value_1.ClampSelf(20, 30);
// > value_1=30, 30以上なので範囲内の最大値30を設定する

int value_2 = 10;
value_2.ClampSelf(20, 30);
// > ret_2=20, 20以下なので範囲内の最小値20を設定する

実装コード

使い方の項目のメソッドを各基本型に拡張メソッドとして実装します。

RangeExtensionクラス

全部のコードがここに入っています。死ぬほど長いので全部コピペしてエディタ上で見たほうがいいかもしれません。

/// <summary>
/// 基本型の機能を拡張し、範囲に関係する機能を追加します。
/// </summary>
public static class RangeExtension
{
    // (1) 現在値が範囲内なら代入してtrue、範囲外の場合代入せずにfalse
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool Assign(this ref byte self, byte newValue, byte min, byte max)
    {
        if (self < min || self > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool Assign(this ref sbyte self, sbyte newValue, sbyte min, sbyte max)
    {
        if (self < min || self > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool Assign(this ref decimal self, decimal newValue, decimal min, decimal max)
    {
        if (self < min || self > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool Assign(this ref double self, double newValue, double min, double max)
    {
        if (self < min || self > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool Assign(this ref float self, float newValue, float min, float max)
    {
        if (self < min || self > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool Assign(this ref int self, int newValue, int min, int max)
    {
        if (self < min || self > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool Assign(this ref uint self, uint newValue, uint min, uint max)
    {
        if (self < min || self > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool Assign(this ref long self, long newValue, long min, long max)
    {
        if (self < min || self > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool Assign(this ref ulong self, ulong newValue, ulong min, ulong max)
    {
        if (self < min || self > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool Assign(this ref short self, short newValue, short min, short max)
    {
        if (self < min || self > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool Assign(this ref ushort self, ushort newValue, ushort min, ushort max)
    {
        if (self < min || self > max) return false;
        self = newValue;
        return true;
    }

    // (2) 現在値が最小値以上なら代入してtrue、下回っている場合代入せずにfalse
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrMore(this ref byte self, byte newValue, byte min)
    {
        if (self < min) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrMore(this ref sbyte self, sbyte newValue, sbyte min)
    {
        if (self < min) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrMore(this ref decimal self, decimal newValue, decimal min)
    {
        if (self < min) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrMore(this ref double self, double newValue, double min)
    {
        if (self < min) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrMore(this ref float self, float newValue, float min)
    {
        if (self < min) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrMore(this ref int self, int newValue, int min)
    {
        if (self < min) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrMore(this ref uint self, uint newValue, uint min)
    {
        if (self < min) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrMore(this ref long self, long newValue, long min)
    {
        if (self < min) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrMore(this ref ulong self, ulong newValue, ulong min)
    {
        if (self < min) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrMore(this ref short self, short newValue, short min)
    {
        if (self < min) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrMore(this ref ushort self, ushort newValue, ushort min)
    {
        if (self < min) return false;
        self = newValue;
        return true;
    }

    // (3) 現在値が最大値以下なら代入してtrue、上回っている場合代入せずにfalse
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrLess(this ref byte self, byte newValue, byte max)
    {
        if (newValue > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrLess(this ref sbyte self, sbyte newValue, sbyte max)
    {
        if (newValue > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrLess(this ref decimal self, decimal newValue, decimal max)
    {
        if (newValue > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrLess(this ref double self, double newValue, double max)
    {
        if (newValue > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrLess(this ref float self, float newValue, float max)
    {
        if (newValue > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrLess(this ref int self, int newValue, int max)
    {
        if (newValue > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrLess(this ref uint self, uint newValue, uint max)
    {
        if (newValue > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrLess(this ref long self, long newValue, long max)
    {
        if (newValue > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrLess(this ref ulong self, ulong newValue, ulong max)
    {
        if (newValue > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrLess(this ref short self, short newValue, short max)
    {
        if (newValue > max) return false;
        self = newValue;
        return true;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AssignIfOrLess(this ref ushort self, ushort newValue, ushort max)
    {
        if (newValue > max) return false;
        self = newValue;
        return true;
    }

    // (3) 現在値が範囲内に収まっているか確認する、true : 範囲内 / false : 範囲外
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool IsInRange(this byte self, byte min, byte max) => max <= self && self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRange(this sbyte self, sbyte min, sbyte max) => max <= self && self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRange(this decimal self, decimal min, decimal max) => max <= self && self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRange(this double self, double min, double max) => max <= self && self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRange(this float self, float min, float max) => max <= self && self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRange(this int self, int min, int max) => max <= self && self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRange(this uint self, uint min, uint max) => max <= self && self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRange(this long self, long min, long max) => max <= self && self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRange(this ulong self, ulong min, ulong max) => max <= self && self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRange(this short self, short min, short max) => max <= self && self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRange(this ushort self, ushort min, ushort max) => max <= self && self >= min;

    // (4) 現在値が指定した最小値以上かどうか確認する、true : 最小値以上 / false : 最小値未満
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrMore(this byte self, byte min) => self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrMore(this sbyte self, sbyte min) => self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrMore(this decimal self, decimal min) => self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrMore(this double self, double min) => self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrMore(this float self, float min) => self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrMore(this int self, int min) => self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrMore(this uint self, uint min) => self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrMore(this long self, long min) => self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrMore(this ulong self, ulong min) => self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrMore(this short self, short min) => self >= min;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrMore(this ushort self, ushort min) => self >= min;

    // (5) 現在値が指定した最大値以下かどうか確認する、true : 最大値以下 / false : 最大値より大きい
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrLess(this byte self, byte max) => max <= self;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrLess(this sbyte self, sbyte max) => max <= self;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrLess(this decimal self, decimal max) => max <= self;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrLess(this double self, double max) => max <= self;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrLess(this float self, float max) => max <= self;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrLess(this int self, int max) => max <= self;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrLess(this uint self, uint max) => max <= self;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrLess(this long self, long max) => max <= self;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrLess(this ulong self, ulong max) => max <= self;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrLess(this short self, short max) => max <= self;
    [MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static bool IsInRangeOrLess(this ushort self, ushort max) => max <= self;

    // (6) 自分自身から範囲内に収まるように値を取得します。
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static byte Clamp(this byte self, byte min, byte max)
    {
        byte value = self;
        if (self < min) { value = min; }
        else if (self > max) { value = max; }
        return value;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static sbyte Clamp(this sbyte self, sbyte min, sbyte max)
    {
        sbyte value = self;
        if (self < min) { value = min; }
        else if (self > max) { value = max; }
        return value;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static decimal Clamp(this decimal self, decimal min, decimal max)
    {
        decimal value = self;
        if (self < min) { value = min; }
        else if (self > max) { value = max; }
        return value;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static double Clamp(this double self, double min, double max)
    {
        double value = self;
        if (self < min) { value = min; }
        else if (self > max) { value = max; }
        return value;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static float Clamp(this float self, float min, float max)
    {
        float value = self;
        if (self < min) { value = min; }
        else if (self > max) { value = max; }
        return value;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static int Clamp(this int self, int min, int max)
    {
        int value = self;
        if (self < min) { value = min; }
        else if (self > max) { value = max; }
        return value;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static uint Clamp(this uint self, uint min, uint max)
    {
        uint value = self;
        if (self < min) { value = min; }
        else if (self > max) { value = max; }
        return value;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static long Clamp(this long self, long min, long max)
    {
        long value = self;
        if (self < min) { value = min; }
        else if (self > max) { value = max; }
        return value;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static ulong Clamp(this ulong self, ulong min, ulong max)
    {
        ulong value = self;
        if (self < min) { value = min; }
        else if (self > max) { value = max; }
        return value;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static short Clamp(this short self, short min, short max)
    {
        short value = self;
        if (self < min) { value = min; }
        else if (self > max) { value = max; }
        return value;
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static ushort Clamp(this ushort self, ushort min, ushort max)
    {
        ushort value = self;
        if (self < min) { value = min; }
        else if (self > max) { value = max; }
        return value;
    }

    // (7) 自分自身をが範囲外なら範囲内に収まるように調整します。
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void ClampSelf(this ref byte self, byte min, byte max)
    {
        if (self < min) { self = min; }
        else if (self > max) { self = max; }
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void ClampSelf(this ref sbyte self, sbyte min, sbyte max)
    {
        if (self < min) { self = min; }
        else if (self > max) { self = max; }
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void ClampSelf(this ref decimal self, decimal min, decimal max)
    {
        if (self < min) { self = min; }
        else if (self > max) { self = max; }
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void ClampSelf(this ref double self, double min, double max)
    {
        if (self < min) { self = min; }
        else if (self > max) { self = max; }
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void ClampSelf(this ref float self, float min, float max)
    {
        if (self < min) { self = min; }
        else if (self > max) { self = max; }
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void ClampSelf(this ref int self, int min, int max)
    {
        if (self < min) { self = min; }
        else if (self > max) { self = max; }
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void ClampSelf(this ref uint self, uint min, uint max)
    {
        if (self < min) { self = min; }
        else if (self > max) { self = max; }
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void ClampSelf(this ref long self, long min, long max)
    {
        if (self < min) { self = min; }
        else if (self > max) { self = max; }
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void ClampSelf(this ref ulong self, ulong min, ulong max)
    {
        if (self < min) { self = min; }
        else if (self > max) { self = max; }
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void ClampSelf(this ref short self, short min, short max)
    {
        if (self < min) { self = min; }
        else if (self > max) { self = max; }
    }
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void ClampSelf(this ref ushort self, ushort min, ushort max)
    {
        if (self < min) { self = min; }
        else if (self > max) { self = max; }
    }
}

【C#】Anyメソッド解説 & 範囲指定できるように拡張する

LinqのAnyメソッド使い方と範囲指定できるように機能拡張を行います。

Anyメソッドとは

簡単な説明

まず初めにAnyメソッドの説明です。

AnyメソッドはIEnumerableの拡張メソッド(=Linq)として定義されていて指定した配列やリストに中身があるかどうかを判定できます。

// Anyの宣言(1):中身があるかどうか判定できる
public static bool Any<TSource>(this IEnumerable<TSource> source);
// Anyの宣言(2):predicateで指定した条件が中身に存在するか確認できる
public static bool Any<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);

Anyメソッドの使い方

使い方は以下の通りです。

// Anyの宣言(1)の使い方
public static bool Any<TSource>(this IEnumerable<TSource> source);

public void Foo()
{
    // リストに中身があるかどうか判定する
    
    var list_1 = new List<int>();
    bool result_1 list_1.Any();
    // > result_1 = false : 空のリストの場合 false
    
    var list_2 = new List<int>() { 0, 1, 2, 3, 4, 5, 6 };
    bool result_2 = list_2.Any();
    // > result_2 = true : 中身があれば true
}

// Anyの宣言(2):の使い方
public static bool Any<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);

public void Foo()
{
    // リストに条件に一致する中身があるかどうか判定する
    
    var list_1 = new List<int>();
    bool result_1 list_1.Any(p => p > 5 /*5以上の要素があるか?*/);
    // > result_1 = false : 条件を指定しても空なので false
    
    var list_2 = new List<int>() { 0, 1, 2, 3, 4, 5, 6 };
    bool result_2 = list_2.Any(p => p > 5 /*5以上の要素があるか?*/);
    // > result_2 = true : 5, 6が条件に一致するため true, p > 10 とすれば false
}

サンプルではListに対してAnyメソッドを実行していますが配列に対しても同様の操作が可能です。

更に、IEnumerableを継承しているほかの型でもAnyで確認ができます。

Anyメソッドは使用頻度(低)

余談ですがAnyメソッドは単体では使う機会はほぼ無いです。要素があるかどうかは「Count」や「Length」で確認できます。配列とListで操作を統一できるメリットはありますが、内部でイテレータを使用しているため速度がCountやLengthよりかなり遅いです。

それに条件を指定するほうも、同じLinqでContainsと機能がほぼ被っているのでメソッド名から意図がくみ取りやすいContainsを使用します。最近見かけませんが、昔に Linq to Object or SQL という構文中で割と使っていましたが最近ほぼ使わないですね。

Anyに範囲を指定するように拡張する

タイトルの件ですが、そんなAnyメソッドですが利用性を向上するために範囲指定できるようにしたいと思います。

EnumerableExtensionクラス

以下の通りEnumerableExtensionクラスを作成してIEnumerableの拡張メソッドとして処理を作成したいと思います。

// IEnumerableの拡張メソッドを定義するクラス
public static class EnumerableExtension
{
    // (1) 要素の開始位置を指定して要素の有無を確認します。
    public static bool Any<TSource>(this IEnumerable<TSource> source, 
        Func<TSource, bool> predicate, int startIndex)
    {
        int len = source.Count();
        if (startIndex < 0 || startIndex > len)
            throw new ArgumentOutOfRangeException(nameof(startIndex), $"inde={startIndex}");
        
        int i = 0;
        foreach (var item in source)
        {
            if (i++ < startIndex) continue;
            if (predicate(item)) return true;
        }
        return false;
    }

    // (2) 要素の対象範囲を指定して要素の有無を確認します。
    public static bool Any<TSource>(this IEnumerable<TSource> source, 
        Func<TSource, bool> predicate, int startIndex, int endIndex)
    {
        int len = source.Count();
        if (startIndex < 0 || startIndex > len) 
            throw new ArgumentOutOfRangeException(nameof(startIndex), $"nameof(startIndex)={startIndex}");
        if (endIndex < 0 || endIndex > len)
            throw new ArgumentOutOfRangeException(nameof(endIndex), $"nameof(endIndex)={endIndex}");

        int i = 0;
        foreach (var item in source)
        {
            if (i > endIndex) return false;
            if (i++ < startIndex) continue;
            if (predicate(item)) return true;
        }
        return false;
    }
}

指定できる範囲の指定は配列の要素番号で行います。

使い方

上記メソッドの使い方はそれぞれ以下の通りです。

拡張メソッドとして作成したためAnyと同じようにListに対してメソッド呼び出しで処理を記述できます。

public void Foo(params string[] args)
{
    // (1) 要素の開始位置を指定して要素の有無を確認する
    var list = new List<int>() { 0, 1, 2, 3, 4, 5, 6 };
    bool result_1 = list.Any(p => p > 5, 3); // リストの3つ目以降に5以上の値があるか?
    Console.WriteLine(result_1);
    // result_1 = true : p > 10 等にすると存在しないのでfalse
    
    // (2) 要素の対象範囲を指定して要素の有無を確認する
    bool result_2 = list.Any(p => p == 3, 2, 3); // リストの2つ目 ~ 4つ目の範囲内に3が存在するか?
    Console.WriteLine(result_2);
    // result_2 = true : p == 10 等にすると存在しないのでfalse
}

【参考】Anyメソッドの実装

.NET(Framework)のソースコードはReferencesourceというサイトで中身が確認できます。

今回紹介したAnyメソッドの実装は以下の通りです。

https://referencesource.microsoft.com/#System.Core/System/Linq/Enumerable.cs,8788153112b7ffd0

public static bool Any<TSource>(this IEnumerable<TSource> source) {
    if (source == null) throw Error.ArgumentNull("source");
    using (IEnumerator<TSource> e = source.GetEnumerator()) {
        if (e.MoveNext()) return true;
    }
    return false;
}

public static bool Any<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) {
    if (source == null) throw Error.ArgumentNull("source");
    if (predicate == null) throw Error.ArgumentNull("predicate");
    foreach (TSource element in source) {
        if (predicate(element)) return true;
    }
    return false;
}

見ればわかると思いますが、Any()はイテレータで最初の要素が取れればtrueで要素があると判定しています。

あんまり効率が良くなさそうです。

Any(predicate)は最初から最後までforeachでループして要素があるか確認してます。IEnumerableは長さが無限の場合が稀にあるので安易に実行すると処理が戻ってこない可能性があります。まぁ滅多に無いので頭の片隅に置いておくくらいで大丈夫です。

【C#】配列に要素を追加・削除する、中身をシャッフルする

C#の配列に要素を追加したり削除したり中身をランダム化する方法の紹介です。

配列は一度宣言してしまうとサイズ変更は(基本的に)できないです。そういった事がした場合は動的リストの「System.Collections.Generic」名前空間にある「List」を使用しますが、パフォーマンスや制約などで配列でデータを持っているとろへ要素を追加したりしたくなったのでコードを書いてみました。

先に注意点ですが、(基本的に)できないことを行っているのでパフォーマンスが同様の操作をListに対して行うよりだいぶ悪いと思います。もし要素の追加・削除を頻繁に行うようであればListの仕様を検討ください。

実装コード

かなり長いので、このコードはコピペして使い方の項目まで飛ばしても問題ありません。

ArrayExtensionクラス

各操作を配列に対する拡張メソッドの形式で定義しています。

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

/// <summary>
/// 配列に対する拡張機能を提供します。
/// </summary>
public static class ArrayExtension
{
    //
    // (1) 配列の要素に対する基本操作
    // - - - - - - - - - - - - - - - - - - - -

    /// <summary>
    /// 配列に対する操作 <see cref="Array.Exists{T}(T[], Predicate{T})"/> を標準の検索方法で拡張メソッド化します。
    /// </summary>
    public static bool Exists<T>(this T[] array, T item) => Array.Exists(array, p => p.Equals(item));

    /// <summary>
    /// 配列に対する操作 <see cref="Array.Exists{T}(T[], Predicate{T})"/> を拡張メソッド化します。
    /// </summary>
    public static bool Exists<T>(this T[] array, Predicate<T> match) => Array.Exists(array, match);

    /// <summary>
    /// 配列に対する操作 <see cref="Array.Find{T}(T[], Predicate{T})"/> を標準の検索方法で拡張メソッド化します。
    /// </summary>
    public static T Find<T>(this T[] array, T item) => Array.Find(array, p => p.Equals(item));

    /// <summary>
    /// 配列に対する操作 <see cref="Array.Find{T}(T[], Predicate{T})"/> を拡張メソッド化します。
    /// </summary>
    public static T Find<T>(this T[] array, Predicate<T> match) => Array.Find(array, match);

    /// <summary>
    /// 配列に対する操作 <see cref="Array.FindIndex{T}(T[], Predicate{T})"/> を拡張メソッド化します。
    /// (見つからない場合-1)
    /// </summary>
    public static int FindIndex<T>(this T[] array, T item) => Array.FindIndex(array, p => p.Equals(item));

    /// <summary>
    /// 配列に対する操作 <see cref="Array.FindIndex{T}(T[], Predicate{T})"/> を準の検索方法で拡張メソッド化します。
    /// (見つからない場合-1)
    /// </summary>
    public static int FindIndex<T>(this T[] array, Predicate<T> match) => Array.FindIndex(array, match);

    //
    // (2) 配列に要素を追加する
    // - - - - - - - - - - - - - - - - - - - -

    /// <summary>
    /// 配列の先頭に値を追加し、値が追加された新しい配列を取得します。
    /// </summary>
    public static T[] InsertTop<T>(this T[] array, T value)
    {
        var newArray = new T[array.Length + 1];
        newArray[0] = value;
        Array.Copy(array, 0, newArray, 1, array.Length);
        return newArray;
    }

    /// <summary>
    /// 配列の最後に値を追加し、値が追加された新しい配列を取得します。
    /// </summary>
    public static T[] InsertLast<T>(this T[] array, T value)
    {
        var newArray = new T[array.Length + 1];
        Array.Copy(array, 0, newArray, 0, array.Length);
        newArray[newArray.Length - 1] = value;
        return newArray;
    }

    /// <summary>
    /// 指定した位置に値を追加し、値が追加された新しい配列を取得します。
    /// </summary>
    public static T[] Insert<T>(this T[] array, int index, T value)
    {
        if (array == null)
            throw new ArgumentNullException(nameof(array));
        if (index < 0 || index >= array.Length)
            throw new ArgumentOutOfRangeException($"index is out of range. index={index}.");

        var newArray = new T[array.Length + 1];
        Array.Copy(array, 0, newArray, 0, index); // インデックスより前
        newArray[index] = value;
        Array.Copy(array, index, newArray, index + 1, array.Length - index);

        return newArray;
    }

    /// <summary>
    /// 指定した位置に collection で指定した要素を連続で追加し、値が追加された新しい配列を取得します。
    /// </summary>
    public static T[] InsertRange<T>(this T[] array, int index, IEnumerable<T> collection)
    {
        if (array == null)
            throw new ArgumentNullException(nameof(array));
        if (index < 0 || index >= array.Length)
            throw new ArgumentOutOfRangeException($"index is out of range. index={index}.");

        int len = collection.Count();
        var newArray = new T[array.Length + len];

        Array.Copy(array, 0, newArray, 0, index); // インデックスより前

        int i = 0;
        foreach (var item in collection)
        {
            newArray[index + i++] = item;
        }
        Array.Copy(array, index, newArray, index + len, array.Length - index);

        return newArray;
    }

    //
    // (3) 特定の要素を削除
    // - - - - - - - - - - - - - - - - - - - -

    // 補足:
    // 以下の削除処理は、メモリ効率と動作速度がかなり悪いため
    // 頻繁にこのような操作が発生する場合は System.Collections.Generic.List<T> の使用を検討すること。

    /// <summary>
    /// 配列から指定した位置の要素を削除した新しい配列を取得します。
    /// </summary>
    public static T[] RemoveAt<T>(this T[] array, int index)
    {
        if (array == null)
            throw new ArgumentNullException(nameof(array));
        if (index < 0 || index >= array.Length)
            throw new ArgumentOutOfRangeException($"index is out of range. index={index}.");

        var newArray = new T[array.Length - 1];
        Array.Copy(array, 0, newArray, 0, index); // インデックスより前
        Array.Copy(array, index + 1, newArray, index, array.Length - index - 1); // インデックスより後

        return newArray;
    }

    /// <summary>
    /// 配列のいちばん最初に見つかった1つの要素を削除し新しい配列を取得します。削除されなかった場合null を返します。
    /// </summary>
    public static T[] RemoveFirst<T>(this T[] array, T item)
    {
        int index = array.FindIndex(item);
        if (index == -1) return null;

        return array.RemoveAt(index);
    }

    /// <summary>
    /// 指定した条件を満たす配列のいちばん最初に見つかった要素を削除し新しい配列を取得します。
    //// 削除されなかった場合null を返します。
    /// </summary>
    public static T[] RemoveFirst<T>(this T[] array, Predicate<T> match)
    {
        int index = array.FindIndex(match);
        if (index == -1) return null;

        return array.RemoveAt(index);
    }

    /// <summary>
    /// 指定した要素と同じ値を配列から全て削除します。削除されなかった場合 null を返します。
    /// </summary>
    public static T[] RemoveAll<T>(this T[] array, T item) => array.RemoveAll(elem => elem.Equals(item));

    /// <summary>
    /// 指定した条件を満たす要素を配列から全て削除します。削除されなかった場合 null を返します。
    /// </summary>
    public static T[] RemoveAll<T>(this T[] array, Predicate<T> match)
    {
        var list = new List<T>();
        for (int i = 0; i < array.Length; i++)
        {
            if (!match(array[i]))
            {
                list.Add(array[i]);
            }
        }
        return list.Count == array.Length ? null : list.ToArray();
    }

    //
    // (4) 配列に対するランダムな操作
    // - - - - - - - - - - - - - - - - - - - -

    /// <summary>
    /// 配列からランダムに要素を1つ取り出します。
    /// </summary>
    public static T PickupOne<T>(this T[] array)
    {
        return array[Rand.Next(0, array.Length)];
    }

    /// <summary>
    /// 配列からランダムに1つ要素を取り出した後その要素を配列から削除します。
    /// </summary>
    public static (T[] /*newArray*/, T /*poppedItem*/) PickupOneAndRemove<T>(this T[] array)
    {
        var newArray = new T[array.Length - 1];
        int index = Rand.Next(0, array.Length);
        T item = array[index];
        for (int i = 0, j = 0; i < array.Length; i++)
        {
            if (i == index)
            {
                continue;
            }
            newArray[j++] = array[i];
        }
        return (newArray, item);
    }

    /// <summary>
    /// 指定した配列をランダムに並び替えます。
    /// </summary>
    public static void Shuffle<T>(this T[] array)
    {
        for (int i = 0; i < array.Length; i++)
        {
            array.Swap(i, Rand.Range(0, array.Length));
        }
    }

    // 元の配列はそのままで新しいランダムな配列を作る取得します。

    /// <summary>
    /// 指定した配列からランダム化された新しい配列を作成・取得します。
    /// </summary>
    public static T[] GetNewRandomArray<T>(this T[] array)
    {
        var newArray = new T[array.Length];
        Array.Copy(array, newArray, array.Length);
        newArray.Shuffle();
        return newArray;
    }

    /// <summary>
    /// 配列の指定した2つのインデックス間の値を入れ替えます。
    /// </summary>
    public static void Swap<T>(this T[] array, int i, int j)
    {
        T tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

    //
    // (5) Linq風の便利な操作
    // - - - - - - - - - - - - - - - - - - - -

    /// <summary>
    /// 指定した配列をリストに変換します。
    /// </summary>
    public static List<T> ToList<T>(this T[] array)
    {
        var list = new List<T>();
        for (int i = 0; i < array.Length; i++)
        {
            list.Add(array[i]);
        }
        return list;
    }

    /// <summary>
    /// 指定した配列を述語に従って新しい型の配列に変換します。
    /// </summary>
    public static Dest[] Convert<T, Dest>(this T[] array, Func<T, Dest> func)
    {
        var newArray = new Dest[array.Length];
        for (int i = 0; i < array.Length; i++)
        {
            newArray[i] = func(array[i]);
        }
        return newArray;
    }

    /// <summary>
    /// 配列に対する操作 <see cref="Array.ForEach{T}(T[], Action{T}))"/> を拡張メソッド化します。
    /// </summary>
    public static void ForEach<T>(this T[] array, Action<T> action)
    {
        Array.ForEach(array, action);
    }

    //
    // (6) 配列に対するソート操作
    // - - - - - - - - - - - - - - - - - - - -

    /// <summary>
    /// <see cref="Array.Sort(Array)"/> を拡張メソッド化します。
    /// </summary>
    public static void Sort<T>(this T[] array) => Array.Sort(array);

    /// <summary>
    /// <see cref="Array.Sort{T}(T[], Comparison{T})"/> を拡張メソッド化します。
    /// </summary>
    public static void Sort<T>(this T[] array, Comparison<T> comparer) => Array.Sort(array, comparer);
}

使い方

以下、上記クラスの使い方になります。

配列にに要素が含まれているかどうかを確認・検索する(Exist, Find系)

要素の追加・削除などの前に配列に要素が存在するかどうかを確認するExist、要素がどの位置に存在するのかを確認するFindメソッドの使い方になります。

public static void Main(params string[] args)
{
    //
    // (1) 配列の要素に対する基本操作
    // - - - - - - - - - - - - - - - - - - - -
    int[] array = new int[] { 1, 2, 3, 4, 5, 6 };

    // (1-1) 要素が存在するか確認する
    string msg = array.Exists(3) ? "要素は存在します。" : "要素は存在しません。";
    // > msg = 要素は存在します。

    // (1-2) 条件を指定の要素が存在するか確認する
    string msg2 = array.Exists(elem => elem >= 3) ? "要素は存在します。" : "要素は存在しません。";
    // > msg2 = 要素は存在します。

    // (2-1) 指定した値を取得
    var value = array.Find(5);
    // > value = 5

    // (2-2) 条件を指定して要素を取得
    int value2 = array.Find(elem => elem > 5); // 5より大きい最初の値を取得(例としてちょっと微妙
    // > value2 = 6

    // (3-1) 指定した位置の要素を取得
    int index = array.FindIndex(3);
    Console.WriteLine($"3 は index={index} の位置にに存在します。");

    // (3-2) 指定した条件に一致する要素を取得
    int index2 = array.FindIndex(elem => elem >= 3);
    Console.WriteLine($"elem > 3 を満たす要素は index={index2} の位置にに存在します。");
}

配列に値を追加・挿入する(Insert系)

配列に対して値を追加・挿入する操作の説明です。

注意点として、Insertすると配列の要素数が増えますが、元の配列はそのままにして戻り値に要素が追加された配列が帰ってくるので追加したら以降はその配列を使用します。

public static void Main(params string[] args)
{
    //
    // (2) 配列に要素を追加する
    // - - - - - - - - - - - - - - - - - - - -
    int[] array = new int[] { 1, 2, 3, 4, 5 };

    // (1) 先頭に要素を追加する
    int[] array1 = array.InsertTop(99);
    // > array1 = 99, 1, 2, 3, 4, 5

    // (2) 末尾に要素を追加する
    int[] array2 = array.InsertLast(99);
    // > array2 = 1, 2, 3, 4, 5, 99

    // (3) 指定した位置に要素を追加する
    int[] array3 = array.Insert(2, 99);
    // > array3 = 1, 2, 99, 3, 4, 5

    // (4) 指定した位置に複数の要素を追加する
    int[] array4 = array.InsertRange(3, new int[] { 99, 98, 97, 96 });
    // > array4 = 1, 2, 3, 99, 98, 97, 96, 4, 5
}

配列から値を削除する(Remove系)

配列から特定の要素を削除する処理の説明です。

これもInsert系と同じく元の配列はそのままで、戻り値に値が削除された配列が戻るので以降これを使用します。

public static void Main(params string[] args)
{
    //
    // (3) 特定の要素を削除
    // - - - - - - - - - - - - - - - - - - - -
    int[] array = new int[] { 1, 2, 3, 3, 4, 4, 5 };

    // (1) 指定した位置の要素を削除
    int[] array1 = array.RemoveAt(3);
    // array1 = 1, 2, 3, 4, 4, 5 (3がひとつ削除された新しい配列が戻る)

    // (2-1) 最初に見つかった要素を削除
    int[] array2 = array.RemoveFirst(2);
    // array2 = 1, 3, 3, 4, 4, 5 (2が削除された新しい配列が戻る)

    // (2-2) 指定した条件を満たす最初に見つかった要素を削除
    int[] array3 = array.RemoveFirst(elem => elem > 3); // 3より大きい最初の要素を削除
    // array3 = 1, 2, 3, 3, 4, 5 (4がひとつ削除された新しい配列が戻る)

    // (3-1) 指定した値に一致する要素をすべて削除
    int[] array4 = array.RemoveAll(3);
    // array4 = 1, 2, 4, 4, 5 (3が全部削除された新しい配列が戻る)

    // (3-2) 指定した条件に一致するすべての要素を削除
    int[] array5 = array.RemoveAll(elem => elem > 3); // 3より大きい要素を全部削除
    // array5 = 1, 2, 3, 3
}

配列に対しランダムな操作を行う

以下は、ゲームでは割とよく使う配列に対するランダムな操作の説明です。

配列からランダムに1つ値を取り出す(PickupOneメソッド)

配列の中からランダムに値を1つ選択して取り出します。

public static void Main(params string[] args)
{
    int[] array = new int[] { 1, 2, 3, 4, 5 };

    // ランダムに1つ取り出す
    int item = array.PickupOne();
    // > 2
}
配列からランダムに1つ値を取り出して要素を削除する(PickupOneAndRemoveメソッド)

上記のPickupメソッドに似ていますが、取り出した後に値を配列から値を削除します。

削除後の配列と取り出したデータは戻り値のタプルで受け取れます。

public static void Main(params string[] args)
{
    int[] array = new int[] { 1, 2, 3, 4, 5 };

    // ランダムに1つ取り出して要素を削除
    var (newArray, poppedItem) = array.PickupOneAndRemove();
    // > newArray  = 1, 3, 4, 5
    // > popedItem = 2
}
配列の内容をシャッフルする(Shuffleメソッド)

指定した配列の中身を全てランダムにシャッフルします。

public static void Main(params string[] args)
{
    int[] array = new int[] { 1, 2, 3, 4, 5 };

    // 配列をランダムに並び替える
    array.Shuffle();
    // > array = 3, 5, 1, 4, 2
}
シャッフルした新しい配列を取得する(GetNewRandomArrayメソッド)

上記のShuffleメソッドは元の配列に対してシャッフルしましたが、こちらは元の配列はそのままにしてシャッフル済みの新しい配列を戻り値で受け取れます。

public static void Main(params string[] args)
{
    // ランダムな新しい配列を取得する(元の配列はそのまま)
    var newArray2 = array.GetNewRandomArray();
    // > newArray2 = 2, 1, 5, 4, 3
}

Linq風の便利な操作

以下は、System.Linq風の処理を配列にも適用できるようにした処理です。

配列をリスト(List)に変換する(ToListメソッド)

配列をリスト(List)に変換します。List.ToArray()はLinqにあるのに逆が無いみたいなので実装しています。

public static void Main(params string[] args)
{
    int[] array = new int[] { 1, 2, 3, 4, 5 };

    // (1) 配列をリストに変換する
    List<int> list = array.ToList(); // 同じ型のリストに変換できる
}
配列を別の型の配列に変換する(Convertメソッド)

配列を別の型の配列に変換します。各要素に対してラムダ式で変換方法を指定することによって別の型に変換することができます。

public static void Main(params string[] args)
{
    int[] array = new int[] { 1, 2, 3, 4, 5 };

    // (2) 配列を他の型の配列に変換する
    float[] array2 = array.Convert(elem => (float)elem); // ラムダで変換条件を指定する
}
配列をforeachする(ForEachメソッド)

配列は通常のforeach文で使用できますが、このForEachメソッドを使うと処理内容をラムダで指定してLinq風に回す事ができます。

public static void Main(params string[] args)
{
    int[] array = new int[] { 1, 2, 3, 4, 5 };

    // (2) 配列を他の型の配列に変換する
    float[] array2 = array.Convert(elem => (float)elem); // ラムダで変換条件を指定する
}

配列に対するソート処理(Sort系)

以下、配列をソートする方法です。指定方法は性的メソッドの「Array.Sort」の方が多彩ですが、このコードを入れておくと配列に対するメソッドとして処理を行うことができるようになります。多分この方が直観的なのかと思います。機能不足の場合は各自実装を追加したほうがいいかもしれません。

public static void Main(params string[] args)
{
    int[] array = new int[] { 1, 2, 3, 4, 5 };
    array.Shuffle(); // (4)で紹介した内容をランダム化で内容をランダム化しておく
    // > 3, 1, 4, 5, 2

    // (1) 配列をソートする
    array.Sort();
    // > 1, 2, 3, 4, 5

    // (2) 整列条件を指定して配列をソートする
    array.Sort((a, b) => b - a); // この場合逆順の指定
    // > 5, 3, 4, 2, 1
}

リンク

ランダム化で使用しているRandクラスは以下リンクに掲載しているのでそちらも併せて確認ください。

takachan.hatenablog.com

すごく長くなってしましたが以上です。

Unityと.NET環境で乱数生成を共通化する

タイトルの通り、Unity環境下での乱数生成と非Unityの乱数生成を共通化しようという話です。

何故このようなことをするかというと、非UnityのMonoBehaviorも何もない環境で作成したアルゴリズムをUnityへインポートしたりするときに、UnityのRandomクラスと、.NET 標準のRandomクラスの性能が異なるためそういったコードをUnityインポート後にRandomな部分の書き換えが発生するのを抑えたいというのが動機です。

早速コードです。

/// <summary>
/// [デバッグ用] Unityとそれ以外の.NET環境で乱数を共通化するためのクラス
/// </summary>
public static class Rand
{
#pragma warning disable IDE1006

#if false // ← これを環境によって自分で切り替える

    //
    // Unityで使用するコード
    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    /// <summary>
    /// シート値を指定して乱数生成を初期化します。
    /// </summary>
    public static void Init(int seed) => Random.InitState(seed);

    /// <summary>
    /// 指定した整数の範囲内でランダムな値を取得します。maxは値に含まれない。
    /// </summary>
    public static int Next(int min, int max) => Random.Range(min, max);

    /// <summary>
    /// 指定した整数の範囲内でランダムな値を取得します。maxは値に含まれない。
    /// </summary>
    public static int Range(int min, int max) => Random.Range(min, max);

    /// <summary>
    /// 指定した浮動小数の範囲内でランダムな値を取得します。maxは値に含まれます。
    /// </summary>
    public static float Range(float min, int max) => Random.Range(min, max);

    /// <summary>
    /// 0.0f ~ 1.0fまでの範囲でランダムな値を取得します。
    /// </summary>
    public static float Value => Random.value; // .NETの一般的な命名規則との互換用

    /// <summary>
    /// 0.0f ~ 1.0fまでの範囲でランダムな値を取得します。
    /// </summary>
    public static float value => Value;
#else
    //
    // Unity以外の通常のシステムで使用するコード
    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    private static Random _r = new Random();

    /// <summary>
    /// シート値を指定して乱数生成を初期化します。
    /// </summary>
    public static void Init(int seed) => _r = new Random(seed);

    /// <summary>
    /// 指定した整数の範囲内でランダムな値を取得します。maxは値に含まれない。
    /// </summary>
    public static int Next(int min, int max) => _r.Next(min, max);

    /// <summary>
    /// 指定した整数の範囲内でランダムな値を取得します。maxは値に含まれない。
    /// </summary>
    public static int Range(int min, int max) => _r.Next(min, max);

    /// <summary>
    /// 指定した浮動小数の範囲内でランダムな値を取得します。maxは値に含まれます。
    /// </summary>
    public static float Range(float min, int max) => throw new NotSupportedException();

    /// <summary>
    /// 0.0f ~ 1.0fまでの範囲でランダムな値を取得します。
    /// </summary>
    public static float Value => (float)_r.NextDouble();

    /// <summary>
    /// 0.0f ~ 1.0fまでの範囲でランダムな値を取得します。
    /// </summary>
    public static float value => Value; // Unityの命名規則の互換用
#endif

#pragma warning restore IDE1006
}

同じシグネチャーのメソッドをUnity用、それ以外用の両方でifdefで区切って定義します。

これで冒頭の「#if false」の部分をUnityならtrue、それ以外ならfalseとして各々のコードから使用します。

環境ごとに中身の実装が違うのでちょっと乱数の出かたが異なり(floatの範囲選択が.NET 環境ではできないですが…)Randクラスを使用する事で冒頭の書き換えるなどの課題が解消できると思います。

【C#】Unityと.NET標準ライブラリの命名規則の違い

.NET標準は「System名前空間内にあるクラス類」を指します。一方のUnityは「UnityEngine名前空間内にあるクラス類」以下にあるオブジェクト群を指します。それぞれ同じC#言語ですが各々の間で大きく命名規則が異なっています。という訳でちょっとまとめてみました。

命名規則の種類

命名規則にはいくつか代表的な形式があります。代表的な形式を以下に挙げます。

Item Description e.g.
Pascal 形式 先頭が大文字で後は小文字 "ItemList", "GameControlelr"
camel 形式 先頭が小文字で語句ごとに大文字 "itemList", "gameController"
snake(スネーク) 形式 全部小文字、語句をアンダースコアでつなぐ "item_list", "game_controller"
CONSTANT 形式 全部大文字、語句をアンダースコアでつなぐ "ITEM_LSIT, "GAME_CONTROLLER"

C#では(というか古くはMSの推奨として)、「snake 形式」と「CONSTANT 形式」は使用しません。従って、他言語では見かけることもある定数を「CONSTANT 形式」で宣言といった文化はありません。enumのメンバーも同様です。

public static class Cosntant
{
    // こういう命名規則は使用しない
    public const int ERROR_CODE = -1;

    // 一般的にこうする
    public const int ErrorCode = -1;
}

最近はIDE上で定数は特別な色で表示されるシーンも多いため命名規則で区別しないでも大丈夫みたいな側面はあると思います。個人的にはそこまで困ったことないですね。

識別子の命名規則

.NET標準とUnityの命名規則の違いを以下にまとめてみました。

種類 .NET標準 Unity e.g
クラス・構造体 Pascal形式 Pascal形式 class Sample { ...
抽象クラス Pascal形式 Pascal形式 abstract class Sample { ...
インターフェース Pascal形式 Pascal形式 interfalce class Sample { ...
列挙型(宣言) Pascal形式 Pascal形式 enum Color { ...
列挙型(メンバー) Pascal形式 Pascal形式 enum Color { Red, Blue...
デリゲート Pascal形式 Pascal形式 delegate void Foo(int, int)
定数(private以外) Pascal形式 Pascal形式 public const string Message
定数(private) Pascal形式 Pascal形式 private const string message
フィールド(private以外) 使わないで camel形式 public int transform
フィールド(private) _camel形式 (*1) _camel形式 (*2) private int [ _transform | m_transform ]
メソッド(private以外) Pascal形式 Pascal形式 public void Foo()
メソッド(private) Pascal形式 Pascal形式 private void Foo()
プロパティ(private以外) Pascal形式 camel形式 public int [ Count|count ] { get; }
プロパティ(private) calem形式 不明 あまり使わない
イベント(private以外) Pascal形式 camel形式 publc event Action [ Clicked|clickd ]
イベント(private) calem形式 不明 滅多に使わない
パラメータ(メソッド引数) calem形式 calem形式 public Foo(int count)
ネームスペース Pascal形式 Pascal形式 namespace UnityEngine, System
  • (*1) 以前からアンダースコアつけるべしでしたが最近スタティックには「s_」, スレッドは「t_」などが追加されて議論を呼んでいしたね。
  • (*2) 「m_」だったり「_」だったり形式は色々あるみたいです

「private以外」の表記は「public」「protected」「internal」「protected internal」「private protected(C#7.2から追加)」の4つを指します。つまり自クラス外に可視性があることを指しています。

リストから分かるかと思いますが、ほぼ Pascal 形式で Unity と .NET での違いは外部公開するフィールドとプロパティくらいです。

この命名規則ですが、private なプロパティとか同じクラスにある常数が Pascal 形式で見分けがつかないのが少し気になります。ただ、最近は IDE が種類ごとに細かく色を付けてくれるので完全に識別不能ではないと思います。

Unity 上で開発を行う場合、基本的にUnityの命名規則に従う方が良いと思いまが、Unityに依存しない汎用的なライブラリ類は.NET標準に寄せるなどの使い分けをしたほうがいいかもしれません。汎用アルゴリズムとしてgithub上にコードを公開する場合は .NET の規約に従ったほうが受け入れられやすいと思います。外部公開のプロパティが小文字のライブラリとか Unity のライブラリ以外で見た事がありません。

その他の規約や書き方

publicフィールドの扱い

Unity では変数をインスペクター上に表示するために public で宣言する事がありますが、やはりインスペクター以外の他のクラスから自由に書き換えできるのでできれば避けたいところです。

// Sample.cs
using UnityEngine;

public class Sample : MonoBehaviour
{
    public Vector3 offset; // インスペクター上で編集したい
}

文字数が多くなって面倒ですが(多人数で作業する場合特に)予期せず書き換えられて自分のクラスの処理(Updateなど)が失敗してしまう事もあり外から書き換えてほしくない場合は、最近は以下のように宣言します。

// Sample.cs
using UnityEngine;

public class Sample : MonoBehaviour
{
    // インスペクター上から編集できるが他のクラスからは見えなくなる
    [SerializeField] private Vector3 offset;
}

また、他のクラスから値を確認したい場合プロパティを経由して値の取得だけできるようにします。

// Sample.cs
using UnityEngine;

public class Sample : MonoBehaviour
{
    [SerializeField] private Vector3 offset;

    // (1) C# 6.0以前のプロパティ書き方(= Unity2018.3以前)
    public Vector3 Offset { get { return this.offset; } }

    // (2) C# 6.0以降の書き方 (= Unity2018.3以降)
    public Vector3 Offset => this.offset;
}

// 短く記述可能なので(2)がおすすめ

インスペクター上に表示しつつ読み書きしたい場合、以下のように記述します。

// Sample.cs
using UnityEngine;

public class Sample : MonoBehaviour
{
    [SerializeField] private Vector3 offset;

    // (1) C# 6.0以前のプロパティ書き方
    public Vector3 Offset
    {
        get { return this.offset; }
        set // 値の設定用のプロパティ宣言
        {
            if(value.x <= 2.0f)
            {
                return; // ★ある数値の場合は設定しないなどの値のチェックができる
            }
            this.offset = value;
        }
    }

    // ★(2) C#7.0 の書き方(get/set指定はC# 7.0が必要)
    // ★単純な値の出し入れなら短く書ける
    public Vector3 Offset { get => this.offset; set => this.offset = value; }

    public Vector3 Offset
    {
        get => this.offset; // ★getだけラムダ式で書くこともできる
        set
        {
            // ★長い場合普通の書き方ができる
            if(value.x <= 2.0f)
            {
                return;
            }
            this.offset = value;
        }
    }
}

thisキーワードの有無

クラスのメンバー指すときに使用する this キーワードですが基本は付けません。

.NET標準 Unity
つけない つけない

実際付け始めると this だらけになると視認性が低下します。(VS の設定はすごく昔は this ありだったような気がしますが(?)引きずって this まみれになってたりするのをたまに見かけます)this キーワードを使用するしないはプロジェクトによって異なると思いますが、基本使用しないと思います(this を使用しない場合、自分のクラスの static メンバーとインスタンスメンバーの区別がつかなくなりますがそれが問題になる事は殆どありません)

特定のケース「引数とクラスのフィールド名が同じ場合 this を付ける」とか「継承元のクラスを明示的に指す必要があり base キーワードを使用する時に this を明示したい」、「メソッドにnew キーワードを使用しているメソッドを base と区別するために明示する」など、一部の場面で base やパラメータと区別するために this が必要になるのでそういった場合 this が必要になります。

個人の場合は好きにしましょう。まぁホビーユースなら面倒なので付けないと思います。

フィールドメンバーの先頭にm_, s_

割と良くあると思いますが以下のような感じです。

// Unity
private Vector3 m_position;
private Transform m_transform;

// .NET
private float _value; // 基本的にアンダースコアを付ける
private static float s_value; // static は先頭に s_

基本的にUnityだとインスペクター上に表示するときは、「m_」 とか先頭の「_(アンダースコア)」は取り外してくれるし UnityEngine 内では使われまくっているので事実上標準です。但し使ってる理由が「メンバー変数がたくさんあって分類するため」だと少し考えちゃいますね。クラスの設計自体がインスペクターの表示に影響を受けることがあるため善し悪しです。

変数名の先頭にアンダースコア

前述の「m_」の事もありますが、これはプロジェクトや個人のポリシーで使用するか決めたほうがいいと思います。

.NET だと先頭に「s_」、「t_」を付けろだとか言っていますがまぁ慣れていないと気持ち悪い、他のプロジェクトと整合が取れない個人の分類法に反するなど色々あると思うので。

但し、プレフィックスは、this キーワードとすこぶる相性が悪いので例えば以下のように併用してしまうとめちゃくちゃです。

private int _count = 10;

public void Foo()
{
    this._count = 20; // thisと併用すると見た目が意味不明になる
}

こういうケースが発生しないように規約は調整する必要があります。

結局どうすればいいの?

他人に公開するつもりのあるコードはファイル先頭に以下を宣言します。

#pragma warning disable // コード内の警告を全て抑制する

// もしくは

#if RELEASE
#pragma warning disable
#endif

という冗談はさておき、特殊な規約を採用しているプロジェクトのコードが自分のプロジェクトに取り込まれた時にめちゃくちゃ警告が出るのは予防できそうですtが。

まぁでも規約は規約なので、守るかかもらないか、スタイルをどうするかは個人の場合は好みで決めて大丈夫です。何が良いという事はないです。

ただ自分の決めたポリシーと既存のコードの規約が違う場合に、Githubの規約にも記載ありましたが他人のコードの方を無理に直そうとするのはやめた方がいいです。普通に時間の無駄 or 反感を買うだけなので既存のスタイルがあるならそ少なくともそのファイル内はそのスタイルを踏襲して表記を統一しましょう。いくつか書き方の引き出しのセットがあって切り替えてくイメージです。

ちなみにゼロから開発するときにプロジェクト内の場所ごとに表記が違うのNGです。チームの場合はガイドラインを設けましょう。必ず。絶対に。ネット上にあるコーディング規約でJava由来のクソみたいなあまり良くないのが見つかりますがそれは採用してはいけません。

規約を決めたら CodeFX や Lint に規約を設定してツールで逐次チェックしているのが安いです(設定は大変だと思いますが最終的に安いです)

参考資料

MSDN、「名前付けガイドライン」:

https://docs.microsoft.com/ja-jp/dotnet/standard/design-guidelines/naming-guidelines

Unityの命名規則のフォーラム:

https://forum.unity.com/threads/c-naming-conventions-for-unity.135617/

.NET Core のコーディングガイドライン

https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/coding-style.md