【C#】文字列を分割するSplitメソッドをより使いやすいよう改善する

C# である文字列を特定の文字で複数の文字列に分割する Split メソッドの利用性を向上するための拡張メソッドの紹介です。

はじめに

Split メソッドっは大変便利ですがセパレーターに複数の文字を指定する場合や、文字列をセパレーターとして使う場合以下のように配列を宣言しないといけません。

string str = "a b c,d";

// 複数の区切り文字をしていする場合配列を用意しないといけない
str.Split(new char[] { ' ', '\t', '\n' }, StringSplitOptions.RemoveEmptyEntries);

// 複数の文字列をセパレーターとして使う場合配列を用意しないといけない
str.Split(new string[] { "\n", "\r\n" });

これだと少し面倒なので以下のように配列宣言が不要な拡張メソッドを作成したいと思います。

string str = "a b c,d";

// 配列宣言不要に変更
str.Split(StringSplitOptions.RemoveEmptyEntries, ' ', '\t', '\n');

// 配列宣言不要に変更
str.Split("\n", "\r\n" );

確認環境

この記事の動作確認環境は以下の通りです。

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

コンソールプログラムで動作を確認

実装コード

上記使用方法に合うように可変引数が指定できるようにパラメーターの順序を調整します。

C#の言語仕様上可変引数はメソッドの引数の順序の最後にしか指定できない制限があるためです。

以下をコードに追加すると冒頭のように配列宣言が不要になります。.NET 標準ライブラリのパラメーターの増え方の規則を無視して可変引数を引数の最後に移動しています。

public static class StringExtension
{
    public static string[] Split(this string self, int count, params char[] separator)
    {
        return self.Split(separator, count);
    }
    public static string[] Split(this string self, StringSplitOptions oprtions, params char[] separator)
    {
        return self.Split(separator, oprtions);
    }
    public static string[] Split(this string self, params string[] separator)
    {
        return self.Split(separator);
    }
    public static string[] Split(this string self, int count, 
        StringSplitOptions oprtions, params char[] separator)
    {
        return self.Split(separator, count, oprtions);
    }
    public static string[] Split(this string self, StringSplitOptions options, params string[] separator)
    {
        return self.Split(separator, options);
    }
    public static string[] Split(this string self, int count, 
        StringSplitOptions options, params string[] separator)
    {
        return self.Split(separator, count, options);
    }
}

【C#】プロパティにつけた属性を取得する方法

C#でプロパティにつけた属性を取得する方法の紹介です。自作のクラスなどでプロパティに付与したカスタム属性を取得してその値を利用する方法です。

確認環境

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

コンソールプログラムで動作を確認

実装コード

取得方法は以下の通りです。すべての処理で共通してインスタンスか型(Type)を指定して、属性を取得したいプロパティの名前を文字列で指定します。

この名前を文字列で指定するのは

public static class AttributeUtility
{
    //
    // T で指定したプロパティを1つだけ取得
    //

    // 型を指定して Public プロパティの属性を取得する
    public static T GetPropertyAttribute<T>(Type type, string name) where T : Attribute
    {
        var prop = type.GetProperty(name);
        if (prop == null)
        {
            Trace.WriteLine($"Property is not found. {name}");
            return default; // 指定したプロパティが見つからない
        }
        var att = prop.GetCustomAttribute<T>();
        if (att == null)
        {
            Trace.WriteLine($"Attribute is not found. {name}");
            return default; // 指定した属性が付与されていない
        }
        return att;
    }

    //
    // プロパティについている属性を全部取得
    //
    
    // インスタンスを指定して Public プロパティの属性を取得します。
    public static T GetPropertyAttribute<T>(object instance, string name) where T : Attribute
    {
        return GetPropertyAttribute<T>(instance.GetType(), name);
    }

    // 型を指定して Public プロパティに付与されているすべてのプロパティを取得する
    public static IEnumerable<Attribute> GetPropertyAttributes(Type type, string name)
    {
        var prop = type.GetProperty(name);
        if (prop == null)
        {
            Trace.WriteLine($"Property is not found. {name}");
            return default;
        }

        return prop.GetCustomAttributes<Attribute>();
    }

    // インスタンスを指定して Public プロパティに付与されているすべてのプロパティを取得する
    public static IEnumerable<Attribute> GetPropertyAttributes(object instance, string name)
    {
        return GetPropertyAttributes(instance.GetType(), name);
    }
}

public なプロパティしか対象としていませんがそれ以外のプロパティから属性を取得したい場合は、以下のように GetProperty() メソッドに BindingFlags を指定するように修正します。

// 以下の通りコードを修正する
var prop = 
    type.GetProperty(name, 
        BindingFlags.InvokeMethod | // ★ここから追加
        BindingFlags.NonPublic | 
        BindingFlags.Instance);

使いかた

例えば以下のような自作のカスタム属性を2つ用意し、それがプロパティに付与されているとします。

// 1つ目の属性
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class IDAttribute : Attribute
{
    public string ID { get; set; }
    
    public IDAttribute(string id)
    {
        this.ID = id;
    }
}

// 2つ目の属性
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class TagAttribute : Attribute
{
    public string Tag { get; set; }

    public TagAttribute(string tag)
    {
        this.Tag = tag;
    }
}

// プロパティに上記2つの属性を付与する
public class Sample
{
    [ID("ididid"), Tag("tagtag")]
    public string Name { get; set; }
}

上記の属性は以下のように取得します。

