深いnullチェック、より良い方法はありますか?


130

注:この質問はの導入前に頼まれたC#6 / Visual Studioの2015年には事業者.?

私たちは全員そこに行ったことがあります。例外がないので、nullかどうかをチェックする必要があるcake.frosting.berries.loaderのような深いプロパティがあります。方法は、短絡するifステートメントを使用することです

if (cake != null && cake.frosting != null && cake.frosting.berries != null) ...

これは厳密にはエレガントではありません。チェーン全体をチェックして、null変数/プロパティに直面するかどうかを確認する簡単な方法がおそらくあるはずです。

いくつかの拡張メソッドを使用することは可能ですか、それとも言語機能でしょうか、それとも単に悪い考えですか?


3
私はそれを頻繁に望んでいましたが、私が思いついたすべてのアイデアは実際の問題よりも悪かったです。
peterchen 2010年

他の人々が同じ考えを持っていたことを確認するためにすべての答えと興味深いことに感謝します。私はこれが自分で解決されるのを見てみたいと思いました。エリックの解決策は素晴らしいですが、(IsNull(abc))の場合、または(IsNotNull(abc))の場合は多分、このようなものを書くだけだと思いますそれは私の好みです:)
Homde

あなたがフロスティングをインスタンス化するとき、それはベリーのプロパティを持っているので、コンストラクターのその時点で、フロスティングが空の(nullではない)ベリーを作成するようにインスタンス化されるときはいつでもフロスティングに伝えることができますか?そして、ベリーが変更されたフロスティングはいつでも値のチェックを行いますか????
Doug Chamberlain

多少ゆるく関連していますが、ここで私が回避しようとしていた「ディープヌル」問題に適した手法をいくつか紹介しました。 stackoverflow.com/questions/818642/...
AaronLS

回答:


223

新しい操作「?」の追加を検討しました。必要なセマンティクスを持つ言語に。(これは現在追加されています。以下を参照してください。)つまり、

cake?.frosting?.berries?.loader

コンパイラはすべての短絡チェックを生成します。

それはC#4の障壁を作りませんでした。おそらく、この言語の仮想的な将来のバージョンのためでしょう。

更新(2014):?.オペレータは、現在され、計画の次のロザリンコンパイラのリリースのために。演算子の正確な構文解析と意味解析についてはまだ議論があることに注意してください。

更新(2015年7月): Visual Studio 2015がリリースされ、null条件演算子?.?[]をサポートするC#コンパイラが同梱されています


