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


67

だから、これが良いコード設計なのか悪いコード設計なのかはわからないので、私は尋ねた方が良いと思った。

クラスを含むデータ処理を行うメソッドを頻繁に作成し、メソッドで多くのチェックを行って、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をチェックするimを見ることができます。しかし、メソッドはこのチェックを行うべきではありませんか?

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

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

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


7
誰かがnullチェックを実行できず、SetNameが例外をスローするとどうなりますか?
ロバートハーベイ

5
someEntity を実際にNull にしたい場合はどうなりますか?必要なものを決定します。someEntityはNullでもNullでもないので、それに応じてコードを記述します。決してヌルにしたくない場合は、チェックをアサートに置き換えることができます。
gnasher729

5
あなたは見ることができゲイリーベルナールの境界が(すなわち、ヌル、などのチェック)の外殻であり、その原料を気にする必要はありません内部コアシステムを有する消毒データとの間の明確な区分を行うことについて話す-とだけはやるその作業。
mgarciaisaia

1
C#8を使用すると、適切な設定が有効になっている場合にnull参照例外を心配する必要がなくなる可能性があります。blogs.msdn.microsoft.com
dotnet

3
これが、C#言語チームがnull許容の参照型(およびnull許容でない参照型)を導入する理由の1つです-> github.com/dotnet/csharplang/issues/36
Mafii

回答:


168

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

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

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

3番目のソリューションはより多くの作業を行いますが、はるかに堅牢です。

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

状況によっては、実行している作業の前にすべての関数の引数の有効性をチェックすることが理にかなっている場合もあります。スペクトルのどちらの端にいるかは、問題のドメインに依存します。3番目のオプションには、呼び出し元がすべてを適切に実行することをコンパイラーに強制させることができるという点で、大きな利点があります。

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


10
@aroth:ソフトウェアを含むあらゆるものの設計には、制約が伴います。設計のまさにその行為は、それらの制約がどこに行くかを選択することであり、それは私の責任でもなければ、入力を受け入れてそれで何か役に立つことを期待されることでもありません。null架空のライブラリでデータ型を定義し、有効なものとそうでないものを定義したため、間違っているか無効かを知っています。あなたはそれを「強制的な仮定」と呼びますが、何を推測しますか:それが存在するすべてのソフトウェアライブラリがすることです。nullが有効な値である場合がありますが、それは「常に」ではありません。
-whatsisname

4
コードnull適切に処理されないために壊れている」-これは、適切な処理の意味の誤解です。ほとんどのJavaプログラマのために、それがために、例外を投げる意味任意の無効な入力。を使用@ParametersAreNonnullByDefaultするとnull、引数がとしてマークされていない限り、すべてに対しても意味し@Nullableます。他のことを行うということは、最終的に他の場所で爆発するまでパスを長くすることを意味し、それによってデバッグに費やされる時間も長くなります。まあ、問題を無視することで、それが決して吹かないことを達成するかもしれませんが、間違った振る舞いはかなり保証されます。
maaartinus

4
@aroth親愛なる主よ、私はあなたが書いたコードで作業することを嫌います。爆発は、無効な入力に対する他の唯一のオプションである無意味なことを行うよりも無限に優れています。メソッドが私が意図したものを推測できない場合、例外は、呼び出し元である私に、そのような場合に行うべき正しいことを決定させます。チェックされた例外と不必要に拡張することExceptionは、まったく無関係の議論です。(両方とも面倒です。)この特定の例でnullは、クラスの目的がオブジェクトのメソッドを呼び出すことである場合、ここでは明らかに意味がありません。
jpmc26

3
「あなたのコードは、呼び出し元の世界に仮定を強制するべきではありません」-呼び出し元に仮定を強制しないことは不可能です。そのため、これらの仮定をできるだけ明確にする必要があります。
ディオゴコルロス

3
@aroth あなたのそのパッシングため、コードが壊れている、私のそれはプロパティとして名前を持っている何かを渡すべきであるとき、コードはnullを。爆発以外の動作は、動的なタイピングのある種のバックドアの奇妙なバージョンです。
mbrig

56

_someEntityどの段階でも変更できるため、SetName呼び出されるたびにテストするのが理にかなっています。結局、メソッドが最後に呼び出されてから変更されている可能性があります。ただし、コードSetNameはスレッドセーフではないため、あるスレッドでそのチェックを実行_someEntitynull、別のスレッドで設定すると、コードは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);
}

可能な場合、これはこのような状況での使用をお勧めする方法です。可能性のあるヌルを処理する方法を選択するときにスレッドセーフを意識できない場合。

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

private Entity _someEntity;
public Entity SomeEntity => _someEntity;

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

次のものに置き換えることができます。

public Entity SomeEntity { get; set; }

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


5
なぜこれが質問に答えるのですか?質問はnullチェックに対応しており、これはほとんどコードレビューです。
IEatBagels

17
@TopinFrassiは、現在のnullチェックアプローチはスレッドセーフではないことを説明しています。次に、それを回避するために他の防御的なプログラミング手法に触れます。次に、不変性を使用して、そもそもそのような防御的なプログラミングを行う必要性を回避するという提案で締めくくります。また、追加のボーナスとして、コードをより適切に構造化する方法に関するアドバイスも提供します。
デビッドアルノ

