web-dev-qa-db-ja.com

C#でnullチェックを行うよりクリーンな方法は?

このインターフェイスを持っているとします

interface IContact
{
    IAddress address { get; set; }
}

interface IAddress
{
    string city { get; set; }
}

class Person : IPerson
{
    public IContact contact { get; set; }
}

class test
{
    private test()
    {
        var person = new Person();
        if (person.contact.address.city != null)
        {
            //this will never work if contact is itself null?
        }
    }
}

Person.Contact.Address.City != null(Cityがnullかどうかを確認するために機能します。)

ただし、住所、連絡先、または個人自体がヌルの場合、このチェックは失敗します。

現在、私が考えることができる1つの解決策はこれでした:

if (Person != null && Person.Contact!=null && Person.Contact.Address!= null && Person.Contact.Address.City != null)

{ 
    // Do some stuff here..
}

これを行うよりクリーンな方法はありますか?

nullチェックが(something == null)として実行されるのは本当に好きではありません。代わりに、something.IsNull()メソッドのような何かをする別の素敵な方法がありますか?

一般的な方法では、式ツリーを使用して、拡張メソッドで確認できます。

if (!person.IsNull(p => p.contact.address.city))
{
    //Nothing is null
}

完全なコード:

public class IsNullVisitor : ExpressionVisitor
{
    public bool IsNull { get; private set; }
    public object CurrentObject { get; set; }

    protected override Expression VisitMember(MemberExpression node)
    {
        base.VisitMember(node);
        if (CheckNull())
        {
            return node;
        }

        var member = (PropertyInfo)node.Member;
        CurrentObject = member.GetValue(CurrentObject,null);
        CheckNull();
        return node;
    }

    private bool CheckNull()
    {
        if (CurrentObject == null)
        {
            IsNull = true;
        }
        return IsNull;
    }
}

public static class Helper
{
    public static bool IsNull<T>(this T root,Expression<Func<T, object>> getter)
    {
        var visitor = new IsNullVisitor();
        visitor.CurrentObject = root;
        visitor.Visit(getter);
        return visitor.IsNull;
    }
}

class Program
{
    static void Main(string[] args)
    {
        Person nullPerson = null;
        var isNull_0 = nullPerson.IsNull(p => p.contact.address.city);
        var isNull_1 = new Person().IsNull(p => p.contact.address.city);
        var isNull_2 = new Person { contact = new Contact() }.IsNull(p => p.contact.address.city);
        var isNull_3 =  new Person { contact = new Contact { address = new Address() } }.IsNull(p => p.contact.address.city);
        var notnull = new Person { contact = new Contact { address = new Address { city = "LONDON" } } }.IsNull(p => p.contact.address.city);
    }
}
236
Toto

コードには、null参照を確認する必要があるよりも大きな問題がある場合があります。現状では、 デメテルの法則 に違反している可能性があります。

デメテルの法則は、「自分自身を繰り返さないでください」のようなヒューリスティックの1つであり、簡単に保守可能なコードを作成するのに役立ちます。プログラマは、直接のスコープから遠く離れたものにアクセスしないように指示します。たとえば、次のコードがあるとします。

public interface BusinessData {
  public decimal Money { get; set; }
}

public class BusinessCalculator : ICalculator {
  public BusinessData CalculateMoney() {
    // snip
  }
}

public BusinessController : IController {
  public void DoAnAction() {
    var businessDA = new BusinessCalculator().CalculateMoney();
    Console.WriteLine(businessDA.Money * 100d);
  }
}

DoAnActionメソッドは、デメテルの法則に違反しています。 1つの関数で、BusinessCalcualtorBusinessData、およびdecimalにアクセスします。これは、次の変更のいずれかが行われた場合、行をリファクタリングする必要があることを意味します。

  • BusinessCalculator.CalculateMoney()の戻り値の型が変わります。
  • BusinessData.Moneyのタイプが変更されます

Hadの状況を考慮すると、これらの変更はかなり起こりそうです。このようなコードがコードベース全体に記述されている場合、これらの変更を行うことは非常に高価になる可能性があります。それに加えて、BusinessControllerBusinessCalculatorBusinessDataの両方のタイプに結合されていることを意味します。