10
ドットがないと、条件付き(A?B:C)演算子と構文的にあいまいになります。トークンストリーム内の任意の場所を「先読み」することを要求する字句構造を回避しようとします。(残念ながら、C#には既にそのような構成要素があります。これ以上追加することはしません。)
Eric Lippert

33
@Ian:この問題は非常に一般的です。これは、最も頻繁に寄せられるリクエストの1つです。
Eric Lippert、2010

7
@Ian:可能な場合はnullオブジェクトパターンを使用することも好みますが、ほとんどの人は自分で設計したオブジェクトモデルで作業する余裕がありません。既存のオブジェクトモデルの多くはnullを使用しているため、私たちが共存しなければならない世界です。
Eric Lippert、2010

12
@ジョン:私たちはこの機能のリクエストをほとんどすべての経験豊富なプログラマーから受け取ります。MVPは常にこれを要求します。しかし、私は意見が異なることを理解しています。あなたの批判に加えて、建設的な言語設計の提案をしたい場合は、私はそれを検討させていただきます。
Eric Lippert

28
@lazyberezovsky:私はデメテルのいわゆる「法則」を理解したことがありません。まず第一に、それはより正確に「デメテルの提案」と呼ばれるようです。第2に、その論理的な結論に「1つのメンバーアクセスのみ」を適用した結果は、「神のオブジェクト」であり、すべてのオブジェクトはすべてのクライアントのためにすべてを実行する必要があり、クライアントの操作方法を知っているオブジェクトを渡すことができない欲求。私は、デメテルの法則の正反対を好みます。すべてのオブジェクトは少数の問題をうまく解決します。それらの解決策の1つは、「問題をよりよく解決する別のオブジェクトです」
Eric Lippert

27

この質問に触発されて、この種の深いnullチェックを、式ツリーを使用したより簡単できれいな構文で実行する方法を見つけようとしました。階層の深いインスタンスに頻繁にアクセスする必要がある場合は設計が悪い可能性があるという回答には同意しますが、データの表示など、場合によっては非常に役立つこともあると思います。

だから私はあなたが書くことを可能にする拡張メソッドを作成しました:

var berries = cake.IfNotNull(c => c.Frosting.Berries);

式のどの部分もnullでない場合、これはベリーを返します。nullが検出されると、nullが返されます。ただし、いくつかの注意点があります。現在のバージョンでは、単純なメンバーアクセスでのみ機能し、.NET Framework 4でのみ機能します。これは、v4の新機能であるMemberExpression.Updateメソッドを使用するためです。これは、IfNotNull拡張メソッドのコードです。

using System;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace dr.IfNotNullOperator.PoC
{
    public static class ObjectExtensions
    {
        public static TResult IfNotNull<TArg,TResult>(this TArg arg, Expression<Func<TArg,TResult>> expression)
        {
            if (expression == null)
                throw new ArgumentNullException("expression");

            if (ReferenceEquals(arg, null))
                return default(TResult);

            var stack = new Stack<MemberExpression>();
            var expr = expression.Body as MemberExpression;
            while(expr != null)
            {
                stack.Push(expr);
                expr = expr.Expression as MemberExpression;
            } 

            if (stack.Count == 0 || !(stack.Peek().Expression is ParameterExpression))
                throw new ApplicationException(String.Format("The expression '{0}' contains unsupported constructs.",
                                                             expression));

            object a = arg;
            while(stack.Count > 0)
            {
                expr = stack.Pop();
                var p = expr.Expression as ParameterExpression;
                if (p == null)
                {
                    p = Expression.Parameter(a.GetType(), "x");
                    expr = expr.Update(p);
                }
                var lambda = Expression.Lambda(expr, p);
                Delegate t = lambda.Compile();                
                a = t.DynamicInvoke(a);
                if (ReferenceEquals(a, null))
                    return default(TResult);
            }

            return (TResult)a;            
        }
    }
}

これは、式を表す式ツリーを調べて、パーツを次々に評価することによって機能します。結果がnullでないことを確認するたび。

MemberExpression以外の式がサポートされるように、これを拡張できると確信しています。これを概念実証コードと見なしてください。これを使用するとパフォーマンスが低下することに注意してください(多くの場合、問題にはならないでしょうが、タイトループでは使用しないでください:-))。


私はあなたのラムダスキルに感銘を受けました:)しかし、構文は、ifステートメントシナリオでは、少なくとも1つよりも少し複雑に見えます
Homde

かっこいいですが、if .. &&よりも100倍多くのコードが実行されます if .. &&にコンパイルされる場合にのみ価値があります。
Monstieur 2013年

1
あ、それから私はDynamicInvokeそこで見ました。私はそれを
熱心に

24

この拡張機能は、深い入れ子のシナリオに非常に役立つことがわかりました。

public static R Coal<T, R>(this T obj, Func<T, R> f)
    where T : class
{
    return obj != null ? f(obj) : default(R);
}

これは、C#とT-SQLのnullの合体演算子から派生したアイデアです。戻り値の型が常に内部プロパティの戻り値の型であることは素晴らしいことです。

その方法でこれを行うことができます:

var berries = cake.Coal(x => x.frosting).Coal(x => x.berries);

...または上記のわずかなバリエーション:

var berries = cake.Coal(x => x.frosting, x => x.berries);

これは私が知っている最良の構文ではありませんが、機能します。


なぜ「石炭」、それは非常に気味が悪いように見えます。;)ただし、フロスティングがnullの場合、サンプルは失敗します。次のようになっているはずです:var berries = cake.NullSafe(c => c.Frosting.NullSafe(f => f.Berries));
Robert Giesecke

ああ、でも、あなたは2番目の議論が石炭への呼びかけではないことを意味しています。便利な改造です。セレクター(x => x.berries)は、2つの引数を取るCoalメソッド内のCoal呼び出しに渡されます。
John Leidegren、2010

合体または合体という名前はT-SQLから取られたものです。IfNotNullは、nullでない場合に何かが発生することを意味します。ただし、それが何であるかは、IfNotNullメソッド呼び出しでは説明されません。石炭は確かに変わった名前ですが、これは確かに注目に値する奇妙な方法です。
John Leidegren、2010

