66

nullをチェックする必要がない場合のガイドラインは何ですか?

私が最近取り組んできた継承されたコードの多くは、nullチェックと悪意を持っています。些細な関数のnullチェック、null以外の戻り値を示すAPI呼び出しのnullチェックなど。場合によっては、nullチェックは妥当ですが、多くの場合、nullは妥当な期待ではありません。

「他のコードを信頼できない」から「常に防御的にプログラムする」、「言語がnull以外の値を保証するまで、常にチェックする」など、さまざまな議論を聞いています。私は確かにこれらの原則の多くにある程度同意しますが、過度のnullチェックは、通常これらの原則に違反する他の問題を引き起こすことがわかりました。粘り強いヌルチェックは本当に価値がありますか?

頻繁に、過剰なnullチェックを含むコードは、実際には高品質ではなく、低品質であることがわかりました。コードの多くはnullチェックに重点を置いているため、開発者は読みやすさ、正確さ、例外処理などの他の重要な品質を見失っています。特に、多くのコードがstd :: bad_alloc例外を無視しているのがわかりますが、。に対してnullチェックを実行しますnew

C ++では、nullポインターを逆参照するという予測できない動作のために、これをある程度理解しています。null逆参照は、Java、C#、Pythonなどでより適切に処理されます。注意深いnullチェックの悪い例を見たことがありますか、それとも本当に何かありますか?

私は主にC++、Java、およびC#に関心がありますが、この質問は言語に依存しないことを目的としています。


過剰に見えるヌルチェックの例には、次のものがあります。


この例では、C ++仕様では、失敗したnewが例外をスローすると述べているため、非標準のコンパイラを考慮しているようです。非準拠のコンパイラを明示的にサポートしていない限り、これは意味がありますか?これは、JavaやC#(またはC ++ / CLR)のような管理された言語では意味がありますか

try {
   MyObject* obj = new MyObject(); 
   if(obj!=NULL) {
      //do something
   } else {
      //??? most code I see has log-it and move on
      //or it repeats what's in the exception handler
   }
} catch(std::bad_alloc) {
   //Do something? normally--this code is wrong as it allocates
   //more memory and will likely fail, such as writing to a log file.
}

もう1つの例は、内部コードで作業する場合です。特に、独自の開発手法を定義できる小さなチームの場合、これは不要のようです。一部のプロジェクトやレガシーコードでは、ドキュメントを信頼することは合理的ではないかもしれません...しかし、あなたやあなたのチームが管理する新しいコードの場合、これは本当に必要ですか?

表示して更新できる(または責任のある開発者に怒鳴ることができる)メソッドにコントラクトがある場合でも、nullをチェックする必要がありますか?

//X is non-negative.
//Returns an object or throws exception.
MyObject* create(int x) {
   if(x<0) throw;
   return new MyObject();
}

try {
   MyObject* x = create(unknownVar);
   if(x!=null) {
      //is this null check really necessary?
   }
} catch {
   //do something
}

プライベート関数またはその他の内部関数を開発する場合、コントラクトがnull以外の値のみを要求する場合、nullを明示的に処理する必要が本当にありますか?なぜアサーションよりもヌルチェックの方が望ましいのでしょうか?

(明らかに、パブリックAPIでは、APIを誤って使用したことでユーザーに怒鳴るのは失礼だと考えられているため、ヌルチェックは不可欠です)

//Internal use only--non-public, not part of public API
//input must be non-null.
//returns non-negative value, or -1 if failed
int ParseType(String input) {
   if(input==null) return -1;
   //do something magic
   return value;
}

に比べ:

//Internal use only--non-public, not part of public API
//input must be non-null.
//returns non-negative value
int ParseType(String input) {
   assert(input!=null : "Input must be non-null.");
   //do something magic
   return value;
}
4

18 に答える 18

36

覚えておくべきことの 1 つは、小規模なチームであり、適切なドキュメントを作成できる場合に、今日作成したコードは、他の誰かが保守しなければならないレガシー コードになるということです。私は次のルールを使用します。

  1. 他の人に公開されるパブリック API を作成している場合は、すべての参照パラメーターに対して null チェックを行います。

  2. アプリケーションに内部コンポーネントを作成している場合、null が存在する場合に何か特別なことを行う必要がある場合、またはそれを明確にしたい場合に、null チェックを作成します。それ以外の場合は、何が起こっているのかがかなり明確であるため、null 参照例外が発生してもかまいません。

  3. 他の人々のフレームワークからの戻りデータを扱う場合、null を返すことが可能で有効な場合にのみ、null をチェックします。彼らの契約がnullを返さないと言っている場合、私はチェックを行いません。