23

しかし、メソッドはこのチェックを行うべきではありませんか?

これはあなたの選択です。

publicメソッドを作成することで、一般の人々にメソッドを呼び出す機会を提供します。これは、常に上の暗黙の契約と一緒に来てどのようにその方法とどのようなそう際に期待するを呼び出すこと。この契約には、「はい、そうだnull、パラメータ値として渡すと、すぐに爆発する」が含まれる場合があります(含まれない場合があります)。その契約の他の部分は、「ああ、ところで:2つのスレッドがこのメソッドを同時に実行するたびに、子猫が死ぬ」かもしれません。

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

  • 便利で一貫性のあるAPIを作成し、

  • 特にこれらのエッジケースの場合、適切に文書化します。

メソッドがどのように呼び出される可能性が高いケースは何ですか?


同時アクセスがある場合、ルールではなくスレッドセーフが例外です。したがって、非表示/グローバル状態の使用は文書化されており、ケースの同時実行はとにかく安全です。
デュプリケータ

14

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

  • publicメソッドとprotectedメソッドは、呼び出し元を制御しないものです。nullが渡されたときにスローする必要があります。そうすれば、プログラムがクラッシュしないようにしたいので、決してあなたにヌルを渡さないように呼び出し元を訓練します。

  • 内部および民間の方法は、あなたがあるものである、呼び出し元を制御します。nullが渡されたときにアサートする必要があります。その後、おそらくクラッシュしますが、少なくとも最初にアサーションを取得し、デバッガーに侵入する機会を得ました。繰り返しになりますが、発信者を傷つけないようにすることで、発信者を正しく呼び出すようにトレーニングする必要があります。

それがあれば今、あなたは何をしますか可能性がある中で渡されるヌルのために有効であること?nullオブジェクトパターンを使用すると、この状況を回避できます。つまり、型の特別なインスタンスを作成し、無効なオブジェクトが必要なときにいつでも使用すること要求します。これで、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);

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


さらに良いifのは、nullオブジェクトのsのチェーンを持たず、ポリモーフィズムを使用してそれらを正しく動作させることです。
コンラッドルドルフ

@KonradRudolph:確かに、それはしばしば良いテクニックです。私はそれをデータ構造に使用するのが好きです。データ構造でEmptyQueueは、デキュー時にスローする型を作成します。
エリックリッパー

@EricLippert-私はまだここで学んでいるので許してください。しかし、Personクラスが不変であり、引数ゼロのコンストラクタが含まれていない場合、あなたのインスタンスをどのように構築します ApproverNotRequiredApproverUnknown?つまり、コンストラクタに渡す値は何ですか?

2
SE全体であなたの回答/コメントにぶつかるのが大好きです。彼らは常に洞察力に富んでいます。内部/プライベートメソッドのアサートを確認するとすぐに、エリック
リッパートの

4
皆さんは、私が与えた特定の例を過度に考えています。ヌルオブジェクトパターンの概念をすばやく理解することを目的としていました。これは、ペーパーアンドペイチェック自動化システムの優れたデザインの例であることを意図していませんでした。実際のシステムでは、「承認者」のタイプを「人」にすることは、ビジネスプロセスと一致しないため、おそらく悪い考えです。承認者がいない場合もあれば、複数必要な場合もあります。そのドメインで質問に答えるときによく注意するように、本当に欲しいのはポリシーオブジェクトです。例にこだわらないでください。
エリックリッパー

8

他の答えは、あなたのコードがある場所で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になる可能性があり、スタックトレースからどれを知ることができなくなります。チェックにより、どのオブジェクトがヌルであったかを正確に知ることができます。これは、欠陥の選択を解除するときに非常に役立ちます。

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

if (x == null) return;

特定の状況で使用できますが、コードベースの根本的な問題を隠すための非常に優れた方法を提供することもできます(より頻繁に)。


4

いいえ、まったく違いますが、それは異なります。

それに反応したい場合にのみ、null参照チェックを行います。

あなたがいる場合はnull参照のチェックを行うと、チェック例外をスローし、あなたはそれに反応し、彼が回復することを可能にする方法をユーザーに強制します。プログラムは期待どおりに動作します。

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


4

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

優れた防御的プログラミングとは、プログラム全体がクラッシュしようとしているという前提で、現在のルーチンを単独でコーディングするという姿勢です。失敗が選択肢にならないミッションクリティカルなコードを書いている場合、これがほとんど唯一の方法です。

さて、実際にnull値をどのように扱うかは、ユースケースに大きく依存します。

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

null問題のルーチンを呼び出したときに起こるべきではない場合、つまり、OPの場合、APIコントラクトがAssignEntity()有効な状態で呼び出される必要がある場合、Entity例外をスローするか、そうでなければプロセスで多くのノイズを発生させます。

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


または、それを完全に避けてください(48分29秒:「プログラムの近くでヌルポインターを使用したり、許可したりしないでください」)。
ピーターモーテンセン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.