C#の文字列補完はToStringした方が高速になる理由を調べてみた

はじめに

C# の「文字列補完」機能という機能があります。ものすごい雑に言うと C# 6 以降で可能な文字列の先頭に「$」起動を付けることでフォーマット付き文字列を埋め込んで記述することができ視認性を向上することができる機能です。

記述例は以下の通りです。

// ★文字列補完を使った表現
int a = 1010;
string msg3 = $"({a})";
// > (1010)

// 上記を今までの方法で記述すると以下の通り
// (1) + で連結する
string msg1 = "(" + a + ")";
// > (1010)

// (2) Fromatメソッドを使う
string msg2 = string.Format("({0})", a);
// > (10)

string.Format をインラインで記述できるようになったイメージです。

確認環境

  • .NET Core3.1
  • VisualStudio2019
  • Windows10

{ } の中はToString した方が高速?

まずは確認ですが記述方法によって速度にが明確に差があります。

これがどういうことから調査したいと思います。以下のコードでは「case.3」が高速です。「全部 ToString した時だけ」動作が早いです。

int a = 10;
int b = 20;

// case.1
string msg1 = $"{a}, {b}";
// > 0.000258828msec

// case.2
string msg2 = $"{a}, {b.ToString()}";
// > 0.000250034msec

// case.3:★こうしたほうが早い
string msg3 = $"{a.ToString()}, {b.ToString()}";
// > 0.000158491msec

この記述例だと約40%くらい速度が違うのでモバイルとかだと ToString したほうがよさそうです。

どうして速度が違うのか?

じゃあ、どうしてこのような違いが出たのでしょうか?こういう時はSharpLabが便利です。

先述のコードをツールにかけると概ね以下のようになります。

// case.1
string msg1 = $"{a}, {b}";
↓
string msg1 = string.Format("{0}, {1}", a, b);

// case.2
string msg2 = $"{a}, {b.ToString()}";
↓
string msg2 = string.Format("{0}, {1}", a, b.ToString());

// case.3
string msg3 = $"{a.ToString()}, {b.ToString()}";
↓
string msg3 = string.Concat(a.ToString(), ", ", b.ToString());

どうも展開のされ方が違うようです。

全部文字列だと判断された場合だけ「string.Format」ではなく「string.Concat」が使用されています。string.Format の方が実装が複雑なので処理時間が違うようです(この程度だとボックス化による処理速度の影響はほぼ無視できる程度のため純粋に使用したメソッドの性能によるところが大きいと思います)

つまり、全部文字列だったら早い ので、一部を ToString するだけでは効果がありません。

まぁもともと、string.Concat は文字列操作界隈では動作速度は最遅レベルですが、ワーストワンので番速度が遅い string.Format が使われるりは多少マシって感じです。まぁ、ここら辺の速度を Unity 上で追及するなら StringBuilder をインスタンスを使いまわしすか、ZString というライブラリを使ったほうがいいとは思いますが。

C#10.0では高速化されます

ちなみにこの文字列補完ですが、動作速度がいくら何でも遅すぎるということで、C#10.0から速度が大幅に向上します。