public static void Main(string[] args)
{
    var s = new Sample() { Name = "000", };

    // ひとつだけ属性を取得する場合
    var attribute = AttributeUtility.GetPropertyAttribute< IDAttribute>(s, nameof(Sample.Name));
    Console.WriteLine(attribute.ID);
    // > ididid

    // 全部取得する場合
    var list = AttributeUtility.GetPropertyAttributes(s, nameof(Sample.Name));
    foreach (var att in list)
    {
        Console.WriteLine(att.GetType().Name);
        // > IDAttribute
        // > TagAttribute
    }
}

【C#】文字列の先頭だけを大文字 or 小文字に変換する

ある文字列の中の1文字だけを大文字・小文字に切り替える方法の紹介です。

C#の標準ライブラリにはこういった操作が存在しないので自分で実装することになります。今回は string クラスの拡張メソッドとして実装します。処理効率は特別な外部ライブラリを使用しない限り割と早い部類だと思います(検索して出てくるコードがいずれも効率がちょっと悪い感じだったので代替案として書いています。

先頭文字を大文字・小文字に変更する以外に番号を指定すればN文字目の大文字 ⇔ 小文字を変更することができます。

確認環境

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

  • Unity 2019.4.10f1 & .NET Core 3.1
  • VisualStudio 2019
  • Windows10

実装コード

実装コードは以下の通りです。StringExtension クラスを作成して実装します。

// StringExtension.cs

/// <summary>
/// <see cref="string"/> クラスを拡張します。
/// </summary>
public static class StringExtension
{
    /// <summary>
    /// 指定した n 番目の文字を大文字に変換します。
    /// </summary>
    public static string ToUpper(this string self, int no = 0)
    {
        if (no > self.Length)
        {
            return self;
        }

        var _array = self.ToCharArray();
        var up = char.ToUpper(_array[no]);
        _array[no] = up;
        return new string(_array);
    }

    /// <summary>
    /// 指定した n 番目の文字を小文字に変換します。
    /// </summary>
    public static string ToLower(this string self, int no = 0)
    {
        if (no > self.Length)
        {
            return self;
        }

        var _array = self.ToCharArray();
        var up = char.ToLower(_array[no]);
        _array[no] = up;
        return new string(_array);
    }
}

使い方

使い方は以下の通り。

string str = "abc123";
string newStr = str.ToUpper(0); // 先頭の文字を大文字に変換する
// newStr = Abc123

string newStr_2 = str.ToUpper(3);
// newStr_2 = abc123
//  → 変換できない場合元の文字列が帰る

短いですが以上です。

C#のバージョンと.NET Framework, .NET Coreの対応表

備忘録的な意味で書き残しておきます。

(1) C#言語バージョンと各ランタイムの関係性

.NET Framwework と .NET Core がどのC#の言語バージョンにあたるのかの対応表。

C# .NET Framework .NET Core VisualStudio
1.0 1.0 no 2002
1.2 1.1 no 2003
2.0 2.0 no 2005
3.0 2.0 no 2005
3.5 3.0 no 2008, 2010
4.0 4.0 no 2012
5.0 4.5~4.5.2 1 2013
6.0 4.6~4.6.2 1.1 2015
7.0 4.7~4.7.2 1 2017
7.1 4.7~4.7.2 2.0 2017
7.2 4.7~4.7.2 2.1 2017
7.3 4.8 2.2 2017
8.0 no 3.0, 3.1 2019
  • サポート期間
    • 1.0, 1.1 はもう言語サポートが完全に終了している。
    • VisualStuidoも2020年7月で2010までサポートが切れている

VisualStudioは2013以降開発者キットを別途導入すればどの.NETのバージョンの開発が可能になっていが、より新しいC#の構文にエディタが対応していないのでシンタックスエラー扱いされる。

(2) .NET Standardとランラムの関係性

.NET Framwework と .NET Core がどの .NET Standard に当てはまるかの対応表。そのランタイムの標準ライブラリに API がどの標準に準拠しているかです。ライブラリを作成する際に強く意識しないと他方で使えないライブラリになってしまう可能性があるのでよく注意したほうがいいです。

.NET Standard .NET Framework .NET Core
1.0 4.5 1
1.1 4.5 1
1.2 4.5.1 1
1.3 4.6 1
1.4 4.6.1 1
1.5 4.6.1(4.7.2以降推奨) 1
1.6 4.6.1(4.7.2以降推奨) 1
2.0 4.6.1(4.7.2以降推奨) 2.0
2.1 no 3.0

C# のバージョンとか関係なく別途.NET Standardへの準拠バージョンが存在するので注意

備考

選ぶときに考慮すること。

  • .NET Frameworkは4.8で終了。
    • C# 8.0未対応のままになる
    • .NET Standard 2.1未準拠、2.0相当で終了
      • サポートは継続するが今後新機能の追加は行われなくなるので C#8.0が来ると思わないこと。
  • .NET 3.1はLTSの対象、2年間の長期サポートあり
  • 今後出てくる .NET 5はLTS対象ではない
    • .NET 6.0がLTSの対象

