【C#】TypeCodeを使った高速な型判定を行う?

ライブラリなどの比較的低レイヤー寄りの実装をしていると受け取った、ジェネリックな型や、object 型で受け取った変数がどの型なのかを判定するケースがありますが、タイトルの通り TypeCode を使うと Type を使った判定より、効率的かつ高速な処理にできるケースがあります。

今回はそんな TypeCode を使った高速な型判定の方法を紹介します。

確認環境

  • .NET 5, C#9.0
  • VisualStudio2019
  • Windows10

この記事はリリースビルドをコンソールから実行して動作検証をしています。

初めに

まず始めに、対象となる状況ですが以下のようなケースを想定します。

入力されたジェネリックの型 <T> に応じて int なら 1、double なら 2、それ以外なら -1 を返すような処理です。特に何も考慮しない場合以下のような処理を書くと思います。

// 入力された変数の型に応じて数値を返す
public static int Foo<T>(T input)
{
    Type inputType = input.GetType();
    if (inputType == typeof(int))
    {
        return 1;
    }
    else if (inputType == typeof(double))
    {
        return 2;
    }
    return -1;
}

これだと毎回 typeof で型を取る処理が無駄なので static な変数に値をとって型をキャッシュしたりします。

// staticな変数にTypeをキャッシュする
private static readonly Type INT_TYPE = typeof(int);
private static readonly Type DOUBLE_TYPE = typeof(double);

public static int Foo<T>(T input)
{
    Type inputType = input.GetType();
    if (inputType == INT_TYPE) // 判定時に計算済みのキャッシュを利用する
    {
        return 1;
    }
    else if (inputType == DOUBLE_TYPE)
    {
        return 2;
    }
    return -1;
}

この記事では、こういった処理を TypeCode で置き換えることで特定のケースで高速化できるよという話です。

TypeCodeを使った型判定

まず TypeCode が何かですが、TypeCode とは以下のように定義される enum になります。

// System.Runtime.dll

namespace System
{
    //
    // 概要:
    //     Specifies the type of an object.
    public enum TypeCode
    {
        Empty = 0,
        Object = 1,
        DBNull = 2,
        Boolean = 3,
        Char = 4,
        SByte = 5,
        Byte = 6,
        Int16 = 7,
        UInt16 = 8,
        Int32 = 9,
        UInt32 = 10,
        Int64 = 11,
        UInt64 = 12,
        Single = 13,
        Double = 14,
        Decimal = 15,
        DateTime = 16,
        String = 18
    }
}

このように、C# の基本の組み込み型 + null, DBNull, DateTime, string の 4 つの型を加えた 19個が定義されています。

先述の型判定であらかじめ処理する型が static は変数にキャッシュできる場合、まぁまぁ高速に処理することができますが、TypeCode を使うと、特にプリミティブ型などを対象とした処理の速度を僅かに向上させることができます。

この TypeCode を使った実装は以下のように記述できます。