この状況を回避する1つの方法は、次のようにコードを書き直すことです。

public class BusinessCalculator : ICalculator {
  private BusinessData CalculateMoney() {
    // snip
  }
  public decimal CalculateCents() {
    return CalculateMoney().Money * 100d;
  }
}

public BusinessController : IController {
  public void DoAnAction() {
    Console.WriteLine(new BusinessCalculator().CalculateCents());
  }
}

さて、上記の変更のいずれかを行った場合、コードをもう1つリファクタリングする必要があるのはBusinessCalculator.CalculateCents()メソッドだけです。また、BusinessControllerBusinessDataへの依存関係を排除しました。


あなたのコードには同様の問題があります:

interface IContact
{
    IAddress address { get; set; }
}

interface IAddress
{
    string city { get; set; }
}

class Person : IPerson
{
    public IContact contact { get; set; }
}

class Test {
  public void Main() {
    var contact = new Person().contact;
    var address = contact.address;
    var city = address.city;
    Console.WriteLine(city);
  }
}

以下の変更のいずれかが行われた場合、私が書いたメインメソッドまたは書いたヌルチェックをリファクタリングする必要があります。

  • IPerson.contactのタイプが変更されます
  • IContact.addressのタイプが変更されます
  • IAddress.cityのタイプが変更されます

Nullチェックを書き直すよりも、コードのより深いリファクタリングを検討すべきだと思います。


とはいえ、デメテルの法則に従うことが不適切な場合もあると思います。 (結局のところ、それは「法則」と呼ばれていますが、ヒューリスティックであり、厳密な規則ではありません。)

特に、次の場合に思う:

  1. プログラムの永続層に保存されたレコードを表すいくつかのクラスがあり、かつ
  2. 将来、これらのクラスをリファクタリングする必要がないと確信しています。

これらのクラスを特に扱う場合、デメテルの法則を無視することは受け入れられます。これは、アプリケーションが使用するデータを表すため、あるデータオブジェクトから別のデータオブジェクトに到達することは、プログラム内の情報を探索する方法です。上記の私の例では、デメテルの法則に違反することによって引き起こされたカップリングはより深刻でした。スタックの上部にあるコントローラーから、スタックの真ん中にあるビジネスロジック計算機を介してデータクラスに到達する可能性がありました永続層で。

PersonContactAddressなどの名前では、クラスがデータ層POCOのように見えるため、この潜在的な例外をデメテルの法則に上げます。その場合、将来リファクタリングする必要はないと確信している場合は、特定の状況でデメテルの法則を無視することができます。

62
Kevin

あなたの場合は、人のためのプロパティを作成できます

public bool HasCity
{
   get 
   { 
     return (this.Contact!=null && this.Contact.Address!= null && this.Contact.Address.City != null); 
   }     
}

しかし、あなたはまだ人がnullかどうかを確認する必要があります

if (person != null && person.HasCity)
{

}

他の質問に、文字列については、この方法でnullまたは空かどうかも確認できます:

string s = string.Empty;
if (!string.IsNullOrEmpty(s))
{
   // string is not null and not empty
}
if (!string.IsNullOrWhiteSpace(s))
{
   // string is not null, not empty and not contains only white spaces
}
48
Koryu

まったく異なるオプション(これは十分に活用されていないと思います)は、 null object pattern です。特定の状況でそれが理にかなっているかどうかを判断するのは難しいですが、試してみる価値があるかもしれません。つまり、NullContactの代わりに使用するNullAddress実装、null実装などがあります。そうすれば、ほとんどのnullチェックを取り除くことができますが、もちろん、これらの実装の設計に入れなければならないことをいくらか犠牲にしてしまいます。

アダムが彼のコメントで指摘したように、これはあなたが書くことを可能にします

if (person.Contact.Address.City is NullCity)

本当に必要な場合に。もちろん、これは都市が本当に重要なオブジェクトである場合にのみ意味があります...