于 2008-11-19T17:53:14.320 に答える
19

最初に、これは契約チェックの特殊なケースであることに注意してください。文書化された契約が満たされていることを実行時に検証する以外に何もしないコードを書いています。失敗とは、どこかのコードに欠陥があることを意味します。

私は、より一般的に役立つ概念の特殊なケースを実装することについて、常に少し懐疑的です。コントラクト チェックは、プログラミング エラーが初めて API の境界を越えたときに検出されるため、便利です。Null の特別な点は、コントラクトの唯一の部分を確認する必要があることを意味するのでしょうか? まだ、

入力検証について:

Java では null は特別です。多くの Java API は、null が特定のメソッド呼び出しに渡すことができる唯一の無効な値であるように記述されています。そのような場合、null チェックは入力を「完全に検証」するため、コントラクト チェックを支持する完全な議論が適用されます。

一方、C++ では、ほとんどすべてのアドレスが正しい型のオブジェクトではないため、NULL は、ポインター パラメーターが取ることができるほぼ 2^32 (新しいアーキテクチャでは 2^64) の無効な値の 1 つにすぎません。そのタイプのすべてのオブジェクトのリストがどこかにない限り、入力を「完全に検証」することはできません。

(foo *)(-1)問題は、NULL は得られない特別な扱いを受けるのに十分一般的な無効な入力であるかということです。

Java とは異なり、フィールドは NULL に自動初期化されないため、初期化されていないガベージ値は NULL と同じように妥当です。ただし、C++ オブジェクトには、明示的に NULL で初期化されたポインター メンバーが含まれている場合があります。これは、「まだ持っていない」ことを意味します。呼び出し元がこれを行う場合、NULL チェックで診断できる重大なクラスのプログラミング エラーがあります。ソースがないライブラリでのページ フォールトよりも、例外の方がデバッグしやすい場合があります。したがって、コードの肥大化を気にしないのであれば、役立つかもしれません。しかし、それはあなた自身ではなく、あなたが考えるべきあなたの呼び出し元です - これは防御的なコーディングではありません。なぜなら、(foo *)(-1) に対してではなく、NULL に対してのみ「防御」するからです。

NULL が有効な入力ではない場合は、ポインターではなく参照によってパラメーターを取得することを検討できますが、多くのコーディング スタイルでは非 const 参照パラメーターが承認されていません。そして、呼び出し元が *fooptr を渡した場合、fooptr は NULL であり、いずれにせよ、何の役にも立ちません。あなたがしようとしているのは、呼び出し元が「うーん、ここで foptr が null かもしれない?」と考える可能性が高くなることを期待して、関数シグネチャにもう少しドキュメントを絞り込むことです。明示的に逆参照する必要がある場合は、ポインターとして渡すだけの場合よりも。それはこれまでのところしかありませんが、それが行く限り、それは役立つかもしれません.

C# はわかりませんが、参照が (少なくとも安全なコードでは) 有効な値を持つことが保証されているという点で Java に似ていることは理解していますが、Java とは異なり、すべての型に NULL 値があるわけではありません。したがって、null チェックに価値があることはめったにないと思います。安全なコードを使用している場合は、null が有効な入力でない限り、null 許容型を使用しないでください。安全でないコードを使用している場合は、次のように同じ理由が適用されます。 C++で。

出力検証について:

同様の問題が発生します。Java では、出力の型を知ることで出力を「完全に検証」でき、値が null でないことを確認できます。C++ では、NULL チェックを使用して出力を「完全に検証」することはできません。関数が、巻き戻されたばかりの独自のスタック上のオブジェクトへのポインターを返したことを知っているからです。ただし、呼び出し先コードの作成者が通常使用する構造のために NULL が一般的な無効な戻り値である場合は、それを確認すると役立ちます。

すべての場合:

可能であれば、「実際のコード」ではなくアサーションを使用してコントラクトをチェックします。アプリが動作したら、すべての呼び出し先がすべての入力をチェックし、すべての呼び出し元が戻り値をチェックするというコードの肥大化は望ましくないでしょう。

非標準の C++ 実装に移植可能なコードを作成する場合、問題のコードの代わりに、null をチェックして例外をキャッチする代わりに、おそらく次のような関数を使用します。