なので、

  • Windows 固有機能が必要でも.NET Core3.1 + Nuget(WCF/Remoting/WindowsServiceなどなど)で解決できないか調査するべき。.NET Frmaework4.8を選ぶと後で泣きを見ると思われる。

  • 言語自体は堅調にバージョンアップしているが周辺ライブラリの開発速度が.NET4.x 時代と違う速い速度で流れているのでゆっくりしているとあっという間にレガシー化の流れがある。

  • .NET Core3.1 で C++/CLI 対応されたので .NET Framework から乗り換えるなら .NET Core3.1 移行で確定

  • Windows Forms でポトペタしたい場合2020年9月時点では様子見が正解。フォームエディタが挙動がおかしい。慌てて.NET Coreにするメリット無い。どうせ時間が止まってるようなものだからレガシーでメンテしても大して変わらない。IDEが良くなればその時乗り換え検討しても良いくらい。

  • VisualStudio2019は16.7でやっと安定化。XAMLエディタが落ちたりするのがやっと収まったっぽい。.NET Core3.1使えるし乗り換え推奨。2012みたいな地雷ではない。次は2021?出たとしても安定して使えるようになるまであと1年半くらいある。

  • VisualStudio2019 + .NET Core3.1 + C#8.0 で .NET 5が準備できる人はそうするべき

参考資料

C#の言語バージョンと.NETバージョン https://ufcpp.net/study/csharp/cheatsheet/listfxlangversion/

.NET Standard https://docs.microsoft.com/ja-jp/dotnet/standard/net-standard

C# の歴史 https://docs.microsoft.com/ja-jp/dotnet/csharp/whats-new/csharp-version-history

一目で分かる、Visual Studioの各バージョンのサポート期限 https://www.atmarkit.co.jp/ait/articles/1609/02/news033.html

【C#】2つの変数の中身を入れ替える方法4選

2つの変数の中身を入れ替える方法を4種類紹介したいと思います。

(1) 昔ながらの方法

教科書に書いてあるやつ。最も一般的な方法。大抵の言語でもこうやって入れ替えられます。

int a = 10;
int b = 20;

int _tmp = a;
a = b;
b = _tmp;
// > a=20, b=10

ローカル変数がひとつ必要になります。以下のように中カッコのスコープを切って処理する小技もある。

int a = 10;
int b = 20;
{
   int _tmp = a;
   a = b;
   b = _tmp;
}
// こうやって中カッコでくくると _tmp 変数自体がこの「}」を抜ける時に消えるので少し安心できる

但しこんな事するくらいなら細かくメソッドに分割したほうがマシかもしれません。

(2) Tupleを使う

C#7.0で導入されたタプルという機能を使用します。見た目が非常にシンプル。余計な変数が必要ない。ただし .NET のバージョンによっては使えないことがある もはや使えない環境は殆どないので C# ではこちらのほう標準かもしれません。

  • 推奨環境条件
    • C#7.0
    • .NET Core 2.0以降
    • .NET Framework 4.7以降
    • IDEの構文サポート
int a = 10;
int b = 20;
(a, b) = (b, a);
// > a=20, b=10

ValueTuple に対する「()」(丸括弧)を使った省略記法をC#7.0以降から使用可能のためいったん Tuple にまとめて置換すると簡潔に記述できる。余計な変数が必要ない。コンパイルすると変数3つでスワップになるためやってることは古典的な方法と同じ。

これ以前のバージョンでも 追加パッケージを導入すれば ValueTuple は使える。VisualStudio 2017 + .NET4.5 では以下エラーが表示されます。

エラー CS8179 定義済みの型 'System.ValueTuple`2' は定義、またはインポートされていません

Nuget から System.ValueTuple を追加するとエラーが出なくなります。

ViusalStudio2017 より前の環境ではパッケージを追加しても IDE 自体が構文サポートをしていないので赤い波線がでエラー表示が出ます。コンパイルはできる謎の環境になる場合があります。ただし可能であれば新しいVSに乗り換えたほうがいいです。というかもしIDEが会社の金なら即座に乗り換えるべき。C#ならデメリット無いのでは?VC++はしらぬ。 さっさと2019などに移行しましょう。

(3) 外部のメソッドで入れ替える

この方法は微妙かもしれません。ただ、言語バージョン依存が存在しないためどのC#でも使えます。

// ValueUtil.cs

public static class ValueUtil
{
    public static void Swap<T>(ref T a, ref T b)
    {
        T _tmp = a;
        a = b;
        b = _tmp;
    }
}

int a = 10;
int b = 20;
ValueUtil.Swap(ref a, ref b);
// > a=20, b=10

ref を引数に指定しないといけなのが若干気になるりますね。

(4) 拡張メソッドで入れ替える(値型のみ)方法

この方法は構造体(値型)のみ適用できます。変数のメソッドとして実行できるのである意味直観的かも?

C#7.2以降で使用可能な構文を使用する。

  • 環境条件
    • C# 7.2
    • .NET Core2.1以降
    • .NET Framework 4.7以降 (オプションが指定必要)
    • ref 拡張メソッド構文のサポート
// StructExtension.cs

public static class StructExtension
{
    public static void Swap<T>(ref this T self, ref T tgt) where T : struct
    {
        T _tmp = self;
        self = tgt;
        tgt = _tmp;
    }
}

int a = 10;
int b = 20;
a.Swap(ref b);
// > a=20, b=10

クラスは言語仕様上の問題で入れ替え不可。これもやや新しい機能のため使用に制約があります。

余談ですが、この書き方で拡張メソッドを記述すると全ての型にメソッドが追加されたように見せられるのでこの書き方が言語拡張の機能として使用できます。ただ乱用すると標準の C# と使用感が大きく変わってしまいます。

ほかにあるかな?

後ろ2つはやや無理やりでしたが、何かあれば随時。