あるいは、nullオブジェクトはシングルトンとして実装できます(たとえば、nullオブジェクトパターンの使用に関する実用的な指示については here を、C#のシングルトンに関する指示については here を参照してください) )これにより、従来の比較を使用できます。

if (person.Contact.Address.City == NullCity.Instance)

個人的には、この方法を好むのは、パターンに慣れていない人にとって読みやすいと思うからです。

37
bigge

2014年4月28日更新:C#vNextではヌル伝播が計画されています


Nullチェックの伝播よりも大きな問題があります。 読解可能な別の開発者が理解にできるコードを目指してください。

頻繁に行われるチェックの場合は、プロパティまたはメソッド呼び出しとしてPersonクラス内にカプセル化することを検討してください。


そうは言っても、無償のFuncとジェネリックです!

私はこれを決してしませんが、ここに別の選択肢があります:

class NullHelper
{
    public static bool ChainNotNull<TFirst, TSecond, TThird, TFourth>(TFirst item1, Func<TFirst, TSecond> getItem2, Func<TSecond, TThird> getItem3, Func<TThird, TFourth> getItem4)
    {
        if (item1 == null)
            return false;

        var item2 = getItem2(item1);

        if (item2 == null)
            return false;

        var item3 = getItem3(item2);

        if (item3 == null)
            return false;

        var item4 = getItem4(item3);

        if (item4 == null)
            return false;

        return true;
    }
}

呼ばれる:

    static void Main(string[] args)
    {
        Person person = new Person { Address = new Address { PostCode = new Postcode { Value = "" } } };

        if (NullHelper.ChainNotNull(person, p => p.Address, a => a.PostCode, p => p.Value))
        {
            Console.WriteLine("Not null");
        }
        else
        {
            Console.WriteLine("null");
        }

        Console.ReadLine();
    }
26

2番目の質問、

Nullチェックが(何か== null)として実行されるのは本当に好きではありません。代わりに、something.IsNull()メソッドのような何かをする別の素敵な方法がありますか?

拡張メソッドを使用して解決できます:

public static class Extensions
{
    public static bool IsNull<T>(this T source) where T : class
    {
        return source == null;
    }
}
14
MarcinJuraszek

何らかの理由で「オーバーザトップ」ソリューションのいずれかを使用しても構わない場合は、私の ブログ投稿 で説明されているソリューションをチェックアウトすることをお勧めします。式を評価する前に、式ツリーを使用して値がnullかどうかを確認します。ただし、パフォーマンスを許容範囲に保つために、ILコードを作成してキャッシュします。

ソリューションでは、これを書くことができます:

string city = person.NullSafeGet(n => n.Contact.Address.City);
10

あなたは書ける:

public static class Extensions
    {
        public static bool IsNull(this object obj)
        {
            return obj == null;
        }
    }

その後:

string s = null;
if(s.IsNull())
{

}

時々これは理にかなっています。しかし、個人的に私はそのようなことを避けます...これは実際にnullであるオブジェクトのメソッドを呼び出すことができる理由が明確ではないためです。

7

次のような別のmethodで実行します。

private test()
{
    var person = new Person();
    if (!IsNull(person))
    {
        // Proceed
              ........

IsNullmethodの場所

public bool IsNull(Person person)
{
    if(Person != null && 
       Person.Contact != null && 
       Person.Contact.Address != null && 
       Person.Contact.Address.City != null)
          return false;
    return true;
}
5
Ashok Damani

C#が必要ですか、それとも 。NET のみですか?別の.NET言語を混合できる場合は、 Oxygene をご覧ください。これは、.NET(およびOOおよび Cocoa も対象とする、驚くほど非常に現代的なJava言語です。そうです。すべてネイティブです。すばらしいツールチェーン。)

Oxygeneには、まさにあなたが求めることを行うコロン演算子があります。 その他の言語機能ページ から引用するには:

コロン( ":")演算子

Oxygeneでは、多くの言語と同様に、「。」の影響を受けました。演算子は、次のようなクラスまたはオブジェクトのメンバーを呼び出すために使用されます

var x := y.SomeProperty;

これは、「y」に含まれるオブジェクトを「逆参照」し、プロパティゲッターを呼び出し(この場合)、その値を返します。 「y」が割り当てられていない場合(つまり「nil」)、例外がスローされます。

「:」演算子はほぼ同じ方法で機能しますが、割り当てられていないオブジェクトに例外をスローする代わりに、結果は単純にnilになります。開発者向けこれはObjective-Cから来ていますが、これはObjective-Cのメソッド呼び出しが[]構文を使用してどのように機能するかという点でおなじみです。

...(中略)

「:」が本当に輝くのは、チェーン内のプロパティにアクセスするときです。任意の要素がnilになる可能性があります。たとえば、次のコード:

var y := MyForm:OkButton:Caption:Length;

エラーなしで実行され、チェーン内のオブジェクト(フォーム、ボタン、またはキャプション)のいずれかがnilの場合、nilを返します。

4
David
try
{
  // do some stuff here
}
catch (NullReferenceException e)
{
}

実際にこれを行わないでください。 nullチェックを行い、どのフォーマットが最適かを判断します。

3
jwg

このような参照チェーンは、たとえばORMツールを使用し、クラスを可能な限り純粋に保ちたい場合に発生する可能性があります。このシナリオでは、うまく回避できないと思います。

次の拡張メソッド「family」があります。このメソッドは、呼び出されるオブジェクトがnullであるかどうかをチェックし、そうでない場合は、要求されたプロパティの1つを返すか、それを使用していくつかのメソッドを実行します。もちろん、これは参照型に対してのみ機能します。そのため、対応する一般的な制約があります。

public static TRet NullOr<T, TRet>(this T obj, Func<T, TRet> getter) where T : class
{
    return obj != null ? getter(obj) : default(TRet);
}

public static void NullOrDo<T>(this T obj, Action<T> action) where T : class
{
    if (obj != null)
        action(obj);
}

これらのメソッドは、手動のソリューションに比べてオーバーヘッドをほとんど追加せず(リフレクションも、式ツリーもありません)、より優れた構文(IMO)を実現できます。

var city = person.NullOr(e => e.Contact).NullOr(e => e.Address).NullOr(e => e.City);
if (city != null)
    // do something...

またはメソッドで:

person.NullOrDo(p => p.GoToWork());

ただし、コードの長さがあまり変わらないことについて明確に議論することができます。

3
Zoltán Tamási

これに役立つ拡張機能があります。 ValueOrDefault()。ラムダ文を受け入れて評価し、予想される例外(NREまたはIOE)がスローされた場合は評価値またはデフォルト値を返します。

    /// <summary>
    /// Provides a null-safe member accessor that will return either the result of the lambda or the specified default value.
    /// </summary>
    /// <typeparam name="TIn">The type of the in.</typeparam>
    /// <typeparam name="TOut">The type of the out.</typeparam>
    /// <param name="input">The input.</param>
    /// <param name="projection">A lambda specifying the value to produce.</param>
    /// <param name="defaultValue">The default value to use if the projection or any parent is null.</param>
    /// <returns>the result of the lambda, or the specified default value if any reference in the lambda is null.</returns>
    public static TOut ValueOrDefault<TIn, TOut>(this TIn input, Func<TIn, TOut> projection, TOut defaultValue)
    {
        try
        {
            var result = projection(input);
            if (result == null) result = defaultValue;
            return result;
        }
        catch (NullReferenceException) //most reference types throw this on a null instance
        {
            return defaultValue;
        }
        catch (InvalidOperationException) //Nullable<T> throws this when accessing Value
        {
            return defaultValue;
        }
    }

    /// <summary>
    /// Provides a null-safe member accessor that will return either the result of the lambda or the default value for the type.
    /// </summary>
    /// <typeparam name="TIn">The type of the in.</typeparam>
    /// <typeparam name="TOut">The type of the out.</typeparam>
    /// <param name="input">The input.</param>
    /// <param name="projection">A lambda specifying the value to produce.</param>
    /// <returns>the result of the lambda, or default(TOut) if any reference in the lambda is null.</returns>
    public static TOut ValueOrDefault<TIn, TOut>(this TIn input, Func<TIn, TOut> projection)
    {
        return input.ValueOrDefault(projection, default(TOut));
    }

特定のデフォルト値を受け取らないオーバーロードは、すべての参照タイプに対してnullを返します。これはあなたのシナリオで動作するはずです:

class test
{
    private test()
    {
        var person = new Person();
        if (person.ValueOrDefault(p=>p.contact.address.city) != null)
        {
            //the above will return null without exception if any member in the chain is null
        }
    }
}
3
KeithS

私の考えでは、 平等演算子は、より安全で優れた方法ではありません 参照平等のため。

ReferenceEquals(obj, null)を使用する方が常に良いです。これは常に機能します。一方、等価演算子(==)はオーバーロードされる可能性があり、参照の代わりに値が等しいかどうかをチェックしている可能性があるため、ReferenceEquals()の方が安全で優れた方法です。

class MyClass {
   static void Main() {
      object o = null;
      object p = null;
      object q = new Object();

      Console.WriteLine(Object.ReferenceEquals(o, p));
      p = q;
      Console.WriteLine(Object.ReferenceEquals(p, q));
      Console.WriteLine(Object.ReferenceEquals(o, p));
   }
}

リファレンス:MSDNの記事Object.ReferenceEqualsメソッド

しかし、ここにもヌル値に対する私の考えがあります

  • 一般に、データがないことを誰かが示しようとしている場合は、null値を返すのが最善の方法です。

  • オブジェクトがnullではなく空の場合、データが返されたことを意味しますが、nullを返すことは何も返されなかったことを明確に示します。

  • また、IMOでは、nullを返す場合、オブジェクトのメンバーにアクセスしようとするとnull例外が発生します。これは、バグのあるコードを強調表示するのに役立ちます。

C#には、2種類の同等性があります。

  • 参照平等および
  • 値の平等。

型が不変である場合、演算子==をオーバーロードして、参照の等価性ではなく値の等価性を比較すると便利です。

不変の型で演算子==をオーバーライドすることはお勧めしません。

詳細については、MSDNの記事Equals()および演算子のオーバーロードのガイドライン==(C#プログラミングガイド)を参照してください。

2
Microtechie

私がC#を愛しているのと同じくらい、これはオブジェクトインスタンスを直接操作するときにC++に好意的なものの1つです。一部の宣言は単純にcannot nullであるため、nullをチェックする必要はありません。

C#でこのパイのスライスを取得する最良の方法は(あなたの側では少し再設計が多すぎるかもしれません-その場合、他の答えを選んでください)structを使用することです。構造体にインスタンス化されていない「デフォルト」値(つまり、0、0.0、null文字列)がある状況では、「if(myStruct == null)」を確認する必要はありません。

もちろん、それらの使用法を理解せずにそれらに切り替えません。実際には大きなデータブロックではなく、値タイプに使用される傾向があります-ある変数から別の変数に構造体を割り当てると、実際にデータを実際にコピーし、元の各値のコピーを作成する傾向があります( refキーワードを使用してこれを回避できます-繰り返しますが、単に使用するのではなく、読み上げてください)。それでも、StreetAddressのようなものに合うかもしれません-私は確かに、私がnullチェックしたくないものに怠laに使用することはありません。

1
Katana314

「city」変数を使用する目的に応じて、nullチェックを異なるクラスに分けることがより簡単な方法です。そうすれば、デメテルの法則に違反することもありません。代わりに:

if (person != null && person.contact != null && person.contact.address != null && person.contact.address.city != null)
{ 
    // do some stuff here..
}

あなたが持っているだろう:

class test
{
    private test()
    {
        var person = new Person();
        if (person != null)
        {
            person.doSomething();
        }
    }
}

...

/* Person class */
doSomething() 
{
    if (contact != null)
    {
        contact.doSomething();
    }
}

...

/* Contact class */
doSomething()
{
    if (address != null) 
    {
        address.doSomething();
    }
}

...

/* Address class */
doSomething()
{
    if (city != null)
    {
        // do something with city
    }
}

繰り返しますが、それはプログラムの目的に依存します。

1
Thomas

どのような状況で、それらのものはヌルになりますか? nullがコードのバグを示している場合は、コードコントラクトを使用できます。テスト中にnullを取得した場合、それらはそれを取得し、製品バージョンではなくなります。このようなもの:

using System.Diagnostics.Contracts;

[ContractClass(typeof(IContactContract))]
interface IContact
{
    IAddress address { get; set; }
}

[ContractClassFor(typeof(IContact))]
internal abstract class IContactContract: IContact
{
    IAddress address
    {
        get
        {
            Contract.Ensures(Contract.Result<IAddress>() != null);
            return default(IAddress); // dummy return
        }
    }
}

[ContractClass(typeof(IAddressContract))]
interface IAddress
{
    string city { get; set; }
}

[ContractClassFor(typeof(IAddress))]
internal abstract class IAddressContract: IAddress
{
    string city
    {
        get
        {
            Contract.Ensures(Contract.Result<string>() != null);
            return default(string); // dummy return
        }
    }
}

class Person
{
    [ContractInvariantMethod]
    protected void ObjectInvariant()
    {
        Contract.Invariant(contact != null);
    }
    public IContact contact { get; set; }
}

class test
{
    private test()
    {
        var person = new Person();
        Contract.Assert(person != null);
        if (person.contact.address.city != null)
        {
            // If you get here, person cannot be null, person.contact cannot be null
            // person.contact.address cannot be null and person.contact.address.city     cannot be null. 
        }
    }
}

もちろん、可能性のあるヌルが他のどこかから来ている場合は、すでにデータを調整している必要があります。また、nullのいずれかが有効である場合は、null以外を契約の一部にしないでください。nullをテストし、適切に処理する必要があります。

1
digitig

リフレクションを使用すると、すべてのクラスでインターフェイスと余分なコードの実装を強制することを回避できます。静的メソッドを持つ単純なヘルパークラス。これは最も効率的な方法ではないかもしれません、私に優しくしてください、私は処女です(read、noob)..

public class Helper
{
    public static bool IsNull(object o, params string[] prop)
    {
        if (o == null)
            return true;

        var v = o;
        foreach (string s in prop)
        {
            PropertyInfo pi = v.GetType().GetProperty(s); //Set flags if not only public props
            v = (pi != null)? pi.GetValue(v, null) : null;
            if (v == null)
                return true;                                
        }

        return false;
    }
}

    //In use
    isNull = Helper.IsNull(p, "ContactPerson", "TheCity");

オフコースでは、propnamesにタイプミスがある場合、結果は間違っています(ほとんどの場合)。

0
TDull

メソッドのnullチェックを削除する1つの方法は、それらの機能を別の場所にカプセル化することです。これを行う1つの方法は、ゲッターとセッターを使用することです。たとえば、これを行う代わりに:

class Person : IPerson
{
    public IContact contact { get; set; }
}

これを行う:

class Person : IPerson
{
    public IContact contact 
    { 
        get
        {
            // This initializes the property if it is null. 
            // That way, anytime you access the property "contact" in your code, 
            // it will check to see if it is null and initialize if needed.
            if(_contact == null)
            {
                _contact = new Contact();
            }
            return _contact;
        } 
        set
        {
            _contact = value;
        } 
    }
    private IContact _contact;
}

次に、「person.contact」を呼び出すたびに、「get」メソッドのコードが実行されるため、値がnullの場合は値が初期化されます。

すべてのタイプでnullになる可能性があるすべてのプロパティに、これとまったく同じ方法を適用できます。このアプローチの利点は、1)インラインでnullチェックを行う必要がなくなり、2)コードが読みやすくなり、コピーアンドペーストエラーが発生しにくくなることです。

ただし、プロパティの1つis null(つまり、nullの連絡先を持つPersonが実際に何かを意味する場合、何らかのアクションを実行する必要がある状況にいる場合はドメイン?)、このアプローチは助けというよりむしろ障害になります。ただし、問題のプロパティがnever nullである場合、このアプローチはその事実を表す非常にクリーンな方法を提供します。

--jtlovetteiii

0
jtlovetteiii