template<typename T>
static inline void nullcheck(T *ptr) { 
    #if PLATFORM_TRAITS_NEW_RETURNS_NULL
        if (ptr == NULL) throw std::bad_alloc();
    #endif
}

次に、新しいシステムに移植するときに行うことのリストの 1 つとして、PLATFORM_TRAITS_NEW_RETURNS_NULL (およびその他の PLATFORM_TRAITS) を正しく定義します。明らかに、あなたが知っているすべてのコンパイラに対してこれを行うヘッダーを書くことができます。誰かがあなたのコードを取得し、あなたが何も知らない非標準の C++ 実装でコンパイルした場合、彼らはこれよりも大きな理由で基本的に独自であるため、自分でそれを行う必要があります。

于 2008-11-21T12:27:30.937 に答える
10

コードとそのコントラクトを作成する場合、コードをコントラクトに従って使用し、コントラクトが正しいことを確認する責任があります。「null 以外を返す」x と言う場合、呼び出し元は null をチェックするべきではありません。その参照/ポインターでヌルポインター例外が発生した場合、それはあなたのコントラクトが間違っています。

Null チェックは、信頼されていないか、適切なコントラクトを持たないライブラリを使用する場合にのみ、極端に行う必要があります。それが開発チームのコードである場合は、契約を破ってはならないことを強調し、バグが発生したときに契約を誤って使用する人を追跡します。

于 2008-11-19T17:48:59.070 に答える
7

これの一部は、コードがどのように使用されるかによって異なります。たとえば、プロジェクト内でのみ使用可能なメソッドか、パブリック API かによって異なります。API エラー チェックには、アサーションよりも強力なものが必要です。

したがって、単体テストなどでサポートされているプロジェクト内ではこれは問題ありません。

internal void DoThis(Something thing)
{
    Debug.Assert(thing != null, "Arg [thing] cannot be null.");
    //...
}

誰がそれを呼び出すかを制御できないメソッドでは、次のようなものが良いかもしれません:

public void DoThis(Something thing)
{
    if (thing == null)
    {
        throw new ArgumentException("Arg [thing] cannot be null.");
    }
    //...
}
于 2008-11-19T17:56:25.810 に答える
7

状況によります。私の答えの残りの部分は、C++ を前提としています。

  • 私が使用するすべての実装は失敗時に bad_alloc をスローするため、new の戻り値をテストすることはありません。作業中のコードで new が null を返すレガシー テストを見つけた場合は、それを切り取り、わざわざ何かに置き換える必要はありません。
  • 心の狭いコーディング標準で禁止されていない限り、文書化された前提条件を主張します。公開された契約に違反する壊れたコードは、即座に劇的に失敗する必要があります。
  • 壊れたコードが原因ではない実行時障害から null が発生した場合は、スローします。fopen の失敗と malloc の失敗 (C++ でそれらを使用することはめったにありませんが) は、このカテゴリに分類されます。
  • 割り当ての失敗から回復しようとはしません。Bad_alloc が main() でキャッチされます。
  • null テストが私のクラスの共同作業者であるオブジェクトに対するものである場合、コードを書き直して参照によって取得します。
  • 共同作業者が実際に存在しない可能性がある場合は、Null オブジェクト デザイン パターンを使用して、明確に定義された方法で失敗するプレースホルダーを作成します。
于 2008-11-19T20:10:08.770 に答える
4

NULL チェックは、コードのテスト可能性に小さな否定的なトークンを追加するため、一般的に悪です。どこでもNULLチェックを使用すると、「nullを渡す」手法を使用できず、単体テスト時にヒットします。null チェックよりも、メソッドの単体テストを行う方が適切です。

http://www.youtube.com/watch?v=wEhu57pih5w&feature=channelで、Misko Hevery によるその問題と一般的な単体テストに関する適切なプレゼンテーションを確認してください。

于 2008-11-19T18:31:50.047 に答える
3

古いバージョンのMicrosoftC++(およびおそらく他のバージョン)は、newを介した割り当ての失敗に対して例外をスローしませんでしたが、NULLを返しました。標準準拠バージョンと古いバージョンの両方で実行する必要があったコードには、最初の例で指摘した冗長なチェックが含まれます。

失敗したすべての割り当てを同じコードパスに従うようにすると、よりクリーンになります。

if(obj==NULL)
    throw std::bad_alloc();
于 2008-11-19T20:23:59.303 に答える
2

あなたの言語に少し依存すると思いますが、私は C# で Resharper を使用しています。 「if (null != oMyThing && ....)」の場合は常に true になります」その後、null のテストを行いません。