どうも展開のされ方が string.Fromat をから DefaultInterpolatedStringHandler という StringBuilder の親戚で処理されるようになって処理速度が大幅に改善したようです。なのでこの記事は .NET 6.0(C# 9.0) までの話になります。

IDE0071 保管を簡略化することができますが煩わしい場合

ちなみに、VisualStudio2019 では文字列補完中に ToString を書くと「IDE0071 保管を簡略化することができます」と警告が表示されます。

ただ、上記の結果から明示して使用するケースがあるため煩わしい場合は、以下をコードの先頭に記述すればこの警告が抑制できます。

// コードの先頭 ~ using 後くらいの位置に以下を記述する
#pragma warning disable IDE0071

検証コード

この検証で使用したコードは以下の通りです。

using System;
using System.Diagnostics;

#pragma warning disable IDE0071, IDE0059

internal class AppMain
{
    public static void Main(string[] args)
    {
        int a = 10;
        int b = 20;

        var ast = new Stopwatch();
        var bst = new Stopwatch();
        var cst = new Stopwatch();

        string msg1 = "";
        string msg2 = "";
        string msg3 = "";

        int cnt = 100000;

        for (int i = 0; i < cnt; i++)
        {
            ast.Start();
            msg1 = $"{a}, {b}";
            ast.Stop();

            bst.Start();
            msg2 = $"{a}, {b.ToString()}";
            bst.Stop();

            cst.Start();
            msg3 = $"{a.ToString()}, {b.ToString()}";
            cst.Stop();
        }

        Console.WriteLine($"{ast.Elapsed.TotalMilliseconds / cnt}msec");
        Console.WriteLine($"{bst.Elapsed.TotalMilliseconds / cnt}msec");
        Console.WriteLine($"{cst.Elapsed.TotalMilliseconds / cnt}msec");
    }
}

【C#】Listと配列でforとforeachのアクセス速度比較

結論としては以下の通り。

  • 配列は for と foreach の速度はほぼ同じ
  • List<T> は for のほうが foreach より10%以上早い
  • 配列に対する操作は List に対する操作より 50%以上早い
  • List<T> クラスの ForEach メソッドはメリットが無いので使わないほうがいい

また、決まった長さで処理ができる場合配列を使用したほうが常に高速です。

では説明です。

確認環境

  • .NET6 + C# 10.0
  • VisualStudio 2022
  • Windows 11
  • AMD Ryzen 9 5900X
  • 計測には BenchmarkDotNetを使用
  • Relaseビルドしたバイナリをコンソールから実行して確認

補足:

ランタイムのバージョンや言語バージョン(コンパイラのバージョン)PC環境によって変動します。あくまで記載の環境で実行したらこうなります。

計測結果サマリー

|              Method |       Mean |    Error |   StdDev |
|-------------------- |-----------:|---------:|---------:|
|        ArrayForTest |   572.5 ns |  5.39 ns |  4.21 ns | (1) 配列をforで回す
|    ArrayForEachTest |   575.2 ns |  6.33 ns |  5.92 ns | (2) 配列をforeachで回す
| CreateTestDataArray |   313.3 ns |  3.02 ns |  2.83 ns | (1)と(2)のテストデータ作成
|         ListForTest | 1,295.9 ns | 12.07 ns | 10.70 ns | (3) List<T>をforで回す
|     ListForEachTest | 1,482.5 ns | 23.70 ns | 22.17 ns | (4) List<T>をforで回す
| ListForEachMethodTest | 2,557.3 ns | 50.65 ns | 49.74 ns | (5) List<T>のForEachメソッドで回す
|  CreateTestDataList |   791.4 ns | 14.29 ns | 13.37 ns |  (3)と(4)のテストデータ作成

データ生成を除外すると以下の通りです。

項目 速度
(1) 配列 + for 259.2ns
(2) 配列 + foreach 261.9ns
(3) List + for 504.5ns
(4) List + foreach 691.1ns
(5) List + ForEeachメソッド 1766.3ns

List<T>.ForEach メソッド*1を使用したループだけ突出して遅いです。このメソッドを使ってスマートに書ける訳でもなく単に処理速度が遅いだけなので使用しないほうがよいでしょう。

計測コード

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

単純に1000件の配列とリストを作成してそれに対してシーケンシャルアクセスを実行しています。

using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace Takap.Performance
{
    public class AppMain
    {
        public static void Main(string[] args)
        {
            BenchmarkRunner.Run<Test>();
        }
    }

    public class Test
    {
        private const int cnt = 1000;

        private int _sum1;
        private int _sum2;
        private int _sum3;
        private int _sum4;

        // (1) 配列 + for
        [Benchmark]
        public void ArrayForTest()
        {
            var array = CreateTestDataArray();

            for (int i = 0; i < array.Length; i++)
            {
                _sum1 += array[i];
            }
        }

        // (2) 配列 + foreach
        [Benchmark]
        public void ArrayForEachTest()
        {
            var array = CreateTestDataArray();

            foreach (int value in array)
            {
                _sum2 += value;
            }
        }

        [Benchmark]
        public int[] CreateTestDataArray()
        {
            int[] array = new int[cnt];
            for (int i = 0; i < array.Length; i++)
            {
                array[i] = i;
            }
            return array;
        }

        // (3) List + for
        [Benchmark]
        public void ListForTest()
        {
            var list = CreateTestDataList();

            for (int i = 0; i < list.Count; i++)
            {
                _sum3 += list[i];
            }
        }

        // (4) List + foreach
        [Benchmark]
        public void ListForEachTest()
        {
            var list = CreateTestDataList();

            foreach (int value in list)
            {
                _sum4 += value;
            }
        }

        // (5) List + ForEach()
        [Benchmark]
        public void ListForEachMethodTest()
        {
            var list = CreateTestDataList();
            list.ForEach(value => _sum4 += value);
            // ★ListクラスのForEachメソッドは遅いので使わないほうがいい
        }

        [Benchmark]
        public List<int> CreateTestDataList()
        {
            List<int> array = new(cnt);
            for (int i = 0; i < cnt; i++)
            {
                array.Add(i);
            }
            return array;
        }
    }
}

計測用のライブラリで速度を計測しているのでPC環境に左右されない + 簡単に計測できるでとても楽ですね。

最後に

ちなみに、.NET 6 のほうが最適化が進んているのか .NET Core 3.1 よりかなり速度が高速化しています。

で、ここまで長々と書いておいてなんですが、この後、実際はループの中に書く処理の方が何倍も重たいでしょうし、どう書いたところでループの処理コストなんて全体の中で本当に極小のため、こんな事を気にして実装すること自体がナンセンスかもしれませんが目安程度に参考にしてください。

まぁ、この結果に拘泥せず状況によって使い分けたほうがいいのかもしれませんね。

*1:List<T> クラスの ForEach メソッドはよく Linq メソッドと言われますが List クラス固有のメソッドで Linq とは何の関係もありません。

C#で再帰を使わずにフォルダ内のファイルを列挙する

再帰処理を使わないでC#でフォルダ階層をたどってファイルをリストアップする方法の紹介です。

最後に記載がありますが実際はフォルダを巡回する必要ありません。C#はAPIをひとつつ呼ぶだけで実装できます。

再帰処理を使ってファイルを列挙する

まずは古典的な再帰処理を使ったファイルの探索方法です。

// Program.cs

public static void Main(string[] args)
{
  string root = @"c:\sample";
  Foo(root);
}

public static void Foo(string parent)
{
    foreach (string dir in Directory.GetDirectories(parent))
    {
        Console.WriteLine($"dir={dir}");
        foreach (string file in Directory.GetFiles(dir))
        {
            Console.WriteLine($"  file={Path.GetFileName(file)}");
        }
        Foo(dir);
    }
}

再帰処理を使わずにファイルを列挙する

次に再帰処理を使わないファイルの探索方法です。標準APIで実現できます。

すごく昔からあるAPIなので実はフォルダの巡回が必要な時に再帰処理が必要なケースは稀によくあるのですが通常使わないと思います。

// Program.cs

public static void Main(string[] args)
{
  string root = @"c:\sample";
  Foo(root);
}

foreach (var dir in Directory.GetDirectories(path, "*", SearchOption.AllDirectories))
{
    Console.WriteLine($"dir={dir}");
    foreach (string file in Directory.GetFiles(dir))
    {
        Console.WriteLine($"  file={Path.GetFileName(file)}");
    }
}

ちなみに再帰処理を使わない方がパフォーマンスが3倍くらい良いです。従って特殊な要件が無ければ再帰処理を使わない方がおすすめです。

そもそもAPIが用意されている

紹介しておいてあれですが、特定のファイルのリストアップがしたいだけの場合以下APIがあらかじめ用意されているので先述の巡回処理は必要ありません(しかもこのAPIはさらに高速に動作します)

// 特定のフォルダ以下の全部取得する
string[] fileList = Directory.GetFiles(path, "*.txt", SearchOption.AllDirectories);

// 逐次取得する
IEnumerable<string> fileList = Directory.EnumerateFiles(path, "*.txt", SearchOption.AllDirectories);

ファイル数が一万とか予想される場合は Enumerate の方を使った方がPCのリソースにやさしいです。

【C#】StartsWithを複数の文字列対応する

string クラスに StartsWith という特定の文字列で始まるかどうかをチェックできるメソッドがあります。

使い方はこんな感じです。

string str = "aaaabbbbcccc";
if(str.StartsWith("aaa"))  // str が "aaa" から始まるかどうかチェックする
{
    // 一致する
}
else
{
   // 一致しない
}

これによって指定した変数内の文字列が指定した文字列から始まっているかどうかを確認できますがこれを複数の文字列で確認できるように改善したいと思います。

string str = "aaabbbccc";
if(str.StartsWith("aaa", "bbb", "ccc"))
{
    // "aaa", "bbb", "ccc" のいずれかに一致する
}
else
{
    // 一致しない
}

確認環境

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

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

実装コード

以下実装で StartsWith, EndsWith, Contains の3つのメソッドを複数対応します。

// StringExtension.cs

public static class StringExtension
{
    // 指定した複数の文字列のどれかに一致するかどうかを判定する
    // true : 存在する / false : 存在しない
    public static bool Contains(this string self, params string[] words) => Contains(self, false, params string[] words);
    public static bool Contains(this string self, bool ignoreCase, params string[] words)
    {
        for (int i = 0; i < words.Length; i++)
        {
            if (string.Compare(self, words[i], ignoreCase) == 0)
            {
                return true;
            }
        }
        return false;
    }

    // 指定した複数の文字列のいずれかから開始する文字列かどうかを判定します。
    // true : 存在する / false : 存在しない
    public static bool StartsWith(this string self, params string[] words) => StartsWith(self, false, params string[] words);
    public static bool StartsWith(this string self, bool ignoreCase, params string[] words)
    {
        for (int i = 0; i < words.Length; i++)
        {
            if (self.StartsWith(words[i], true, null))
            {
                return true;
            }
        }
        return false;
    }

    // 指定した複数の文字列のいずれかから開始する文字列かどうかを判定します。
    // true : 存在する / false : 存在しない
    public static bool EndsWith(this string self, params string[] words) => EndsWith(self, false, params string[] words);
    public static bool EndsWith(this string self, bool ignoreCase, params string[] words)
    {
        for (int i = 0; i < words.Length; i++)
        {
            if (self.EndsWith(words[i], true, null))
            {
                return true;
            }
        }
        return false;
    }
}

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