LoginSignup
9
9

More than 5 years have passed since last update.

汎用IsNull,IsNotNull再び(追記有り)

Last updated at Posted at 2015-10-28

何をしたいのか?

大昔、汎用IsNull,IsNotNullの実装と言うエントリを書いたのですが、
どうにもこうにも以下のような不満がありました。

  1. 本来不要なボックス化が発生する
  2. 上記に伴う、値型のボクシングコストや、それに付随するマネージヒープを利用し、GCが動くことによるパフォーマンスの低下

という、不満があったけど、当時は解決できなかった。
で、今回、その辺割とうまく捌けたのでちょっと書いてみようかなと言うのが今日のお題。

前提としている条件

以下の議論では、次のような条件を設けているのでご了承の程。

  • Nullable<T>を除く値型は、Nullと比較した結果は常にFalseとなる。

今回のゴール

  • Nullable<T>の判別時ボックス化させない。
  • Nullable<T>を除く値型に対する返却値の定数化

当たりが目的になってます。

実装

実装に関しては以下の通り。

NullCheckExtensions.cs

    public static class NullCheckExtensions
    {
        //新型のNull/IsNull
        public static bool IsNull<T>(this T value) => NullChecker<T>.IsNull(value);
        public static bool IsNotNull<T>(this T value) => !IsNull(value);
        //従来型の汎用Null/IsNull
        public static bool TraditionalIsNull(this object obj) => obj == null;
        public static bool TraditionalIsNotNull(this object obj) => obj != null;

        private static class NullChecker<T>
        {
            private static readonly TypeType _type;

            static NullChecker()
            {
                var type = typeof (T);

                if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof (Nullable<>))
                {
                    _type = TypeType.Nullable;
                }
                else if (type.IsValueType)
                {
                    _type = TypeType.Value;
                }
                else
                {
                    _type = TypeType.Reference;
                }
            }

            public static bool IsNull(T value)
            {
                switch (_type)
                {
                    //値型なら常にFalseを返す。
                    case TypeType.Value:
                        return false;

                    //参照形は都度判定
                    case TypeType.Reference:
                        return value == null;

                    //NullableはEqualメソッド呼び出す。
                    case TypeType.Nullable:
                        return value.Equals(null);

                    default:
                        throw new InvalidOperationException("Unexpected type");
                }
            }

            private enum TypeType
            {
                //Nullable以外の値型
                Value,
                //参照形
                Reference,
                //Null許容形
                Nullable
            }
        }
    }

実装のおおまかな解説

ここで、キモになるのは、内部にNullCheckerというStaticClassを持っている点となります。
コレは何をしてるかというと、NullCheckerはジェネリック型なので、Tの種類だけクラスが生成されることになり、
生成毎にNullChekerの静的コンストラクタによって、所与のTがどのよう型なのか、より具体的には、

  • Nullable<>型
  • 値型
  • 参照型