于 2008-11-19T21:30:11.590 に答える
2

NULL が表示されたときに何をすべきかがわかっている場合にのみ、NULL をチェックします。ここでの「何をすべきかを知っている」とは、「クラッシュを回避する方法を知っている」または「クラッシュの場所以外にユーザーに何を伝えるべきかを知っている」ことを意味します。たとえば、malloc() が NULL を返す場合、通常はプログラムを中止する以外に選択肢はありません。一方、fopen() が NULL を返した場合は、開くことができず、errno である可能性があるファイル名をユーザーに知らせることができます。そして、find() が end() を返した場合、通常、クラッシュせずに続行する方法を知っています。

于 2008-11-21T19:54:29.177 に答える
2

使用されているコンパイラを指定できる場合、null の「新しい」チェックなどのシステム関数はコードのバグです。これは、エラー処理コードを複製することを意味します。重複したコードは、多くの場合、一方が変更され、もう一方は変更されないため、バグの原因になります。コンパイラまたはコンパイラのバージョンを指定できない場合は、より防御的である必要があります。

内部機能に関しては、コントラクトを指定し、ユニット テストを介してコントラクトが強制されていることを確認する必要があります。しばらく前にコードに問題があり、データベースからオブジェクトが見つからない場合に例外をスローするか、null を返しました。これは API の呼び出し元を混乱させるだけだったので、コード ベース全体で一貫性を持たせ、重複チェックを削除しました。

重要なこと (IMHO) は、1 つのブランチが呼び出されないエラー ロジックが重複しないようにすることです。コードを呼び出すことができない場合、それをテストすることはできません。また、コードが壊れているかどうかもわかりません。

于 2008-11-19T17:55:53.767 に答える
2

手順を重視する人 (正しい方法で物事を行うことに集中する) と結果を重視する人 (正しい答えを得る) が存在することは広く知られています。私たちのほとんどは、真ん中のどこかに横たわっています。手順指向の外れ値を見つけたようです。そういう人は、「物事を完全に理解していなければ、何事も可能だ。だから何事にも備えよ」と言うでしょう。彼らにとって、あなたが見ていることは適切に行われています。あなたがそれを変えると、彼らはアヒルがすべて並んでいないので心配します.

他人のコードに取り組むとき、私は 2 つのことを知っていることを確認しようとします。
1. プログラマーの意図
2. なぜそのようなコードを書いたのか

タイプ A プログラマーのフォローアップには、これが役立つかもしれません。

そのため、「どれだけ十分か」は、技術的な問題であると同時に社会的な問題でもあります。それを測定するための合意された方法はありません。

(それは私も気が狂います。)

于 2008-11-19T17:50:34.370 に答える
2

個人的には、ほとんどの場合、null テストは不要だと思います。new が失敗するか malloc が失敗した場合、より大きな問題が発生し、メモリ チェッカーを作成していない場合、回復の可能性はほとんどありません。また、「null」句は空で何もしないことが多いため、null テストは開発段階で多くのバグを隠します。

于 2008-11-19T18:37:47.223 に答える
2

null をチェックするかどうかは、状況によって大きく異なります。

たとえば、私たちのショップでは、作成したメソッドへのパラメーターをメソッド内の null に対してチェックします。単純な理由は、元のプログラマーとして、メソッドが何をすべきかを正確に把握しているからです。ドキュメントや要件が不完全であったり、満足できるものではなかったりしても、状況を理解しています。後になって保守を任されたプログラマーは、コンテキストを理解できず、null を渡すことは無害であると誤って想定する可能性があります。null が有害であることがわかっていて、誰かが null を渡す可能性があると予想できる場合は、メソッドが適切に反応することを確認する簡単な手順を実行する必要があります。

public MyObject MyMethod(object foo)
{
  if (foo == null)
  {
    throw new ArgumentNullException("foo");
  }

  // do whatever if foo was non-null
}
于 2008-11-20T15:13:52.223 に答える
1

下位レベルのコードは、上位レベルのコードからの使用をチェックする必要があります。通常、これは引数のチェックを意味しますが、upcall からの戻り値のチェックを意味する場合もあります。Upcall 引数をチェックする必要はありません。

その目的は、バグを即座かつ明白な方法で発見することと、嘘をつかないコードでコントラクトを文書化することです。

于 2008-11-19T18:05:45.650 に答える
1