これに対する文字通りの最高の名前は、「ReturnIfNotNull」または「ReturnOrDefault」のようなものです
John Leidegren

@flq +1 ...このプロジェクトで、IfNotNullと呼ばれます:)
Marc Sigrist

16

Mehrdad Afshariがすでに指摘しているように、デメテルの法則に違反する以外に、決定ロジックには「ディープnullチェック」が必要なようです。

これは、空のオブジェクトをデフォルト値で置き換えたい場合によく発生します。この場合、Nullオブジェクトパターンの実装を検討する必要があります。これは実際のオブジェクトの代理として機能し、デフォルト値と「非アクション」メソッドを提供します。


いいえ、objective-cはnullオブジェクトへのメッセージの送信を許可し、必要に応じて適切なデフォルト値を返します。そこに問題はありません。
ヨハネスルドルフ

2
うん。それがポイントです。基本的に、Nullオブジェクトパターンを使用してObjC動作をエミュレートします。
Mehrdad Afshari、2010年

10

更新: Visual Studio 2015以降、C#コンパイラー(言語バージョン6)が?.演算子を認識するようになり、 "ディープnullチェック"が簡単になりました。詳細については、この回答を参照してください。

コードを再設計することとは別に、 この削除された回答が示唆するように、別の(ひどいですが)オプションは、try…catchブロックを使用して、NullReferenceExceptionその深いプロパティルックアップ中にいつか発生するかどうかを確認することです。

try
{
    var x = cake.frosting.berries.loader;
    ...
}
catch (NullReferenceException ex)
{
    // either one of cake, frosting, or berries was null
    ...
}

私は個人的には次の理由でこれをしません:

  • 見た目が良くない。
  • 例外処理を使用します。例外処理は、例外的な状況を対象とするものであり、通常の操作中に頻繁に発生するものではありません。
  • NullReferenceExceptionsはおそらく明示的に捕捉されるべきではありません。(この質問を参照してください。)

だからいくつかの拡張メソッドを使用することは可能ですか、それとも言語機能でしょうか、[...]

これは、C#でより高度な遅延評価が行われていない限り、またはリフレクションを使用したい場合(おそらくこれもおそらくない)でない限り、ほぼ間違いなく言語機能(.?and ?[]演算子の形式でC#6で利用可能)でなければなりません。パフォーマンスとタイプセーフの理由から良い考えです)。

cake.frosting.berries.loader関数に単純に渡す方法がないため(評価されてnull参照例外がスローされます)、次の方法で一般的なルックアップメソッドを実装する必要があります。調べる:

static object LookupProperty( object startingPoint, params string[] lookupChain )
{
    // 1. if 'startingPoint' is null, return null, or throw an exception.
    // 2. recursively look up one property/field after the other from 'lookupChain',
    //    using reflection.
    // 3. if one lookup is not possible, return null, or throw an exception.
    // 3. return the last property/field's value.
}

...

var x = LookupProperty( cake, "frosting", "berries", "loader" );

(注:コードは編集されています。)

このようなアプローチにはいくつかの問題があります。まず、単純な型のプロパティ値の型の安全性とボックス化の可能性はありません。次に、null何か問題が発生した場合に戻ることができ、呼び出し側の関数でこれを確認する必要があります。または、例外をスローして、開始した場所に戻ります。第三に、それは遅いかもしれません。第四に、それはあなたが始めたものより醜く見えます。

[...]、またはそれは単に悪い考えですか?

私はどちらかにとどまります:

if (cake != null && cake.frosting != null && ...) ...

または、Mehrdad Afshariによる上記の回答を使用してください。


PS:私がこの回答を書いたとき、ラムダ関数の式ツリーは考慮していませんでした。この方向での解決策については、たとえば@driisの回答を参照してください。また、一種のリフレクションに基づいているため、より単純なソリューション(if (… != null & … != null) …)ほどうまく機能しない可能性がありますが、構文の観点からはより適切に判断できます。


2
なぜこれが反対票が投じられたのかわからない、私はバランスに賛成票を投じた:答えは正解であり、新しい側面をもたらします(そしてこのソリューションの欠点を明示的に述べています...)
MartinStettner