を判定し、IsNullメソッドの呼び出し時に、かつてのようにObject型へのキャスト無しに判定を実行可能にしています。
また、静的コンストラクタは、静的コンストラクター (C# プログラミング ガイド)記載の通り、アプリケーションのライフサイクルの中で必要であれば1回だけ実行されるので、随時判定より特に値型の判定に大きなアドバンデージを持つことになります。

Nullableを特殊扱いしているわけ

Nullableに対しては、実装を見ればわかるとおり、特殊扱いを行っています。これは、文字通り、Nullableは値型かつNullを許容できるので、一般的な値型のコンテキストで判定してしまうと、HasValueがfalseであっても、IsNullメソッドがtrueになってしまうので、この点を何とかする必要がありました。
対策として、Equalsメソッドを明示的に呼び出すことにより、ボックス化を経ること無く判定を可能にしています。

参照型でEqualsメソッドを利用すると酷い目に遭う

case TypeType.Reference:
    return value == null;

この部分を、以下のように書き換えると、

case TypeType.Reference:
    return value.Equals(null);

以下のようなシナリオで、StackOverFlowが発生するので、注意が必要です。


    public class RecursiveCall
    {
        public int Value { get; set; }

        public override bool Equals(object obj)
        {
            var tmp = obj as RecursiveCall;

            if (tmp == null) return false;

            //ここでOpEqualが呼ばれて、無限再帰が完成する。
            return tmp == this;
        }

        public static bool operator ==(RecursiveCall x, RecursiveCall y)
        {
            //ここで、Equalsが呼ばれる。
            if (x.IsNull() && y.IsNull()) return true;
            return (y.IsNotNull()) && (x.IsNotNull()) && (x.Value == y.Value);
        }

        public static bool operator !=(RecursiveCall x, RecursiveCall y) => !(x == y);
    }

どれくらい速くなったか?

最後に、パフォーマンスがどの程度改善したのが検証してみたいと思います。

まず最初に、

  • Nullable
  • string
  • int

型のIsNotNullを判定させてみました。


        private static void Main()
        {

            const int seed = 42;
            const int count = 25000000;

            var chrono = new Stopwatch();

            var rnd = MiscExtensions.GetRandomSequence(seed, count);

            // 今回新たに実装した方式。
            chrono.Start();
            var cnt = GcCounter.GetCurrent();

            var nullableCount = rnd.Select(x => x%3 == 0 ? null : new int?(x)).Count(x => x.IsNotNull());
            var stringCount = rnd.Select(x => x%3 == 0 ? null : x.ToString()).Count(x => x.IsNotNull());
            var intCount = rnd.Select(x => x).Count(x => x.IsNotNull());

            chrono.Stop();
            cnt = GcCounter.GetCurrent() - cnt;

            Console.WriteLine("NewMethod");
            Console.WriteLine($"Nullable:{nullableCount} String:{stringCount} Integer:{intCount}");
            Console.WriteLine($"Elapsed:{chrono.Elapsed}");
            Console.WriteLine(cnt.ToString());

            Console.WriteLine();
            Console.WriteLine();


            //従来のオブジェクトにキャストする方式
            rnd = MiscExtensions.GetRandomSequence(seed, count);
            chrono.Restart();
            cnt = GcCounter.GetCurrent(true);

            nullableCount = rnd.Select(x => x%3 == 0 ? null : new int?(x)).Count(x => x.TraditionalIsNotNull());
            stringCount = rnd.Select(x => x % 3 == 0 ? null : x.ToString()).Count(x => x.TraditionalIsNotNull());
            intCount = rnd.Select(x => x).Count(x => x.TraditionalIsNotNull());

            chrono.Stop();
            cnt = GcCounter.GetCurrent() - cnt;

            Console.WriteLine("TraditionalMethod");
            Console.WriteLine($"Nullable:{nullableCount} String:{stringCount} Integer:{intCount}");
            Console.WriteLine($"Elapsed:{chrono.Elapsed}");
            Console.WriteLine(cnt.ToString());
        }

このサンプルの結果は、デバッガアタッチ無しのリリースビルドで、以下の通りでした。

NewMethod
Nullable:16671891 String:16664383 Integer:25000000
Elapsed:00:00:05.1121741
Gen0:135 Gen1:0 Gen2:0 TTL:135

TraditionalMethod
Nullable:16671891 String:16664383 Integer:25000000
Elapsed:00:00:06.3988913
Gen0:254 Gen1:0 Gen2:0 TTL:254

パフォーマンスに、一応差が付いているのがわかります。

さらに、今回チューニングを行った、値型、Nullable型に動作を絞った以下のサンプルを作りテストしてみた場合、


        private static void Main()
        {

            const int seed = 42;
            const int count = 25000000;

            var chrono = new Stopwatch();

            var rnd = MiscExtensions.GetRandomSequence(seed, count);

            // 今回新たに実装した方式。
            chrono.Start();
            var cnt = GcCounter.GetCurrent();

            var nullableCount = rnd.Select(x => x%3 == 0 ? null : new int?(x)).Count(x => x.IsNotNull());
            var intCount = rnd.Select(x => x).Count(x => x.IsNotNull());

            chrono.Stop();
            cnt = GcCounter.GetCurrent() - cnt;

            Console.WriteLine("NewMethod");
            Console.WriteLine($"Nullable:{nullableCount} Integer:{intCount}");
            Console.WriteLine($"Elapsed:{chrono.Elapsed}");
            Console.WriteLine(cnt.ToString());

            Console.WriteLine();
            Console.WriteLine();


            //従来のオブジェクトにキャストする方式
            rnd = MiscExtensions.GetRandomSequence(seed, count);
            chrono.Restart();
            cnt = GcCounter.GetCurrent(true);

            nullableCount = rnd.Select(x => x%3 == 0 ? null : new int?(x)).Count(x => x.TraditionalIsNotNull());
            intCount = rnd.Select(x => x).Count(x => x.TraditionalIsNotNull());

            chrono.Stop();
            cnt = GcCounter.GetCurrent() - cnt;

            Console.WriteLine("TraditionalMethod");
            Console.WriteLine($"Nullable:{nullableCount} Integer:{intCount}");
            Console.WriteLine($"Elapsed:{chrono.Elapsed}");
            Console.WriteLine(cnt.ToString());
        }

NewMethod
Nullable:16671891 Integer:25000000
Elapsed:00:00:01.7142565
Gen0:0 Gen1:0 Gen2:0 TTL:0

TraditionalMethod
Nullable:16671891 Integer:25000000
Elapsed:00:00:03.3442754
Gen0:119 Gen1:0 Gen2:0 TTL:119

よりパフォーマンスの差異が顕著になります。

まとめ

以前、不備が有りつつもそれ以上できないと考えていた、値型のIsNull/IsNotNull判定を比較的うまく捌けたかなと思います。
その際、Nullableが問題になりましたが、コレも又、Equalsメソッドの呼び出しでマネージヒープを使うこと無く判定可能になりました。

次期C#で実装が予定されている、Null非許容の参照型が追加された場合は、別途調整が必要となると思います。1

色々な実装

本エントリをアップ後、@haxe先生と、@yfakariya先生から、別実装のご提案を頂いたので、
ここに紹介させて頂きます。

この場をお借りして、両先生にお礼申し上げます。

yfakariya先生の実装

オリジナルはこちら

sing System;

static class Program
{
    static void Main()
    {
        Test( "a" );
        Test( 1 );
        Test<int?>( 1 );
    }

    static void Test<T>( T nonNull)
    {
        Console.WriteLine( "IsNull<{0}>(default({0})) :{1}", typeof( T ).Name, NullChecker<T>.IsNull( default( T ) ) );
        Console.WriteLine( "IsNull<{0}>(nonNull) :{1}", typeof( T ).Name, NullChecker<T>.IsNull( nonNull ) );
    }
}

internal static class NullChecker<T>
{
    private static readonly Func<T, bool> _isNull =
        InitializeIsNull();

    private static Func<T, bool> InitializeIsNull()
    {
        if ( !typeof( T ).IsValueType )
        {
            return value => value == null;
        }

        var nullableUnderlyingType = Nullable.GetUnderlyingType( typeof( T ) );
        if ( nullableUnderlyingType != null )
        {
            return
                ( Func<T, bool> )
                typeof( NullableNullChecker<> )
                .MakeGenericType( nullableUnderlyingType )
                .GetMethod( "IsNull" )
                .CreateDelegate( typeof( Func<T, bool> ) );
        }

        return _ => false;
    }

    public static bool IsNull( T value )
    {
        return _isNull( value );
    }
}

internal static class NullableNullChecker<T>
    where T : struct
{
    public static bool IsNull( Nullable<T> value )
    {
        return !value.HasValue;
    }
}

動的に、Nullable.HasValueプロパティを呼び出す実装となっております。

haxe先生の実装

オリジナルはこちら
元実装のNullCheckerのみ抜粋


    static class NullableChecker<T>
    {
        private static readonly bool IsValueType;

        static NullableChecker()
        {
            Type type = typeof (T);
            IsValueType = type.IsValueType && Nullable.GetUnderlyingType(type) == null;
        }

        public static bool IsNull(T value) => IsValueType ? false : value == null;

    }

Switchをシンプルでクールな条件判定になさっています。


  1. とはいえ、多分効率を別とすれば動くとは思います。 

9
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
9