public static string ParseByTypeCode<T>(T input)
{
    TypeCode code = Type.GetTypeCode(input.GetType());
    switch (code)
    {
        case TypeCode.Empty: return nameof(TypeCode.Empty);
        case TypeCode.Object: return nameof(TypeCode.Object);
        case TypeCode.DBNull: return nameof(TypeCode.DBNull);
        case TypeCode.Boolean: return nameof(TypeCode.Boolean);
        case TypeCode.Char: return nameof(TypeCode.Char);
        case TypeCode.SByte: return nameof(TypeCode.SByte);
        case TypeCode.Byte: return nameof(TypeCode.Byte);
        case TypeCode.Int16: return nameof(TypeCode.Int16);
        case TypeCode.UInt16: return nameof(TypeCode.UInt16);
        case TypeCode.Int32: return nameof(TypeCode.Int32);
        case TypeCode.UInt32: return nameof(TypeCode.UInt32);
        case TypeCode.Int64: return nameof(TypeCode.Int64);
        case TypeCode.UInt64: return nameof(TypeCode.UInt64);
        case TypeCode.Single: return nameof(TypeCode.Single);
        case TypeCode.Double: return nameof(TypeCode.Double);
        case TypeCode.Decimal: return nameof(TypeCode.Decimal);
        case TypeCode.DateTime: return nameof(TypeCode.DateTime);
        case TypeCode.String: return nameof(TypeCode.String);
        default: return "unknown";
    }
    
    // C# 9.0~の switch ステートメントを使うと以下のように記述できる
    return Type.GetTypeCode(input.GetType()) switch
    {
        TypeCode.Empty => nameof(TypeCode.Empty),
        TypeCode.Object => nameof(TypeCode.Object),
        TypeCode.DBNull => nameof(TypeCode.DBNull),
        TypeCode.Boolean => nameof(TypeCode.Boolean),
        TypeCode.Char => nameof(TypeCode.Char),
        TypeCode.SByte => nameof(TypeCode.SByte),
        TypeCode.Byte => nameof(TypeCode.Byte),
        TypeCode.Int16 => nameof(TypeCode.Int16),
        TypeCode.UInt16 => nameof(TypeCode.UInt16),
        TypeCode.Int32 => nameof(TypeCode.Int32),
        TypeCode.UInt32 => nameof(TypeCode.UInt32),
        TypeCode.Int64 => nameof(TypeCode.Int64),
        TypeCode.UInt64 => nameof(TypeCode.UInt64),
        TypeCode.Single => nameof(TypeCode.Single),
        TypeCode.Double => nameof(TypeCode.Double),
        TypeCode.Decimal => nameof(TypeCode.Decimal),
        TypeCode.DateTime => nameof(TypeCode.DateTime),
        TypeCode.String => nameof(TypeCode.String),
        _ => "unknown",
    };
}

これで、何がうれしいかというと switch 文で処理ができる点です。冒頭の if 判定だと型を判定する場合、if → else if → else if → else if → 目的の処理、という風に型が見つかるまで if で繰りかえしの判定を行いますが、この TypeCode の場合 switch 文で処理できるので TypeCide → switch → 目的の処理という風に無駄なく目的の処理に飛ぶことができます。

但し、判定できるのは、TypeCode に定義された方だけなので、自作の型の場合は冒頭の判定処理を行う必要があります。

Typeを使った型判定

上述の実装と同様の処理を Type を使ったって行う場合以下のような実装になります。

// static な Type のキャッシュ
public static readonly Type Object = typeof(object);
public static readonly Type DBNull = typeof(DBNull);
public static readonly Type Boolean = typeof(bool);
public static readonly Type Char = typeof(char);
public static readonly Type SByte = typeof(sbyte);
public static readonly Type Byte = typeof(byte);
public static readonly Type Int16 = typeof(short);
public static readonly Type UInt16 = typeof(ushort);
public static readonly Type Int32 = typeof(int);
public static readonly Type UInt32 = typeof(uint);
public static readonly Type Int64 = typeof(long);
public static readonly Type UInt64 = typeof(ulong);
public static readonly Type Single = typeof(float);
public static readonly Type Double = typeof(double);
public static readonly Type Decimal = typeof(decimal);
public static readonly Type DateTime = typeof(DateTime);
public static readonly Type String = typeof(string);

public static string Parse<T>(T input)
{
    Type type = input.GetType();
    if (type == Object) return nameof(Object);
    else if (type == DBNull) return nameof(DBNull);
    else if (type == Boolean) return nameof(Boolean);
    else if (type == Char) return nameof(Char);
    else if (type == SByte) return nameof(SByte);
    else if (type == Byte) return nameof(Byte);
    else if (type == Int16) return nameof(Int16);
    else if (type == UInt16) return nameof(UInt16);
    else if (type == Int32) return nameof(Int32);
    else if (type == UInt32) return nameof(UInt32);
    else if (type == Int64) return nameof(Int64);
    else if (type == UInt64) return nameof(UInt64);
    else if (type == Single) return nameof(Single);
    else if (type == Double) return nameof(Double);
    else if (type == Decimal) return nameof(Decimal);
    else if (type == DateTime) return nameof(DateTime);
    else if (type == String) return nameof(String);
    else return "unknown";
}

事前に型をキャッシュしておかないといけないですし、判定は if なので、プリミティブ型を判定したい場合は、TypeCode + switch が割といい感じかと思います。