「上記のMehrdad Afshariによる回答」はどこにありますか?
Marson Mao

1
@MarsonMao:その回答は当面削除されました。(あなたのSOランクが十分に高い場合でもそれを読むことができます。)私の間違いを指摘してくれてありがとう:私はハイパーリンクを使用して他の回答を参照する必要があります。回答は決まった順序で表示されません)。回答を更新しました。
stakx-2015年

5

driisの回答は興味深いものですが、賢明な方法では少し高すぎると思います。多くのデリゲートをコンパイルするのではなく、プロパティパスごとに1つのラムダをコンパイルし、それをキャッシュしてから、多くの型を再度呼び出すことをお勧めします。

以下のNullCoalesceは、まさにそれを行います。パスがnullの場合、nullチェックとdefault(TResult)を返す新しいラムダ式を返します。

例:

NullCoalesce((Process p) => p.StartInfo.FileName)

式を返します

(Process p) => (p != null && p.StartInfo != null ? p.StartInfo.FileName : default(string));

コード:

    static void Main(string[] args)
    {
        var converted = NullCoalesce((MethodInfo p) => p.DeclaringType.Assembly.Evidence.Locked);
        var converted2 = NullCoalesce((string[] s) => s.Length);
    }

    private static Expression<Func<TSource, TResult>> NullCoalesce<TSource, TResult>(Expression<Func<TSource, TResult>> lambdaExpression)
    {
        var test = GetTest(lambdaExpression.Body);
        if (test != null)
        {
            return Expression.Lambda<Func<TSource, TResult>>(
                Expression.Condition(
                    test,
                    lambdaExpression.Body,
                    Expression.Default(
                        typeof(TResult)
                    )
                ),
                lambdaExpression.Parameters
            );
        }
        return lambdaExpression;
    }

    private static Expression GetTest(Expression expression)
    {
        Expression container;
        switch (expression.NodeType)
        {
            case ExpressionType.ArrayLength:
                container = ((UnaryExpression)expression).Operand;
                break;
            case ExpressionType.MemberAccess:
                if ((container = ((MemberExpression)expression).Expression) == null)
                {
                    return null;
                }
                break;
            default:
                return null;
        }
        var baseTest = GetTest(container);
        if (!container.Type.IsValueType)
        {
            var containerNotNull = Expression.NotEqual(
                container,
                Expression.Default(
                    container.Type
                )
            );
            return (baseTest == null ?
                containerNotNull :
                Expression.AndAlso(
                    baseTest,
                    containerNotNull
                )
            );
        }
        return baseTest;
    }


3

私もしばしば、より単純な構文を望んでいました!nullの可能性があるmethod-return-valuesがある場合、特に醜くなります。追加の変数が必要になるためです(例:cake.frosting.flavors.FirstOrDefault().loader

ただし、これは私が使用するかなりまともな代替手段です:Null-Safe-Chainヘルパーメソッドを作成します。これは(Coal拡張メソッドを使用した)上記の@Johnの回答とかなり似ていることを理解していますが、より簡単で入力が少ないことがわかります。これは次のようになります。

var loader = NullSafe.Chain(cake, c=>c.frosting, f=>f.berries, b=>b.loader);

ここに実装があります:

public static TResult Chain<TA,TB,TC,TResult>(TA a, Func<TA,TB> b, Func<TB,TC> c, Func<TC,TResult> r) 
where TA:class where TB:class where TC:class {
    if (a == null) return default(TResult);
    var B = b(a);
    if (B == null) return default(TResult);
    var C = c(B);
    if (C == null) return default(TResult);
    return r(C);
}

また、いくつかのオーバーロード(2〜6個のパラメーターを使用)と、チェーンを値タイプまたはデフォルトで終了できるようにするオーバーロードも作成しました。これは私にとって本当にうまくいきます!



1

John Leidegren回答で示唆されているように、これを回避する 1つの方法は、拡張メソッドとデリゲートを使用することです。それらを使用すると、次のようになります。

int? numberOfBerries = cake
    .NullOr(c => c.Frosting)
    .NullOr(f => f.Berries)
    .NullOr(b => b.Count());

実装は、値型、参照型、およびnull許容値型に対して機能させる必要があるため、面倒です。完全な実装については、Timwiのnull値をチェックする適切な方法は何ですか?」の回答を参照してください。


1

または、リフレクションを使用することもできます:)

