てくメモ

trivial な notes

【C#】文字列から<T>型への汎用コンバート


【追記】
.NET 8 以降は、IParsable<T>インターフェースでこの記事の範囲のことを扱えます。
【C#】文字列から<T>型への汎用コンバート : IParseble<TSelf> - てくメモ


例えば文字列からのジェネリックな汎用コンバートを用意したいとする。

このとき、手段としてTypeConverterが挙げられる場合がある。
TypeDescriptor.GetConverter(Type)から利用できる。

public T ConvertByDescriptor<T>(string str)
{
    var obj = TypeDescriptor.GetConverter(typeof(T)).ConvertFromString(str);
    return obj is T result
        ? result
        : throw new NotSupportedException();
}

存在を知っていれば手短に汎用コンバートを用意できる。

ただ、都合のよいことだけではなくて、標準ライブラリ内ではGUI関連で利用されているようなAPIであり、あまり軽量な処理ではない。

様々な型にコンバートしたい、というのは大抵シリアライズ・デシリアライズの文脈なので、よいならシリアライザの利用を考えたい。

そうでない場合も、型分岐での実装がある。
※ 更新:typeof(T)を変数に取ってしまっていたのを変更(JIT最適化の点で変数に取らないほうがよかった)

public T ConvertByTypeSwitch<T>(string str)
{
    if (typeof(T) == typeof(int))
    {
        return (T)(object)int.Parse(str);
    }
    else if(typeof(T) == typeof(double))
    {
        return (T)(object)double.Parse(str);
    }
    else if (typeof(T) == typeof(char))
    {
        return (T)(object)char.Parse(str);
    }
    else if (typeof(T) == typeof(string))
    {
        return (T)(object)str;
    }
    else if(typeof(T) == typeof(bool))
    {
        return (T)(object)bool.Parse(str);
    }

    throw new NotSupportedException();
}

なお、標準ライブラリ内でも同様にされている。
一例:Source Browserdouble.TryConvertTo<T>


BenchmarkDotNet で確認。

[Params(1, 256)]
public int N;

private readonly string[] lines = new string[]
{
    "12",
    "12.3",
    "c",
    "str",
    "true"
};

[Benchmark]
public string ByTypeDescriptor()
{
    int n = default; double d = default; char c = default; string? str = default; bool b = default;
    for (int i = 0; i < N; i++)
    {
        n   = ConvertByDescriptor<int>(lines[0]);
        d   = ConvertByDescriptor<double>(lines[1]);
        c   = ConvertByDescriptor<char>(lines[2]);
        str = ConvertByDescriptor<string>(lines[3]);
        b   = ConvertByDescriptor<bool>(lines[4]);
    }

    return $"{n}, {d}, {c}, {str}, {b}";
}

[Benchmark]
public string ByTypeSwitch()
{
    int n = default; double d = default; char c = default; string? str = default; bool b = default;
    for (int i = 0; i < N; i++)
    {
        n   = ConvertByTypeSwitch<int>(lines[0]);
        d   = ConvertByTypeSwitch<double>(lines[1]);
        c   = ConvertByTypeSwitch<char>(lines[2]);
        str = ConvertByTypeSwitch<string>(lines[3]);
        b   = ConvertByTypeSwitch<bool>(lines[4]);
    }

    return $"{n}, {d}, {c}, {str}, {b}";
}
           Method |   N |         Mean |        Error |       StdDev |   Gen0 | Allocated |
----------------- |---- |-------------:|-------------:|-------------:|-------:|----------:|
 ByTypeDescriptor |   1 |   1,421.6 ns |     349.3 ns |     19.15 ns | 0.0534 |     168 B |
     ByTypeSwitch |   1 |     357.3 ns |     391.0 ns |     21.43 ns | 0.0229 |      72 B |
 ByTypeDescriptor | 256 | 313,558.4 ns | 303,560.5 ns | 16,639.18 ns | 7.8125 |   24648 B |
     ByTypeSwitch | 256 |  32,820.8 ns |  32,742.2 ns |  1,794.71 ns |      - |      72 B |

時間の差も大きいし、アロケーションの差もある。

中身を確認していないけれど、見る限り型ごとのコンバーターのキャッシュがライブラリ内でされていないと思われるので、重ねて行う場合は型ごとにコンバーターを自前で再利用すれば差は小さくなる。
ただ、そうした機構を用意するくらいなら素直に型分岐でやった方がよいと思われる。