速度計測

では、実際の速度計測をしてみたいと思います。

internal class AppMain
{
    private static void Main(string[] args)
    {
        Stopwatch sw1 = new();
        Stopwatch sw2 = new();

        ArrayList list = new()
        {
            new MyClass(),
            DBNull.Value,
            true,
            'a',
            sbyte.MaxValue,
            byte.MaxValue,
            short.MaxValue,
            ushort.MaxValue,
            int.MaxValue,
            uint.MaxValue,
            long.MaxValue,
            ulong.MaxValue,
            float.MaxValue,
            double.MaxValue,
            decimal.MaxValue,
            DateTime.Now,
            "string",
        };

        for (int loop = 0; loop < 10; loop++) {

            for (int i = 0; i < 10000; i++)
            {
                foreach (var item in list)
                {
                    sw1.Start();
                    string a = PrimitiveInfo.Parse(item);
                    sw1.Stop();

                    sw2.Start();
                    string b = PrimitiveInfo.ParseByTypeCode(item);
                    sw2.Stop();
                }
            }

            Console.WriteLine($"[{loop+1}]回目");
            Console.WriteLine($"{sw1.Elapsed.TotalMilliseconds}ms");
            Console.WriteLine($"{sw2.Elapsed.TotalMilliseconds}ms");
        }

        Console.WriteLine($"[合計]");
        Console.WriteLine($"{sw1.Elapsed.TotalMilliseconds}ms");
        Console.WriteLine($"{sw2.Elapsed.TotalMilliseconds}ms");
    }
}

public class MyClass
{
    public int ID { get; set; }
}

public static class PrimitiveInfo
{
    public static readonly Type Object = typeof(object);
    public static readonly Type DBNull = typeof(DBNull);
    public static readonly Type Boolean = typeof(bool);
    public static readonly Type Char = typeof(char);
    public static readonly Type SByte = typeof(sbyte);
    public static readonly Type Byte = typeof(byte);
    public static readonly Type Int16 = typeof(short);
    public static readonly Type UInt16 = typeof(ushort);
    public static readonly Type Int32 = typeof(int);
    public static readonly Type UInt32 = typeof(uint);
    public static readonly Type Int64 = typeof(long);
    public static readonly Type UInt64 = typeof(ulong);
    public static readonly Type Single = typeof(float);
    public static readonly Type Double = typeof(double);
    public static readonly Type Decimal = typeof(decimal);
    public static readonly Type DateTime = typeof(DateTime);
    public static readonly Type String = typeof(string);

    public static string Parse<T>(T input)
    {
        Type type = input.GetType();
        if (type == Object) return nameof(Object);
        else if (type == DBNull) return nameof(DBNull);
        else if (type == Boolean) return nameof(Boolean);
        else if (type == Char) return nameof(Char);
        else if (type == SByte) return nameof(SByte);
        else if (type == Byte) return nameof(Byte);
        else if (type == Int16) return nameof(Int16);
        else if (type == UInt16) return nameof(UInt16);
        else if (type == Int32) return nameof(Int32);
        else if (type == UInt32) return nameof(UInt32);
        else if (type == Int64) return nameof(Int64);
        else if (type == UInt64) return nameof(UInt64);
        else if (type == Single) return nameof(Single);
        else if (type == Double) return nameof(Double);
        else if (type == Decimal) return nameof(Decimal);
        else if (type == DateTime) return nameof(DateTime);
        else if (type == String) return nameof(String);
        else return "unknown";
    }