反射機能:

public Object GetPropValue(String name, Object obj)
    {
        foreach (String part in name.Split('.'))
        {
            if (obj == null) { return null; }

            Type type = obj.GetType();
            PropertyInfo info = type.GetProperty(part);
            if (info == null) { return null; }

            obj = info.GetValue(obj, null);
        }
        return obj;
    }

使用法:

object test1 = GetPropValue("PropertyA.PropertyB.PropertyC",obj);

私のケース(リフレクション関数でnullの代わりにDBNull.Valueを返す):

cmd.Parameters.AddWithValue("CustomerContactEmail", GetPropValue("AccountingCustomerParty.Party.Contact.ElectronicMail.Value", eInvoiceType));

1

このコードを試してください:

    /// <summary>
    /// check deep property
    /// </summary>
    /// <param name="obj">instance</param>
    /// <param name="property">deep property not include instance name example "A.B.C.D.E"</param>
    /// <returns>if null return true else return false</returns>
    public static bool IsNull(this object obj, string property)
    {
        if (string.IsNullOrEmpty(property) || string.IsNullOrEmpty(property.Trim())) throw new Exception("Parameter : property is empty");
        if (obj != null)
        {
            string[] deep = property.Split('.');
            object instance = obj;
            Type objType = instance.GetType();
            PropertyInfo propertyInfo;
            foreach (string p in deep)
            {
                propertyInfo = objType.GetProperty(p);
                if (propertyInfo == null) throw new Exception("No property : " + p);
                instance = propertyInfo.GetValue(instance, null);
                if (instance != null)
                    objType = instance.GetType();
                else
                    return true;
            }
            return false;
        }
        else
            return true;
    }

0

私はこれを昨夜投稿し、友人が私にこの質問を指摘しました。それが役に立てば幸い。その後、次のようなことができます:

var color = Dis.OrDat<string>(() => cake.frosting.berries.color, "blue");


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;

namespace DeepNullCoalescence
{
  public static class Dis
  {
    public static T OrDat<T>(Expression<Func><T>> expr, T dat)
    {
      try
      {
        var func = expr.Compile();
        var result = func.Invoke();
        return result ?? dat; //now we can coalesce
      }
      catch (NullReferenceException)
      {
        return dat;
      }
    }
  }
}

ブログ全文はこちらをご覧ください

同じ友人もあなたがこれ見ることを提案しました。


3
Expressionコンパイルしてキャッチするだけの場合は、なぜ迷惑をかけるのですか?だけを使用してくださいFunc<T>
Scott Rippey、

0

私はここからコードを少し変更して、尋ねられた質問に対して機能するようにしました:

public static class GetValueOrDefaultExtension
{
    public static TResult GetValueOrDefault<TSource, TResult>(this TSource source, Func<TSource, TResult> selector)
    {
        try { return selector(source); }
        catch { return default(TResult); }
    }
}

そしてはい、これはtry / catchのパフォーマンスに影響するためおそらく最適なソリューションではありませが、機能します:>

使用法:

var val = cake.GetValueOrDefault(x => x.frosting.berries.loader);

0

これを達成する必要がある場合は、次のようにします。

使用法

Color color = someOrder.ComplexGet(x => x.Customer.LastOrder.Product.Color);

または

Color color = Complex.Get(() => someOrder.Customer.LastOrder.Product.Color);

ヘルパークラスの実装

public static class Complex
{
    public static T1 ComplexGet<T1, T2>(this T2 root, Func<T2, T1> func)
    {
        return Get(() => func(root));
    }

    public static T Get<T>(Func<T> func)
    {
        try
        {
            return func();
        }
        catch (Exception)
        {
            return default(T);
        }
    }
}

-3

私はObjective-Cのアプローチが好きです:

「Objective-C言語は、この問題に対する別のアプローチを採用しており、nilでメソッドを呼び出さず、代わりにそのようなすべての呼び出しに対してnilを返します。」

if (cake.frosting.berries != null) 
{
    var str = cake.frosting.berries...;
}

1
他の言語が行うこと(およびその意見)は、C#で動作させることとはほとんど関係ありません。それは誰もがC#の問題を解決するのに役立ちません
ADyson
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.