Nullをチェックするためにnot必要な場合のガイドラインは何ですか?
私が最近取り組んできた継承されたコードの多くは、ヌルチェックと悪意を持っています。些細な関数のnullチェック、null以外の戻り値を示すAPI呼び出しのnullチェックなど。場合によっては、nullチェックは妥当ですが、多くの場合、nullは妥当な期待値ではありません。
「他のコードを信頼できない」から「常に防御的にプログラムする」、「言語がnull以外の値を保証するまで、私は常にチェックする」など、さまざまな議論を聞いています。私は確かにこれらの原則の多くにある程度まで同意しますが、過度のnullチェックは、通常これらの原則に違反する他の問題を引き起こすことがわかりました。粘り強いヌルチェックは本当に価値がありますか?
頻繁に、過剰なnullチェックを含むコードは、実際には高品質ではなく低品質であることがわかりました。コードの多くはnullチェックに重点を置いているため、開発者は読みやすさ、正確さ、例外処理などの他の重要な品質を見失っています。特に、多くのコードがstd :: bad_alloc例外を無視しているのがわかりますが、new
に対してnullチェックを実行します。
C++では、nullポインターを逆参照するという予測できない動作のため、これをある程度理解しています。 null逆参照は、Java、C#、Pythonなどでより適切に処理されます。注意深いnullチェックの悪い例を見たことがありますか、それとも本当に何かありますか?
私は主にC++、Java、およびC#に関心がありますが、この質問は言語に依存しないことを目的としています。
過剰のように見えるヌルチェックの例には、次のものがあります。
この例は、C++仕様で、失敗したnewが例外をスローすると述べているため、非標準コンパイラを考慮しているようです。非準拠のコンパイラを明示的にサポートしていない限り、これは意味がありますか?これは、JavaまたはC#(またはC++/CLR)のような管理言語でanyを意味しますか?
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;
}
最初に、これはコントラクトチェックの特殊なケースであることに注意してください。文書化されたコントラクトが満たされていることを実行時に検証する以外に何もしないコードを記述しています。失敗とは、どこかのコードに欠陥があることを意味します。
私は、より一般的に有用な概念の特別なケースを実装することについて、常に少し疑わしいです。コントラクトチェックは、APIの境界を初めて超えたときにプログラミングエラーをキャッチするので便利です。 nullの何が特別なので、チェックしたいのはnullだけです。それでも、
入力検証の主題について:
nullはJavaでは特別です:多くのJava APIは、nullが特定のメソッド呼び出しに渡すことさえ可能な唯一の無効な値になるように記述されています。そのような場合、nullは完全にチェックします入力を検証する」ので、コントラクトチェックを支持する完全な引数が適用されます。
一方、C++では、ほとんどすべてのアドレスが正しいタイプのオブジェクトではないため、NULLはポインタパラメータが取る可能性のあるほぼ2 ^ 32(新しいアーキテクチャでは2 ^ 64)の無効な値の1つにすぎません。そのタイプのすべてのオブジェクトのリストがどこかにない限り、入力を「完全に検証」することはできません。
問題は、NULLは、(foo *)(-1)
が取得できない特別な処理を取得するのに十分に一般的な無効な入力ですか?
Javaとは異なり、フィールドはNULLに自動初期化されないため、初期化されていないガベージ値はNULLと同じくらい妥当です。ただし、C++オブジェクトには、明示的にNULLで開始されるポインタメンバーが含まれている場合があります。これは、「まだ持っていない」という意味です。呼び出し元がこれを行う場合、NULLチェックで診断できる重要なクラスのプログラミングエラーがあります。例外は、ソースがないライブラリのページフォールトよりもデバッグが簡単な場合があります。したがって、コードの膨張を気にしないのであれば、それは役に立つかもしれません。しかし、それはあなたが考えるべきあなたの呼び出し元であり、あなた自身ではありません-これは防御的なコーディングではありません。なぜなら、それは(foo *)(-1)に対してではなく、NULLに対してのみ「防御する」からです。
NULLが有効な入力でない場合は、ポインターではなく参照によってパラメーターを取得することを検討できますが、多くのコーディングスタイルでは、非const参照パラメーターが承認されていません。そして、呼び出し元が* fooptrを渡した場合(fooptrはNULL)、とにかく誰も何の役にも立ちません。あなたがやろうとしているのは、呼び出し元が「うーん、ここではfooptrがnullかもしれない」と思う可能性が高くなることを期待して、関数シグネチャにもう少しドキュメントを詰め込むことです。ポインタとして渡すだけの場合よりも、明示的に逆参照する必要がある場合。これまでのところだけですが、それが役立つかもしれません。
C#はわかりませんが、参照が(少なくとも安全なコードでは)有効な値を持つことが保証されているという点でJavaのようですが、Javaすべての型にNULL値があるわけではないので、nullチェックが価値があることはめったにないと思います。安全なコードを使用している場合は、nullが有効な入力でない限り、null許容型を使用しないでください。安全でないコードを使用している場合は、C++と同じ理由が当てはまります。
出力検証の主題について:
同様の問題が発生します:in 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++実装でそれをコンパイルする場合、これよりも大きな理由で基本的に彼ら自身であるので、彼らはそれを自分でしなければなりません。
今日書いたコードは、小さなチームであり、優れたドキュメントを作成できるものの、他の誰かが維持しなければならないレガシーコードに変わることを覚えておいてください。私は次のルールを使用します。
他の人に公開されるパブリックAPIを作成している場合は、すべての参照パラメーターに対してnullチェックを実行します。
アプリケーションに内部コンポーネントを作成している場合、nullが存在するときに特別なことを行う必要があるとき、またはそれを非常に明確にしたいときに、nullチェックを作成します。それ以外の場合は、null参照例外が発生してもかまいません。これは、何が起こっているのかもかなり明確だからです。
他の人々のフレームワークからの戻りデータを処理するとき、nullを返すことが可能で有効な場合にのみ、nullをチェックします。彼らの契約がnullを返さないと言っている場合、私はチェックを行いません。
コードとそのコントラクトを作成する場合は、そのコントラクトの観点からコードを使用し、コントラクトが正しいことを確認する責任があります。 「null以外を返す」xと言った場合、呼び出し元はnullをチェックしないでください。その後、その参照/ポインターでnullポインター例外が発生した場合、正しくないのはコントラクトです。
ヌルチェックは、信頼されていないライブラリ、または適切なコントラクトがないライブラリを使用する場合にのみ極端になります。開発チームのコードである場合は、契約が破られてはならないことを強調し、バグが発生したときに契約を誤って使用した人を追跡します。
状況によります。私の答えの残りはC++を前提としています。
これの一部は、コードがどのように使用されるかによって異なります。たとえば、プロジェクト内でのみ使用可能なメソッドであるか、パブリック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.");
}
//...
}
NULLチェックは、コードのテスト容易性に小さな負のトークンを追加するため、一般的には悪です。どこでもNULLチェックを使用すると、「nullを渡す」手法を使用できず、単体テスト時にヒットします。 nullチェックよりも、メソッドの単体テストを行う方が適切です。
Misko Heveryによるその問題とユニットテスト全般に関する適切なプレゼンテーションを http://www.youtube.com/watch?v=wEhu57pih5w&feature=channel でチェックしてください。
古いバージョンのMicrosoftC++(およびおそらく他のバージョン)は、newを介した割り当ての失敗に対して例外をスローしませんでしたが、NULLを返しました。標準準拠バージョンと古いバージョンの両方で実行する必要があったコードには、最初の例で指摘した冗長なチェックが含まれます。
失敗したすべての割り当てを同じコードパスに従わせる方がクリーンです。
if(obj==NULL)
throw std::bad_alloc();
個人的には、ほとんどの場合、ヌルテストは不要だと思います。 newが失敗したり、mallocが失敗したりすると、より大きな問題が発生し、メモリチェッカーを作成していない場合、回復の可能性はほぼゼロになります。また、nullテストでは、「null」句が空で何もしないことが多いため、開発フェーズでバグが多く隠されます。
言語にも少し依存すると思いますが、C#でResharperを使用しているので、基本的に「この参照はnullになる可能性があります」と表示されます。その場合は、「これ」と表示されたらチェックを追加します。 「if(null!= oMyThing && ....)」の場合は常にtrueになります。それを聞いて、nullをテストしません。
手順志向の人(正しい方法で物事を行うことに焦点を当てる)と結果志向の人(正しい答えを得る)がいることは広く知られています。私たちのほとんどは真ん中のどこかにあります。プロシージャ指向の外れ値を見つけたようです。これらの人々は、「あなたが物事を完全に理解しない限り、何でも可能です。だから、何かに備えてください」と言うでしょう。彼らにとって、あなたが見ることは適切に行われています。アヒルが一列に並んでいないので、変えても心配です。
他の人のコードで作業するとき、私は2つのことを知っていることを確認しようとします。
1。プログラマーが意図したこと
2。彼らがコードを書いた理由
タイプAプログラマーのフォローアップには、おそらくこれが役立ちます。
したがって、「いくらで十分か」は、技術的な質問と同じくらい社会的な質問になります。それを測定するための合意された方法はありません。
(それは私も気が狂います。)
Nullをチェックするかどうかは、状況によって大きく異なります。
たとえば、当店では、メソッド内でnull用に作成したメソッドのパラメーターをチェックします。単純な理由は、元のプログラマーとして、メソッドが何をすべきかを正確に理解しているからです。ドキュメントと要件が不完全または満足のいくものではない場合でも、コンテキストを理解しています。メンテナンスを担当する後のプログラマーは、コンテキストを理解できず、誤ってnullを渡すことは無害であると想定する可能性があります。 nullが有害であることがわかっていて、誰かがnullを渡す可能性があると予想できる場合は、メソッドが適切に反応することを確認する簡単な手順を実行する必要があります。
public MyObject MyMethod(object foo)
{
if (foo == null)
{
throw new ArgumentNullException("foo");
}
// do whatever if foo was non-null
}
使用しているコンパイラを指定できる場合、「新規」などのシステム関数では、nullのチェックはコードのバグです。これは、エラー処理コードを複製することを意味します。多くの場合、一方が変更され、もう一方が変更されないため、重複コードはバグの原因となることがよくあります。コンパイラーまたはコンパイラーのバージョンを指定できない場合は、より防御的にする必要があります。
内部機能については、契約を指定し、単体テストで契約が実施されていることを確認する必要があります。しばらく前にコードで問題が発生し、データベースからオブジェクトが欠落している場合に例外をスローするか、nullを返しました。これにより、APIの呼び出し元が混乱するため、コードベース全体で一貫性を保ち、重複するチェックを削除しました。
重要なこと(IMHO)は、1つのブランチが呼び出されない重複エラーロジックを持たないことです。コードを呼び出すことができない場合、コードをテストすることはできず、コードが壊れているかどうかを知ることはできません。
NULLが表示されたときに何をすべきかがわかっている場合にのみ、NULLをチェックします。ここでの「何をすべきかを知っている」とは、「クラッシュを回避する方法を知っている」または「クラッシュの場所以外にユーザーに何を伝えるべきかを知っている」ことを意味します。たとえば、malloc()がNULLを返す場合、通常、プログラムを中止する以外に選択肢はありません。一方、fopen()がNULLを返す場合は、開くことができず、errnoである可能性があるファイル名をユーザーに知らせることができます。そして、find()がend()を返す場合、私は通常、クラッシュせずに続行する方法を知っています。
悪いコードではないと思います。かなりの量のWindows/Linux API呼び出しは、ある種の失敗時にNULLを返します。したがって、もちろん、APIが指定する方法で失敗をチェックします。通常、エラー処理コードを複製する代わりに、何らかの方法で制御フローをエラーモジュールに渡すことになります。
これに関する私の最初の問題は、nullチェックなどが散らばっているコードにつながることです。読みやすさが損なわれ、保守性も損なわれると言っても過言ではありません。特定の参照が実際にはnullであってはならないコードを記述している場合、nullチェックを忘れがちだからです。そして、あなたはヌルチェックがいくつかの場所で欠落していることを知っています。これにより、実際にはデバッグが必要以上に難しくなります。元の例外がキャッチされず、誤った戻り値に置き換えられなかった場合、有益なスタックトレースを備えた貴重な例外オブジェクトを取得できたはずです。欠落しているヌルチェックは何をもたらしますか?あなたを行かせるコードの一部のNullReferenceException:wtf?この参照は決してnullであってはなりません!
したがって、コードがどのように呼び出されたか、および参照がnullになる可能性がある理由を理解する必要があります。これには多くの時間がかかる可能性があり、デバッグ作業の効率が大幅に低下します。最終的には本当の問題を理解するでしょうが、それはかなり深く隠されており、本来あるべきよりも多くの時間を検索に費やした可能性があります。
いたるところにあるnullチェックのもう1つの問題は、NullReferenceExceptionが発生したときに、実際の問題について適切に考えるのに時間がかからない開発者がいることです。実際、かなりの数の開発者が、NullReferenceExceptionが発生したコードの上にnullチェックを追加するのを見てきました。すばらしい、例外はもう発生しません!やあ!家に帰れます!うーん…「いや、顔にひじをつけるに値する」という試合はどうですか?本当のバグはもう例外を引き起こさないかもしれません、しかし今あなたはおそらく行方不明または欠陥のある振る舞いを持っています…そして例外はありません!これはさらに苦痛であり、デバッグにさらに時間がかかります。
言語によってnullでないことが保証されていないポインターを受け取り、nullが私を壊すような方法でそれを逆参照しようとしている場合、またはNULLを生成しないと言った場所に関数を渡した場合、私はNULLを確認してください。
これはNULLだけではなく、関数は可能であれば事前条件と事後条件をチェックする必要があります。
ポインタを与えてくれた関数のコントラクトが、nullを生成しないと言っているかどうかはまったく問題ではありません。私たちは皆バグを作ります。プログラムは早期に頻繁に失敗するという良いルールがあるので、バグを別のモジュールに渡して失敗させる代わりに、私はその場で失敗します。テスト時のデバッグが非常に簡単になります。また、重要なシステムでは、システムを正常に保つことが容易になります。
また、例外がmainをエスケープすると、スタックがロールアップされず、デストラクタがまったく実行されなくなる可能性があります(terminate()のC++標準を参照)。これは深刻かもしれません。したがって、bad_allocをオフのままにしておくと、見た目よりも危険な場合があります。
アサートで失敗するか、実行時エラーで失敗するかは、まったく別のトピックです。
標準のnew()の動作が、スローする代わりにNULLを返すように変更されていない場合、new()の後にNULLをチェックすることは、時代遅れのようです。
もう1つの問題は、mallocが有効なポインターを返したとしても、メモリーが割り当てられて使用できることを意味するわけではないということです。しかし、それは別の話です。
下位レベルのコードは、上位レベルのコードからの使用を確認する必要があります。通常、これは引数をチェックすることを意味しますが、アップコールからの戻り値をチェックすることを意味する場合もあります。アップコール引数をチェックする必要はありません。
目的は、バグを即座に明白な方法でキャッチし、嘘をつかないコードで契約を文書化することです。
最初は、これは奇妙な質問のように見えました。null
チェックは素晴らしく、価値のあるツールです。 new
がnull
を返すことを確認するのは間違いなくばかげています。それを可能にする言語があるという事実を無視するつもりです。正当な理由があると確信していますが、その現実の中での生活を処理できるとは思いません:)冗談はさておき、少なくともnew
がない場合はnull
を返すように指定する必要があるようです。十分なメモリ。
とにかく、必要に応じてnull
をチェックすると、コードがよりクリーンになります。関数パラメーターのデフォルト値を決して割り当てないことが次の論理的なステップであると言っても過言ではありません。さらに先に進むと、必要に応じて空の配列などを返すと、コードがさらにクリーンになります。論理的に意味のある場合を除いて、null
sの取得について心配する必要がないのは素晴らしいことです。エラー値としてのnullは回避する方が適切です。
アサートを使用することは本当に素晴らしいアイデアです。特に、実行時にそれらをオフにするオプションが提供される場合。さらに、それはより明確な契約スタイルです:)