悪いコードではないと思います。かなりの量の Windows/Linux API 呼び出しが、何らかの失敗で NULL を返します。もちろん、API が指定する方法でエラーをチェックします。通常、エラー処理コードを複製する代わりに、なんらかの方法で制御フローをエラー モジュールに渡すことになります。

于 2008-11-19T21:24:04.410 に答える
1

これに関する私の最初の問題は、null チェックなどが散らばっているコードにつながることです。それは可読性を損ないますし、特定の参照が実際には決して null であってはならないコードを書いている場合、null チェックを忘れるのは本当に簡単なので、保守性が損なわれるとさえ言えます。そして、いくつかの場所で null チェックが欠落していることを知っているだけです。これにより、実際にデバッグが必要以上に難しくなります。元の例外がキャッチされず、誤った戻り値に置き換えられていなければ、有益なスタック トレースを含む貴重な例外オブジェクトを取得できたはずです。Null チェックがないと何が得られるでしょうか? あなたを動かすコードの一部の NullReferenceException: wtf? この参照は決して null であってはなりません!

そのため、コードがどのように呼び出されたか、および参照が null になる可能性がある理由を理解し始める必要があります。これには多くの時間がかかる可能性があり、デバッグ作業の効率が大幅に低下します。最終的には本当の問題を突き止めることができますが、それはかなり奥深くに隠されていたため、必要以上に多くの時間を費やして検索した可能性があります。

Null チェックのもう 1 つの問題は、NullReferenceException を受け取ったときに実際の問題について適切に考える時間を割かない開発者もいるということです。実際、NullReferenceException が発生したコードの上に null チェックを追加するだけの開発者をかなり多く見てきました。すばらしい、例外はもう発生しません! 万歳!私たちは今家に帰ることができます!うーん…どうやって「ダメだ、顔面に肘をぶつけられる」って?本当のバグはもはや例外を引き起こさないかもしれませんが、今ではおそらく欠落している、または誤った動作をしている可能性があります...そして例外はありません! これはさらに苦痛であり、デバッグにさらに時間がかかります。

于 2013-09-20T07:04:56.033 に答える
1

言語によってnullではないことが保証されていないポインターを受け取り、nullが私を壊す方法でそれを逆参照するか、NULLを生成しないと言った場所に関数を渡す場合、私はNULL をチェックします。

NULL だけではありません。可能であれば、関数は事前条件と事後条件をチェックする必要があります。

私にポインタを与えた関数のコントラクトが決してヌルを生成しないと言っていても、まったく問題ではありません。私たちは皆、バグを作ります。プログラムは早い段階で頻繁に失敗するという良いルールがあるため、バグを別のモジュールに渡して失敗させるのではなく、その場で失敗します。テスト時のデバッグが非常に簡単になります。また、重要なシステムでは、システムを正常に保つことが容易になります。

また、例外がメインをエスケープした場合、スタックがロールアップされず、デストラクタがまったく実行されないことがあります (terminate() の C++ 標準を参照)。これは深刻かもしれません。したがって、bad_alloc をチェックしないままにしておくと、思ったよりも危険になる可能性があります。

アサートで失敗するか、実行時エラーで失敗するかは、まったく別のトピックです。

標準の new() の動作が、スローの代わりに NULL を返すように変更されていない場合、new() の後に NULL をチェックすることは時代遅れのようです。

別の問題があります。それは、malloc が有効なポインタを返したとしても、まだメモリが割り当てられていて、それを使用できるという意味ではないということです。しかし、それは別の話です。

于 2008-11-20T14:40:35.470 に答える
0

最初は、これは奇妙な質問のように思えました。null小切手は優れた価値のあるツールです。new返品を確認するnullのは間違いなくばかげています。それを可能にする言語があるという事実を無視するつもりです。正当な理由があると確信していますが、その現実での生活を処理できるとは本当に思いません:)冗談はさておき、少なくともメモリが足りないときに がnew返されるように指定する必要があるようですnull.

とにかく、適切な場所をチェックすることnullで、よりクリーンなコードにつながります。関数パラメーターのデフォルト値を決して割り当てないことが、次の論理的なステップであるとまで言えます。さらに進んで、適切な場所で空の配列などを返すと、さらにクリーンなコードになります。null論理的に意味のある場合を除いて、 s の取得について心配する必要がないのは素晴らしいことです。エラー値としての Null は避けたほうがよいでしょう。

assert を使用することは、非常に優れたアイデアです。特に、実行時にそれらをオフにするオプションがある場合。さらに、それはより明示的な契約スタイルです:)

于 2011-03-25T01:48:05.170 に答える