web-dev-qa-db-ja.com

関数が意図した動作を実行する前にnullチェックを実行する必要がある場合、これは悪い設計ですか?

だから、これが良いコード設計か悪いコード設計かわからないので、もっと質問したほうがいいと思いました。

クラスを含むデータ処理を行うメソッドを頻繁に作成し、メソッドの多くのチェックを行って、事前にnull参照やその他のエラーが発生しないことを確認しています。

非常に基本的な例として:

// fields and properties
private Entity _someEntity;
public Entity SomeEntity => _someEntity;

public void AssignEntity(Entity entity){
    _someEntity = entity;
}
public void SetName(string name)
{
    if (_someEntity == null) return; //check to avoid null ref
    _someEntity.Name = name;
    label.SetText(_someEntity.Name);
}

あなたが見ることができるように毎回nullをチェックしています。しかし、メソッドにはこのチェックがないはずですか?

たとえば、外部コードが事前にデータをクレンジングして、メソッドが以下のように検証する必要がないようにする必要があります。

 if(entity != null) // this makes the null checks redundant in the methods
 {
     Manager.AssignEntity(entity);
     Manager.SetName("Test");
 }

要約すると、メソッドは「データ検証」であり、データに対して処理を行うか、またはメソッドを呼び出す前に保証する必要があります。メソッドを呼び出す前に検証に失敗すると、エラーがスローされます(またはエラー)?

66
WDUK

基本的な例の問題は、nullチェックではないサイレントフェイルですです。

多くの場合、nullポインター/参照エラーはプログラマーエラーです。プログラマーのエラーは、多くの場合、すぐに大声で失敗することによって最もよく対処されます。この状況で問題に対処するには、3つの一般的な方法があります。

  1. チェックを気にせず、ランタイムにnullpointer例外をスローさせるだけです。
  2. チェックし、基本的なNPEメッセージよりも優れたメッセージで例外をスローします。

3番目の解決策は、より多くの作業ですが、はるかに堅牢です。

  1. _someEntityが無効な状態になることは事実上不可能になるようにクラスを構造化します。これを行う1つの方法は、AssignEntityを削除して、インスタンス化のパラメーターとして要求することです。他の依存性注入技術も同様に役立ちます。

状況によっては、すべての関数の引数の有効性をチェックしてから作業を開始するのが適切な場合もあれば、入力をチェックするのではなく、入力が有効であることを確認する責任があることを呼び出し元に伝えるのが適切な場合もあります。スペクトルのどちらの側にいるかは、問題のドメインによって異なります。 3番目のオプションには、ある程度、呼び出し側がすべてを適切に実行するようコンパイラーに強制させることができるという大きな利点があります。

3番目のオプションがオプションではない場合、私の意見では、黙って失敗しない限り、それは本当に問題ではありません。 nullをチェックしないと、プログラムは即座に爆発しますが、代わりに静かにデータを破壊して問題を引き起こす場合は、その場でチェックして対処するのが最善です。

168
whatsisname

_someEntityはどの段階でも変更できるため、SetNameが呼び出されるたびにテストすることは理にかなっています。結局のところ、そのメソッドが最後に呼び出されてから変更されている可能性があります。ただし、SetNameのコードはスレッドセーフではないため、1つのスレッドでそのチェックを実行し、_someEntityを別のスレッドでnullに設定してから、コードを実行できます。とにかくNullReferenceExceptionをバーフにします。

したがって、この問題に対する1つのアプローチは、さらに防御力を高め、次のいずれかを実行することです。

  1. _someEntityのローカルコピーを作成し、メソッドを介してそのコピーを参照します。
  2. _someEntityの周りにロックを使用して、メソッドの実行時に変更されないようにします。
  3. try/catchを使用し、メソッド内で発生するすべてのNullReferenceExceptionに対して、最初のnullチェックで実行するのと同じアクション(この場合はnopアクション)を実行します。

しかし、あなたが本当にすべきことは、立ち止まって自分自身に質問することです。実際に_someEntityの上書きを許可する必要がありますか?コンストラクタで一度設定してみませんか。そのようにすれば、一度だけnullチェックを行う必要があるだけです。

private readonly Entity _someEntity;

public Constructor(Entity someEntity)
{
    if (someEntity == null) throw new ArgumentNullException(nameof(someEntity));
    _someEntity = someEntity;
}

public void SetName(string name)
{
    _someEntity.Name = name;
    label.SetText(_someEntity.Name);
}

可能であれば、これは私がそのような状況でとることをお勧めするアプローチです。可能なnullの処理方法を選択するときにスレッドの安全性を気にすることができない場合。

おまけのコメントとして、私はあなたのコードが奇妙であり、改善できると思います。すべての:

private Entity _someEntity;
public Entity SomeEntity => _someEntity;

public void AssignEntity(Entity entity){
    _someEntity = entity;
}

と置き換えることができます:

public Entity SomeEntity { get; set; }

これは同じ機能を持ち、別の名前のメソッドではなく、プロパティセッターを介してフィールドを設定できるようになっています。

55
David Arno

しかし、メソッドにはこのチェックがないはずですか?

これはyourの選択です。

publicメソッドを作成することにより、publicを呼び出す機会を提供します。これは常に、そのメソッドを呼び出すためのhowの暗黙のコントラクトと、それを行うときに何を期待するかを伴います。この契約には「」が含まれる場合と含まれない場合があります。ええ、ええと、パラメータ値としてnullを渡すと、顔の中で爆発します。 "。そのコントラクトの他の部分は「oh、BTW:2つのスレッドが同時にこのメソッドを実行するたびに、子猫が死ぬ」かもしれません。