関連記事

takap-tech.com

【C#】2つのDictionaryを1つにマージする

2つの Dictionary を 1つの Dictionary にマージして1つにまとめる方法の紹介です。

確認環境

確認環境は以下の通りです(とはいってもどの環境でも動きます。

  • .NET Core 3.1
  • Windows10(Core-i7 3770K)
  • VisualStudio2019

実装コード

以下のコードで2つの Dictionaty をマージして新しい Dictionary を作成して取得できます。

/// <summary>
/// 2 つの Dictionary を 1 つの Dictionary に結合し新しいテーブルを取得します。
/// データが重複する場合 a の Dictionary のデータが優先されます。
/// </summary>
public static IDictionary<TKey, TValue> Marge<TKey, TValue>(IDictionary<TKey, TValue> a, 
                                                            IDictionary<TKey, TValue> b)
{
    var table = new Dictionary<TKey, TValue>();
    foreach (var item in a)
    {
        table[item.Key] = item.Value;
    }

    foreach (var item in b)
    {
        if (!table.ContainsKey(item.Key))
        {
            table[item.Key] = item.Value;
        }
    }

    return table;
}

// 使い方

// データ準備
var d1 = new Dictionary<string, int>()
d1["a"] = 1;
d1["b"] = 2;
var d2 = new Dictionary<string, int>();
d2["b"] = 3;
d2["c"] = 4;

// マージする
Dictionary<string, int> d3 = Marge(a, b);
// > a : 1
// > b : 2
// > c : 4

また、自分の Dictionary に別の Dictionary をマージするには以下のように拡張メソッドを記述します。

public static class DictionaryExtension
{
    /// <summary>
    /// 現在の <see cref="Dictionary{TKey, TValue}"/> に b をマージします。
    /// b のキーが重複する場合マージされません。
    /// </summary>
    public static void Marge<TKey, TValue>(this IDictionary<TKey, TValue> a, 
                                                IDictionary<TKey, TValue> b)
    {
        foreach (var item in b)
        {
            if (a.ContainsKey(item.Key))
            {
                continue;
            }
            a[item.Key] = item.Value;
        }
    }
}

// 使い方

// データ準備
var d1 = new Dictionary<string, int>()
d1["a"] = 1;
d1["b"] = 2;
var d2 = new Dictionary<string, int>();
d2["b"] = 3;
d2["c"] = 4;

// マージする
d1.Marge(b);
// > a : 1
// > b : 2
// > c : 4

副作用がないので最初のほうがおすすめですが、新しいテーブルを作成するときに倍メモリが必要なので、テーブルの中身が変わってもいいなら拡張メソッドの書き方のほうがおすすめです。

Linqでマージする

この記事で簡単にマージできるのですが正直あまりお勧めはしません。Linq は注意しないと処理コストがかかる場合が多いです。というかこれくらいなら普通にforeachを書いたほうが良いと思います。

C# の Dictionary 同士を簡単にマージする方法

参考までに Linq のクエリー式でマージすると以下のようになります。

// Linq のクエリー式でマージする
var marged = 
    (from p1 in d1 
        where !d2.ContainsKey(p1.Key) select p1).Concat(d2);
var map = marged.ToDictionary(p => p.Key, p => p.Value);

こんな感じで拡張メソッドがあると ToDictionary がシンプルにできるかもしれません。

// IEnumerableExtension.cs
public static class IEnumerableExtension
{
    /// <summary>
    /// <see cref="KeyValuePair{TKey, TValue}"/><see cref="Dictionary{TKey, TValue}"/> に変換します。
    /// </summary>
    public static Dictionary<TKey, TValue> 
        ToDictionary<TKey, TValue>(this IEnumerable<KeyValuePair<TKey, TValue>> self)
    {
        return self.ToDictionary(p => p.Key, p => p.Value);
    }

    /// <summary>
    /// 2 つの値の組み合わせの Tuple を <see cref="Dictionary{TKey, TValue}"/> に変換します。
    /// </summary>
    public static Dictionary<TKey, TValue>
        ToDictionary<TKey, TValue>(this IEnumerable<(TKey k, TValue v)> self)
    {
        return self.ToDictionary(p => p.k, p => p.v);
    }
}

実行速度の比較

余談ですが、記事中の処理の実行速度を計測した結果を乗せておきます。

  • 測定対象
    • 普通に foreach でマージする (1)
    • Linq を使った方法 (2) ~ (5)
    • Linq のクエリ式を使用 (6)

結論から書きますが、(この程度なら foreach で回したほうがいい節がありますが)Linq でも実行速度は(気を付ければ)だいたい同じになります。

Linq は GropuBy すると急に遅くなります(というか一般的に Linq なメソッドを何個も連結すると相応に遅くなっていきます)

// Program.cs
internal class Program
{
    public static void Main(string[] args)
    {
        // 計算前に少しCPUを回しておく
        for (int i = 0; i < 100; i++)
        {
            // 共通のテストデータ生成
            var dic1 = CreateTable(1000);
            var dic2 = CreateTable(1000);
            Marge1(dic1, dic2);
            Marge2(dic1, dic2);
            Marge3(dic1, dic2);
            Marge4(dic1, dic2);
            Marge5(dic1, dic2);
            Marge6(dic1, dic2);
        }

        var list = new List<int>();

        int count = 5000; // ループ回数
        var sw1 = new Stopwatch();
        var sw2 = new Stopwatch();
        var sw3 = new Stopwatch();
        var sw4 = new Stopwatch();
        var sw5 = new Stopwatch();
        var sw6 = new Stopwatch();
        for (int i = 0; i < count; i++)
        {
            // 共通のテストデータ生成
            var dic1 = CreateTable(1000);
            var dic2 = CreateTable(1000);

            sw1.Start();
            var item = Marge1(dic1, dic2); // (1) 普通にforeachで結合
            sw1.Stop();
            list.Add(item.Count);

            sw2.Start();
            item = Marge2(dic1, dic2); // (2) Qiita の方法で結合(1)
            sw2.Stop();
            list.Add(item.Count);

            sw3.Start();
            item = Marge3(dic1, dic2); // (3) Qiita の方法で結合(2)
            sw3.Stop();
            list.Add(item.Count);

            sw4.Start();
            item = Marge4(dic1, dic2); // (4) Qiita の方法で結合(改定版)
            sw4.Stop();
            list.Add(item.Count);

            sw5.Start();
            item = Marge5(dic1, dic2); // (5) コガネブログの方法で結合
            sw5.Stop();
            list.Add(item.Count);

            sw6.Start();
            item = Marge6(dic1, dic2); // (6) Linq のクエリー式でマージ
            sw6.Stop();
            list.Add(item.Count);
        }

        // 結果表示
        Console.WriteLine($"Count={count}, Tmp={list.Count}");
        Console.WriteLine($"(1) Total={sw1.Elapsed.TotalMilliseconds:F3}ms, 
            per={sw1.Elapsed.TotalMilliseconds / count:F3}ms");
        Console.WriteLine($"(2) Total={sw2.Elapsed.TotalMilliseconds:F3}ms, 
            per={sw2.Elapsed.TotalMilliseconds / count:F3}ms");
        Console.WriteLine($"(3) Total={sw3.Elapsed.TotalMilliseconds:F3}ms, 
            per={sw3.Elapsed.TotalMilliseconds / count:F3}ms");
        Console.WriteLine($"(4) Total={sw4.Elapsed.TotalMilliseconds:F3}ms,
            per={sw4.Elapsed.TotalMilliseconds / count:F3}ms");
        Console.WriteLine($"(5) Total={sw5.Elapsed.TotalMilliseconds:F3}ms,
            per={sw5.Elapsed.TotalMilliseconds / count:F3}ms");
        Console.WriteLine($"(6) Total={sw6.Elapsed.TotalMilliseconds:F3}ms, 
            per={sw6.Elapsed.TotalMilliseconds / count:F3}ms");
        // (1) Total=1038.604ms, per=0.208ms ◎
        // (2) Total=1905.880ms, per=0.381ms ◎
        // (3) Total=2332.546ms, per=0.467ms △
        // (4) Total=1048.628ms, per=0.210ms ◎
        // (5) Total=2224.486ms, per=0.445ms △
        // (6) Total=1030.684ms, per=0.206ms ◎

        // (1) = (6) = (4) >>>> (2) = (3) = (5)
        // → GroupBy が入ると急に遅くなる
    }

    // テストデータを作成
    private static Dictionary<string, int> CreateTable(int count)
    {
        IEnumerable<(string, int)> f()
        {
            for (int i = 0; i < count; i++)
            {
                yield return (Guid.NewGuid().ToString(), count);
            }
        }

        return f().ToDictionary();
    }

    // (1) 普通にforeachで結合
    public static IDictionary<TKey, TValue>
        Marge1<TKey, TValue>(IDictionary<TKey, TValue> a, IDictionary<TKey, TValue> b)
    {
        var table = new Dictionary<TKey, TValue>();
        foreach (var item in a) table[item.Key] = item.Value;
        foreach (var item in b) table[item.Key] = item.Value;
        return table;
    }

    // (2) Qiita の方法で結合(1)
    //  → https://qiita.com/Nossa/items/802b0e0de927c0cfec05
    public static IDictionary<TKey, TValue>
        Marge2<TKey, TValue>(IDictionary<TKey, TValue> a, IDictionary<TKey, TValue> b)
    {
        return a.Concat(b)
                .GroupBy(c => c.Key)
                .ToDictionary(c => c.Key, c => c.FirstOrDefault().Value);
    }

    // (3) Qiita の方法で結合(2)
    public static IDictionary<TKey, TValue>
        Marge3<TKey, TValue>(IDictionary<TKey, TValue> a, IDictionary<TKey, TValue> b)
    {
        return a.Concat(b)
                .GroupBy(c => c.Key)
                .ToDictionary(c => c.Key, c => c.FirstOrDefault().Value);
    }

    // (4) Qiita の方法で結合(改定版)
    public static IDictionary<TKey, TValue>
        Marge4<TKey, TValue>(IDictionary<TKey, TValue> a, IDictionary<TKey, TValue> b)
    {
        return a.Concat(b.Where(pair =>
                !a.ContainsKey(pair.Key))).ToDictionary(pair => pair.Key, pair => pair.Value);
    }

    // (5) コガネブログの方法で結合
    //  → https://baba-s.hatenablog.com/entry/2019/09/09/215700
    public static IDictionary<TKey, TValue>
        Marge5<TKey, TValue>(IDictionary<TKey, TValue> a, IDictionary<TKey, TValue> b)
    {
        return a.Concat(b)
                .GroupBy(c => c.Key)
                .ToDictionary(c => c.Key, c => c.FirstOrDefault().Value);
    }

    // (6) Linq のクエリー式でマージ
    public static IDictionary<TKey, TValue>
        Marge6<TKey, TValue>(IDictionary<TKey, TValue> a, IDictionary<TKey, TValue> b)
    {
        var marged = (from p1 in a where !b.ContainsKey(p1.Key) select p1).Concat(b);
        return marged.ToDictionary(p => p.Key, p => p.Value);
    }
}

Dictionary を短時間に数千回も結合するような処理は仕様の方を考え直した方がいいと思いますが実行結果は最終的に同じなのに実行速度が倍近く違うというのは多少気にはなります。特に、モバイル上で実行される可能性を考えるとわざわざ倍遅い方法を採用することはないと思います。

【C#】System.Text.Jsonでオブジェクトのシリアライズ・デシリアライズ

.NET Core 3.0 から利用可能な新しい標準ライブラリ JSON シリアライザー System.Text.JsonJsonSerializer 使い方の紹介です。

動作環境

以下環境で標準で使用することができます。

  • 使用可能な.NETのバージョン
    • .NET Core3.0~
    • .NET Standard 2.1~
    • .NET Framework 4.8~

使い方

早速使い方の紹介です。

まず以下の名前空間を using します。

using System.Text.Json;
using System.Text.Json.Serialization;

今回は以下のようなJSONデータを扱います。

ルート要素をオブジェクト配列として扱います。

[
  {
      "id": 0,
      "name": "Taka",
      "numbers": [ 0, 1, 2, 3 ],
      "list": [ 0, 1, 2, 3 ],
      "map": {
          "key1": "value1",
          "key2": "value2",
          "key3": "value3",
          "key4": "value4",
          "key5": "value5"
      }
  },
  {
      "id": 1,
      "name": "PG",
      "numbers": [ 10, 11, 12, 13 ],
      "list": [10, 20, 30],
      "map": {
        "aaa": "111",
        "bbb": "222",
        "ccc": "333"
      }
  }
]

次に、シリアライズ・デシリアライズするクラスを宣言します。JSON に対応するクラスを作成し、そこに JSON データを当てはめていきます。

// JsonItem.cs

public class JsonItem
{
    // ★★★ プロパティが自動的にシリアライズの対象に選ばれる
    //        オブジェクトに別名を付けたい場合以下のように JsonPropertyName 属性を付ける
    [JsonPropertyName("id")]
    public int ID { get; set; }

    // ★★★ フィールドはシリアライズの対象にできない(指定しても無視される
    //        プロパティだけシリアライズの対象できる
    [JsonPropertyName("asdf")]
    public string sample = "";

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

    [JsonPropertyName("numbers")]
    public int[] Numbers { get; set; }

    [JsonPropertyName("list")]
    public List<float> NumberList { get; private set; } = new List<float>();

    [JsonPropertyName("map")]
    public IDictionary<string, string> Attributes { get; private set; } 
        = new Dictionary<string, string>();
    
    // ★★★ 除外するプロパティには JsonIgnore 属性を付ける
    [JsonIgnore]
    public string PrivacyInfo { get; set; }
}

// 配列として扱うためリストにオブジェクトを格納する
var items = new List<JsonItem>();

1点注意があります。System.Text.Json のシリアライズ対象はプロパティのため、フィールドはシリアライズできません(このため Unity で JsonUtility を使用してる場合の乗り換えは難しいかもしれません)

プロパティ名と JSON 内のノードの名異なる場合「JsonPropertyName (System.Text.Json.Serialization名前空間)」を上記コード例のように付与します。また、デフォルトでは自動的にプロパティが全部出力されるのでシリアライズ対象にしたくないプロパティには「JsonIgnore」を同じく付与します。経験上、プロパティ名 == ノード名だったとしてもJsonPropetyName は指定しておいたほうが無難です。

リスト も Dictionary も配列もすべてサポートしているので一般的な .NET の型はほぼすべてカバーしています。ただし入れ子になったオブジェクト階層は 64までなので深すぎるオブジェクトだけ注意しましょう。

シリアライズ:文字列 → オブジェクト

シリアライズ方法は以下の通りです。

// あらかじめ適当なデータを生成しておく
List<JsonItem> items = SampleData.GetJsonItems(DataLength);

// ★★★ 必要に応じて以下のオプションを指定する
var op = new JsonSerializerOptions
{
    // ★★★ (1) 文字列にマルチバイト文字(≒日本語)が含まれる
    //  → 指定しないと文字コードが出力される
    Encoder = JavaScriptEncoder.Create(UnicodeRanges.All),

    // ★★★ (2) 出力結果をインデントするかどうか
    // true : 空白文字列で整形される / false : されない
    WriteIndented = true,

    // ★★★ (3) null を無視するかどうか
    // true : nullならJSONに出力されない
    // false : 出力する(既定はこっち)
    IgnoreNullValues = true,

    // ★★★ (4) 読み取り専用プロパティを無視するかどうか
    // true : 読み取り専用(getのみ)のプロパティはJSONに出力しない
    // false : 出力する(既定はこっち)
    IgnoreReadOnlyProperties = true,
    
    // ★★★ (5) 出力をCamelケースにするかの指定
    // JsonPropertyName 指定があるとそっちが優先される
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};

// オブジェクトをJSON文字列にシリアライズする
string jsonStr = JsonSerializer.Serialize(item, op);

必要に応じてコード例のコメントの通りオプションを指定します。日本語を含むマルチバイト文字を扱う場合 Encoder を指定します。他のオプションは状況に応じて指定します。他のオプションについては公式のリファレンスを参照してください。

JSON化したデータは以下のようなデータとして jsonStr 変数に入っています。

// jsonStr 変数の中身
[
  {
    "time": "2020-08-21T00:14:58.0850867+09:00",
    "span": "10200",
    "id": 1905413078,
    "name": "2d9f9caa-9f05-4213-a992-3e5f6672c575",
    "numbers": [
      1512193737,
      300677771,
      1625274804,
      1946273800,
      251410219
    ],
    "list": [
      263971600,
      1.634365E+09,
      550383800,
      1.4658991E+09,
      203363660
    ],
    "map": {
      "feb9c436-6e85-453b-98ce-395f549d7b2e": "b1763a54-c41c-4b46-b424-cebf87271469",
      "c23bf690-33ae-4096-998d-0901b09e4ec9": "ef737ce1-5185-4e57-a009-edd538c7ae57",
      "44598508-d4e1-44d8-89a3-4ce3ce241215": "c33f3104-3a8e-46fc-90cb-8d9fcb0952de",
      "4b1a697c-c17e-4458-9e61-ec0f222df2cf": "4bc7eac1-3160-4496-8667-dc11e67979e6",
      "c1d3f90f-64ba-4f9e-a43b-bf97b20d144c": "47fd7ae3-a194-46c4-b299-e60bc9d83ea2"
    }
  }
]

参考までにデータ生成は以下の通り処理を行っています。

public static class SampleData
{
    private static readonly Random r = new Random();

    // 指定した件数分のJSONに変換するデータを生成する
    public static List<JsonItem> GetJsonItems(int count)
    {
        IEnumerable<JsonItem> f()
        {
            for (int i = 0; i < count; i++)
            {
                var item = new JsonItem()
                {
                    ID = r.Next(),
                    Name = Guid.NewGuid().ToString(),
                    Numbers = new int[]
                    {
                    r.Next(),
                    r.Next(),
                    r.Next(),
                    r.Next(),
                    r.Next(),
                    },
                };

                item.NumberList.Add(r.Next());
                item.NumberList.Add(r.Next());
                item.NumberList.Add(r.Next());
                item.NumberList.Add(r.Next());
                item.NumberList.Add(r.Next());

                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 f().ToList();
    }
}

デシリアライズ:オブジェクト → 文字列

デシリアライズの方法は以下の通りです。

先ほどの jsonStr をデシリアライズすると元の List に戻すことができます。

// string jsonStr = JsonSerializer.Serialize(item, op);

// デシリアライズ方法のオプションの指定
op = new JsonSerializerOptions()
{
    // ★★★ (1) デシリアライズするときに大文字と小文字を区別するかしないかのフラグ
    // true : 区別しない
    // fa;se : 区別する(既定)
    PropertyNameCaseInsensitive = true,

    // ★★★ (2) JSONにコメントが含まれる場合の扱い方
    // Allow : 許可する ← なぜかエラーが出る…たぶん使えない
    // Disallow : 許可しない(検出した場合例外:これが既定値)
    // Skip : その行はスキップする
    ReadCommentHandling = JsonCommentHandling.Skip,

    // 以下の2種類の標準日準拠のコメントが対象にできる
    // {
    //    "TemperatureCelsius": 25, // Fahrenheit 77
    //    "Summary": "Hot" /* Zharko */
    // }

    // ★★★ (3) 末尾にコンマ記号がついているJSONを許可するかどうか
    // true : 許可する
    // false : 許可しない(既定)
    AllowTrailingCommas = true,

    // 本来以下のように末尾にコンマ記号がある標準非準拠のJSONを扱うかどうか
    // {
    //    "Item1": "a",
    //    "Item2": "b", ★★★末尾にコンマ
    // }
};

// オブジェクトにデシリアライズする
List<JsonItem> list = JsonSerializer.Deserialize<List<JsonItem>>(jsonStr, op);

これもオプションを指定することができます。

標準に準拠しないJSONをどう扱うかなどのオプションが用意されています。

ストリーム(ファイル)に読み書きする

書き込み方は以下の通りです。

// ファイルに書き出す

var options = new JsonSerializerOptions
{
    // UTF-8が確実かつエスケープ規則が既知ならこれを指定する
    Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};

// 出力先の指定
using var stream = new FileStream(@"c:\tmp\sample.json", FileMode.Create, FileAccess.Write);

// ストリームに対してシリアライズした結果を書き出す
await JsonSerializer.SerializeAsync(stream  // 出力するストリーム, 
                                    items,  // 出力するオブジェクト
                                    op); // オプション(任意)

// WebRequest のレスポンスに書きこむこともできる
// Stream dataStream = request.GetRequestStream();
// await JsonSerializer.SerializeAsync(dataStream, sourceItem, op);

読み込み方法は以下の通り

// ファイルから読み込む

var options = new JsonSerializerOptions
{
    Encoder = JavaScriptEncoder.Create(UnicodeRanges.All),
};

// 読み取り先の指定
using var stream = new FileStream(@"c:\tmp\sample.json", FileMode.Open, FileAccess.Read);
// ストリームから読み取った内容をオブジェクトに復元する
var itemList =  
    await JsonSerializer.DeserializeAsync<List<JsonItem>>(stream, options);

その他の規則

DateTime 、TimeSpan の扱い

以下、標準の状態で DateTime と TimeSpan の変換を見てみます。

// ★ DateTime 型をシリアライズしたときのJSON
[JsonPropertyName("time")]
public DateTime Time { get; set; } = DateTime.Now;
// > "Time": "2020-08-20T11:41:03.7557392+09:00",

// ★ TimeSpan 型をシリアライズしたときのJSON
[JsonPropertyName("span")]
public TimeSpan Span { get; set; } = TimeSpan.FromSeconds(10.2);
// > "Span": {},
//  → 変換できない!

TimeSpan 型は変換できないみたいです。

派生クラスのシリアライズ

派生クラスの場合以下のようにひと手間必要です。ちょっと違和感がありますがお約束だと思ってあきらめましょう。

// 基底クラスを出力する場合工夫が必要
public class A { }
public class B : A { }

B b = new B();
JsonSerializer.Serialize(b); // これだとBの階層しか出力されない
JsonSerializer.Serialize<object>(b); // ジェネリックに<object>を指定すると基底クラスも出力される

カスタムコンバーターを使う

上記で出力されない型を独自の規則で入出力するには自作の Converter を実装する必要があります

先ほどシリアライズできなかった TimeSpan 型をシリアライズ・デシリアライズできるようにするためには以下のようにコンバーターを実装します。

// TimeSpan ⇔ JSONに変換するコンバーター
public class TimeSpanConverter : JsonConverter<TimeSpan>
{
    public override TimeSpan 
        Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        // 文字列の数値をミリ秒で変換
        return TimeSpan.FromMilliseconds(double.Parse(reader.GetString()));

        // 数値の場合以下のように書く
        // return TimeSpan.FromMilliseconds(reader.GetDouble());
    }

    public override void
        Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.TotalMilliseconds.ToString()); // ミリ秒で書き出し
        // writer.WriteNumberValue(value.TotalMilliseconds); // 数値で書き出すときはこう
    }
}

で、プロパティに以下のように記述します。

[JsonConverter(typeof(TimeSpanConverter))]
public TimeSpan Span { get; set; } 

他にもいろいろ変換方法を指定する方法はありますがこれだけ覚えておけば大抵大丈夫だと思います(他にもやり方があるということだけ覚えておいてこれで対応できそうもなければここまで記事を見ていればリファレンスを参照すれば十分対応可能かと思います)

DateTime 型は表現方法が色々あるので標準でなくカスタムコンバーターを記述することもありそうです。

既存のシリアライザーとの動作速度の比較

最後に手元で適当に計測したら速度は以下の通りです。

System.Text.Json > Json.NET >> DataContractSerializer

実行速度のおおまかな比率はシリアライズもデシリアライズも大凡以下の通りです。

DataContractSerializer を 「1」 とした場合、JSON.NET「1.2~1.5 倍速い」、System.Text.Json「2.1~2.9 倍速い」

新規開発の場合とりあえず System.Text.Json をシリアライザとして選択するのがよさそうです。MSDN リファレンスに Newtonsoft.Jsonから移行する方法という移行のサポートページが公開されているため一読するとよさそうです。

関連記事

もう使うことは無いかもしれませんが、これ以前のオブジェクトのシリアライズの方法については以下の通りです。

takap-tech.com

【C#】16進数文字列とバイト配列の相互変換

ちょっと何の役に立つか分からないですが、16進数の文字列をバイトの配列に見た目通りに変換する方法です。

long型などの有限長の型を経由しないため基本的に無限長の長さをそのまま扱えます。

変換方法のサンプル

以下のように確実に16進数文字列と分かっているものを対象に16進数文字列 ⇔ バイト配列を相互に変換できます。

public static void Main(string[] args)
{
    // 元の16進数表記の文字列
    string dexStr = "0x1234567890ABCDEF";

    // バイト配列に変換
    byte[] array = DexConverter.Convert(dexStr);
    // > array = 0x12 34 56 78 90 ab cd ef

    // バイト配列から16進数文字列に変換
    string conv = DexConverter.Convert(array);
    // > conv = 1234567890abcdef
}

DexConverterクラス

上記のサンプルで使用しているクラスの実装です。

おそらく変換効率がかなり悪いと思われるのでプロジェクトで使用するときは修正が必要かもしれません。

// DexConverter.cs

using System;
using System.Text;

/// <summary>
/// 16進数を変換するための機能を提供します。
/// </summary>
public static class DexConverter
{
    /// <summary>
    /// 16進数文字列をバイト配列に変換します。
    /// </summary>
    public static byte[] Convert(string dex)
    {
        if (dex.Length % 2 != 0)
        {
            throw new ArgumentException("Invalid length.");
        }

        // 先頭の '0x' を取り除く
        dex = dex.TrimStart('0').TrimStart('x').Trim('X');

        byte[] array = new byte[dex.Length / 2];
        for (int i = 0; i < dex.Length; i += 2)
        {
            string bstr = dex.Substring(i, 2);
            array[i / 2] = System.Convert.ToByte(bstr, 16);
        }

        return array;
    }

    /// <summary>
    /// バイト配列を16進数の文字列に
    /// </summary>
    public static string Convert(byte[] array)
    {
        var builder = new StringBuilder();
        foreach (byte b in array)
        {
            string bstr = b.ToString("x");
            if (bstr.Length == 1)
            {
                builder.Append("0"); // 1桁の時は上位ビットをうめる
            }
            builder.Append(b.ToString("x"));
        }

        return builder.ToString();
    }
}

【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));
    }
}

以上です。