    public static string ParseByTypeCode<T>(T input)
    {
        TypeCode code = Type.GetTypeCode(input.GetType());
        switch (code)
        {
            case TypeCode.Empty: return nameof(TypeCode.Empty);
            case TypeCode.Object: return nameof(TypeCode.Object);
            case TypeCode.DBNull: return nameof(TypeCode.DBNull);
            case TypeCode.Boolean: return nameof(TypeCode.Boolean);
            case TypeCode.Char: return nameof(TypeCode.Char);
            case TypeCode.SByte: return nameof(TypeCode.SByte);
            case TypeCode.Byte: return nameof(TypeCode.Byte);
            case TypeCode.Int16: return nameof(TypeCode.Int16);
            case TypeCode.UInt16: return nameof(TypeCode.UInt16);
            case TypeCode.Int32: return nameof(TypeCode.Int32);
            case TypeCode.UInt32: return nameof(TypeCode.UInt32);
            case TypeCode.Int64: return nameof(TypeCode.Int64);
            case TypeCode.UInt64: return nameof(TypeCode.UInt64);
            case TypeCode.Single: return nameof(TypeCode.Single);
            case TypeCode.Double: return nameof(TypeCode.Double);
            case TypeCode.Decimal: return nameof(TypeCode.Decimal);
            case TypeCode.DateTime: return nameof(TypeCode.DateTime);
            case TypeCode.String: return nameof(TypeCode.String);
            default: return "unknown";
        }

        // C# 9.0~の switch ステートメントを使うと以下のように記述できる
        return Type.GetTypeCode(input.GetType()) switch
        {
            TypeCode.Empty => nameof(TypeCode.Empty),
            TypeCode.Object => nameof(TypeCode.Object),
            TypeCode.DBNull => nameof(TypeCode.DBNull),
            TypeCode.Boolean => nameof(TypeCode.Boolean),
            TypeCode.Char => nameof(TypeCode.Char),
            TypeCode.SByte => nameof(TypeCode.SByte),
            TypeCode.Byte => nameof(TypeCode.Byte),
            TypeCode.Int16 => nameof(TypeCode.Int16),
            TypeCode.UInt16 => nameof(TypeCode.UInt16),
            TypeCode.Int32 => nameof(TypeCode.Int32),
            TypeCode.UInt32 => nameof(TypeCode.UInt32),
            TypeCode.Int64 => nameof(TypeCode.Int64),
            TypeCode.UInt64 => nameof(TypeCode.UInt64),
            TypeCode.Single => nameof(TypeCode.Single),
            TypeCode.Double => nameof(TypeCode.Double),
            TypeCode.Decimal => nameof(TypeCode.Decimal),
            TypeCode.DateTime => nameof(TypeCode.DateTime),
            TypeCode.String => nameof(TypeCode.String),
            _ => "unknown",
        };
    }
}

[1]回目
6.1562ms
4.1258ms
[2]回目
12.1973ms
8.0042ms
[3]回目
18.2653ms
11.9062ms
[4]回目
24.3487ms
15.8772ms
[5]回目
30.4158ms
19.7781ms
[6]回目
36.5826ms
23.7604ms
[7]回目
42.801ms
27.6657ms
[8]回目
48.8249ms
31.5796ms
[9]回目
54.841ms
35.4524ms
[10]回目
60.9232ms
39.362ms
[合計]
60.9232ms
39.362ms

大体1.5倍くらい高速ですかね?まぁまぁ効果的だと思います。

最後に、とはいっても、、、

ちなみに TypeCode の方が早いと確認しましたが、input.GetType() を利用して型情報を取得しているところを typeof(T) で取得すれば従来の記述方法でも速度的には同じです(input に触ってボクシングが発生しない方が早いに決まってますよね)となると object 型で受けてしまった時の判定だけで有効かと思いますがそもそもそれは避けたいパターンなのでジェネリックでかけるところはジェネリックだし、、、

と考え始めると実際は色々悩ましいです。

TypeCode の取得は IConvertible に GetTypeCode メソッドが定義されているのでプリミティブ型などは基本これを持っているので

TypeCode code;
if (input is IConvertible con)
{
    code = con.GetTypeCode();
}
else
{
    code = Type.GetTypeCode(input.GetType());
}

などとすれば一見効率的そうですが、判定事態にコストが発生するため逆に遅くなってしまいま(汗

実際は、IConvertible を継承したい型だけ受け取るとか、制約としては厳しすぎてライブラリのコードとして適用範囲が狭くなって使いづらいですしなんとも微妙です。このパターンで実際に処理速度が速くなる適用可能なケースも、そう多くなさそうなため、型判定で処理速度を稼ぎたいと思った時に、なんとなく TypeCode でも型判定できるよーって覚えていればいい程度だと思います。

以上。