どのタイプの契約を設計するかはあなた次第です。重要なことは

  • 便利で一貫したAPIを作成し、

  • 特にEdgeのケースでは、それを適切に文書化します。

メソッドが呼び出される可能性のあるケースは何ですか?

23
null

他の答えは良いです。アクセシビリティ修飾子についていくつかコメントすることで、それらを拡張したいと思います。まず、nullが有効でない場合の対処方法:

  • publicおよびprotectedメソッドは、呼び出し元を制御しないメソッドです。 nullが渡されると、throwする必要があります。そうすることで、プログラムがクラッシュしないようにしたいので、呼び出し元にnullを渡さないようにトレーニングします。

  • internalおよびprivateメソッドは、呼び出し元をdoで制御するメソッドです。 nullを渡すと、assertになります。それらはおそらく後でクラッシュしますが、少なくとも最初にアサーションとデバッガに侵入する機会を得ました。繰り返しになりますが、発信者を傷つけないようにして傷つけないようにして、正しく発信者を訓練する必要があります。

では、nullが渡されるのに有効mightである場合はどうしますか?この状況はnullオブジェクトパターンで回避します。つまり、タイプの特別なインスタンスを作成し、無効なオブジェクトが必要なときにいつでも使用できるようにします。これで、ヌルを使用するたびにスロー/アサートする快適な世界に戻りました。

たとえば、次のような状況になりたくありません。

class Person { ... }
...
class ExpenseReport { 
  public void SubmitReport(Person approver) { 
    if (approver == null) ... do something ...

そして呼び出しサイトで:

// I guess this works but I don't know what it means
expenses.SubmitReport(null); 

(1)nullが適切な値であることを呼び出し元にトレーニングしているため、(2)その値のセマンティクスが明確でないため。むしろ、これを行います:

class ExpenseReport { 
  public static readonly Person ApproverNotRequired = new Person();
  public static readonly Person ApproverUnknown = new Person();
  ...
  public void SubmitReport(Person approver) { 
    if (approver == null) throw ...
    if (approver == ApproverNotRequired) ... do something ...
    if (approver == ApproverUnknown) ... do something ...

そして今、呼び出しサイトは次のようになります。

expenses.SubmitReport(ExpenseReport.ApproverNotRequired);

そして今、コードの読者はそれが何を意味するかを知っています。

14
Eric Lippert

他の回答は、コードをクリーンアップしてnullチェックが不要になることを示していますが、次のサンプルコードを検討すると、有用なnullチェックの一般的な回答が得られます。

public static class Foo {
    public static void Frob(Bar a, Bar b) {
        if (a == null) throw new ArgumentNullException(nameof(a));
        if (b == null) throw new ArgumentNullException(nameof(b));

        a.Something = b.Something;
    }
}

これにより、メンテナンスが容易になります。何かがnullオブジェクトをメソッドに渡すというコードの欠陥が発生した場合、どのパラメーターがnullであったかについて、メソッドから有用な情報を取得します。チェックを行わないと、次の行でNullReferenceExceptionが発生します。

a.Something = b.Something;

そして(Somethingプロパティが値型である場合)、aまたはbのいずれかがnullになる可能性があり、スタックトレースからどれを知ることができなくなります。チェックを使用すると、どのオブジェクトがnullであったかを正確に知ることができます。これは、欠陥を選択解除するときに非常に役立ちます。

元のコードのパターンは次のとおりです。

if (x == null) return;

特定の状況で使用することができます。また、コードベースの根本的な問題をマスクする非常に良い方法を(おそらくより頻繁に)提供することもできます。

8
Paddy

いいえ、まったく違いますが、状況によって異なります。

対応したい場合は、null参照チェックのみを行います。

null参照チェックを行うおよびチェックされた例外をスローするの場合、メソッドのユーザーに強制的に反応させ、回復を許可します。プログラムは引き続き期待どおりに動作します。

null参照チェックを行わない(またはチェックされていない例外をスローする)の場合、チェックされていないNullReferenceExceptionがスローされることがあります。これは通常、メソッドのユーザーによって処理されず、アプリケーションをシャットダウンする可能性があります。これは、関数が明らかに完全に誤解されているため、アプリケーションに欠陥があることを意味します。

4
Herr Derb

@nullの答えの拡張の一部ですが、私の経験では、何があってもnullチェックを実行します。

優れた防御的プログラミングとは、残りのプログラム全体がクラッシュすることを想定しているという前提で、現在のルーチンを分離してコーディングする態度です。失敗が許されないミッションクリティカルなコードを書いている場合、これがほとんど唯一の方法です。

ここで、実際にnull値を処理する方法を説明します。これは、ユースケースに大きく依存します。

nullが許容値である場合。 MSVCルーチン_splitpath()への2番目から5番目のパラメーターのいずれか、そしてそれを黙って処理することが正しい動作です。

問題のルーチンを呼び出すときにnullが発生してはならない場合、つまりOPの場合、APIコントラクトはAssignEntity()が有効なEntityで呼び出されていることを要求し、次に例外、またはそれ以外の場合は失敗し、プロセスで多くのノイズを作ります。

Nullチェックのコストについて心配する必要はありません。それらが発生した場合、それらはまったく安価です。最新のコンパイラーでは、静的コード分析は、コンパイラーが発生しないと判断した場合、完全にそれらを排除できます。

4
dgnuff