web-dev-qa-db-ja.com

null参照は本当に悪いことですか?

プログラミング言語にnull参照を含めることは「10億ドルの間違い」だと聞いたことがあります。しかし、なぜ?確かに、それらはNullReferenceExceptionsを引き起こす可能性がありますが、何ですか?言語の任意の要素を不適切に使用すると、エラーの原因になる可能性があります。

そして代わりは何ですか?これを言うのではなく、

Customer c = Customer.GetByLastName("Goodman"); // returns null if not found
if (c != null)
{
    Console.WriteLine(c.FirstName + " " + c.LastName + " is awesome!");
}
else { Console.WriteLine("There was no customer named Goodman.  How lame!"); }

あなたはこれを言うことができます:

if (Customer.ExistsWithLastName("Goodman"))
{
    Customer c = Customer.GetByLastName("Goodman") // throws error if not found
    Console.WriteLine(c.FirstName + " " + c.LastName + " is awesome!"); 
}
else { Console.WriteLine("There was no customer named Goodman.  How lame!"); }

しかし、それはどのように優れていますか?いずれにしても、顧客が存在することを確認するのを忘れると、例外が発生します。

CustomerNotFoundExceptionは、より説明的であるため、NullReferenceExceptionよりもデバッグが少し簡単だと思います。それで全部ですか?

165
Tim Goodman

nullは悪

このトピックに関するInfoQのプレゼンテーションがあります:Null References:The Billion Dollar Mistakebyトニー・ホアレ

オプションタイプ

関数型プログラミングからの代替は、SOME valueまたはNONEを含むことができる Option type を使用することです。

良い記事「オプション」パターンオプションタイプについて説明し、Javaのオプションの実装を提供します。

Javaのバグレポートも発見しました: JavaにNicePointerExceptionsを防ぐためにニースオプションタイプを追加リクエストされた機能 はJava 8.で導入されました。

93
Jonas

問題は、理論的にはオブジェクトはnullになる可能性があり、それを使用しようとすると例外を投げるので、オブジェクト指向のコードは基本的に不発弾のコレクションです。

正常なエラー処理は、ヌルチェックifステートメントと機能的に同じである可能性があります。しかし、自分が納得できなかったときにどうなるか たぶん nullは実際にはnullですか?カーブーム。次に何が起きても、1)優雅ではない、2)気に入らないと私は確信しています。

また、「デバッグが簡単」の値を無視しないでください。成熟したプロダクションコードは、狂ったように広大な生き物です。何が問題で、どこで何時間も掘るのを節約できるかもしれないことについての洞察を与えるものなら何でも。

129
BlairHippo

コードでのnull参照の使用にはいくつかの問題があります。

まず、通常は特別な状態を示すために使用されます。特殊化が通常行われるときに各状態の新しいクラスまたは定数を定義するのではなく、null参照を使用すると、損失のある非常に一般化された型/値が使用されます。

次に、null参照が表示され、それを生成したものを特定しようとすると、コードのデバッグがより困難になります。上流の実行パスを追跡できる場合でも、どの状態が有効で、その原因です。 。

3番目に、null参照により、testへの追加のコードパスが導入されます。

第4に、null参照がパラメーターおよび戻り値の有効な状態として使用されると、防御的プログラミング(設計によって引き起こされる状態の場合)では、さまざまな場所でnull参照チェックを行う必要があります …念のため。

第5に、言語のランタイムは、オブジェクトのメソッドテーブルでセレクタールックアップを実行するときに、型チェックをすでに実行しています。したがって、オブジェクトのタイプが有効/無効かどうかを確認し、ランタイムが有効なオブジェクトのタイプを確認してメソッドを呼び出すようにすることで、作業を重複させています。

NullObject pattern toを使用して、ランタイムのチェックを利用して [〜#〜] nop [〜#〜 ] その状態に固有のメソッド(通常の状態のインターフェースに準拠)と同時に、コードベース全体でnull参照の余分なチェックをすべて削除しますか?

それは特別な状態を表現したいインターフェースごとにNullObjectクラスを作成することにより、より多くの作業を伴います。ただし、少なくとも特殊化は、状態が存在する可能性があるコードではなく、各特殊状態に分離されます。 IOW、メソッドに少ない代替実行パスがあるため、テストの数が減ります。

50
Huperniketes

Nullは、期待していない限り、それほど悪くはありません。あなたはすべき nullを予期していることをコードで明示的に指定する必要があります。これは言語設計の問題です。このことを考慮:

Customer? c = Customer.GetByLastName("Goodman");
// note the question mark -- 'c' is either a Customer or null
if (c != null)
{
    // c is not null, therefore its type in this block is
    // now 'Customer' instead of 'Customer?'
    Console.WriteLine(c.FirstName + " " + c.LastName + " is awesome!");
}
else { Console.WriteLine("There was no customer named Goodman.  How lame!"); }

Customer?でメソッドを呼び出そうとすると、コンパイル時エラーが発生するはずです。より多くの言語がこれを行わない理由(IMO)は、変数の型をそのスコープに応じて変更することを提供していないためです。言語がそれを処理できる場合、問題は完全に内で解決できます。型システム。

Option-typesとMaybeを使用して、この問題を処理する機能的な方法もありますが、私はあまり詳しくありません。正しいコードでは理論上、正しくコンパイルするために1文字だけ追加する必要があるため、この方法を選択します。

nullの不幸な症状をカバーする優れた答えがたくさんあるので、代わりの引数を提示したいと思います。Nullは型システムの欠陥です。

型システムの目的は、プログラムのさまざまなコンポーネントが適切に「適合する」ことを保証することです。適切に型付けされたプログラムは、未定義の動作に「軌道から外れる」ことはできません。

文字列"Hello, world!"を任意の型の任意の変数に割り当てることができる、Javaの架空の方言、または静的型付けの優先言語を検討してください。

Foo foo1 = new Foo();  // Legal
Foo foo2 = "Hello, world!"; // Also legal
Foo foo3 = "Bonjour!"; // Not legal - only "Hello, world!" is allowed

そして、次のように変数をチェックできます:

if (foo1 != "Hello, world!") {
    bar(foo1);
} else {
    baz();
}

これについて不可能なことは何もありません。誰かが望むなら、誰かがそのような言語を設計することができます。特別な値は"Hello, world!"である必要はありません。42、タプル(1, 4, 9)、またはnullの可能性があります。しかし、なぜこれを行うのですか? Foo型の変数はFoosのみを保持する必要があります-これが型システムの要点です! nullFooではなく、"Hello, world!"と同じです。さらに悪いことに、nullany型の値ではなく、それを使ってできることは何もありません。

プログラマーは、変数が実際にFooを保持していることを決して確認できず、プログラムも確認できません。未定義の動作を回避するために、変数をFoosとして使用する前に、"Hello, world!"の変数をチェックする必要があります。前のスニペットで文字列チェックを行っても、foo1が実際にはFooであるという事実が反映されないことに注意してください。安全のために、barにも独自のチェックがある可能性があります。

Maybe/Optionタイプをパターンマッチングで使用する場合と比較してください。

case maybeFoo of
 |  Just foo => bar(foo)
 |  Nothing => baz()

Just foo句の内部では、Maybe Foo変数に実際にFoo値が含まれていること、およびその情報がコールチェーンを介して伝達され、barはチェックを行う必要はありません。 Maybe FooFooとは異なるタイプであるため、Nothingが含まれる可能性があるため、NullPointerExceptionにブラインドサイドすることはできません。プログラムについてより簡単に推論することができ、Foo型のすべての変数に実際にFoosが含まれていることを知っていれば、コンパイラーはnullチェックを省略できます。誰もが勝つ。

20
Doval

nullポインタはツールです

敵ではありません。あなたはちょうどそれらを正しく使うべきです。

典型的な無効なポインターバグを見つけて修正するのにかかる時間について、ヌルポインターのバグを特定して修正する場合と比べて、ほんの少し考えてみてください。ポインタをnullに対してチェックするのは簡単です。 null以外のポインタが有効なデータを指しているかどうかを確認するのはPITAです。

それでも納得できない場合は、シナリオに適切なマルチスレッドを追加して、もう一度考えてみてください。

物語の教訓:水で子供を投げないでください。そして事故の道具を責めないでください。ずっと前にゼロを発明した人は、その日あなたが「何もない」と名付けることができたので、かなり賢い人でした。 nullポインターはそれほど遠くないです。


編集:NullObjectパターンは、nullの可能性がある参照よりも優れたソリューションのようですが、それ自体で問題が発生します。

  • NullObjectを保持している参照は、(理論的には)メソッドが呼び出されても何もしません。したがって、誤って割り当てられていない参照が原因で微妙なエラーが発生する可能性があります。これにより、nullでないことが保証されますが(yehaa!)、不要なアクションを実行します。 NPEを使用すると、問題がどこにあるかは明らかです。 NullObjectの動作が(正しくない)場合、エラーが(あまりに)遅く検出されるリスクがあります。これは、誤って無効なポインターの問題に似ているわけではなく、同様の結果をもたらします。参照は、有効なデータのように見えますが、そうではない何かを指しているため、データエラーが発生し、追跡が難しい場合があります。正直に言うと、これらのケースでは、まばたきがなければ、すぐに失敗するNPEがすぐに失敗する後で突然ヒットする論理/データエラーよりも優先します。道を下って。エラーのコストがどの段階で検出されたかに関係するIBMの調査を覚えていますか?

  • メソッドがNullObjectで呼び出されたとき、プロパティゲッターまたは関数が呼び出されたとき、またはメソッドがoutを介して値を返すときに何もしないという概念は成り立ちません。たとえば、戻り値がintであり、「何もしない」が「戻り値0」を意味すると判断したとします。しかし、0が返される(されない)正しい「何もない」ことをどのようにして確認できますか?結局のところ、NullObjectは何もすべきではありませんが、値を要求されたときに、なんらかの反応をする必要があります。失敗することはオプションではありません。NPEを防ぐためにNullObjectを使用し、それを別の例外(実装されていない、ゼロで除算する、など)と交換することはありませんか?それで、何かを返さなければならないときに、何を正しく何も返さないのですか?

仕方がないのですが、誰かがNullObjectパターンをすべての問題に適用しようとすると、別の問題を実行して1つの間違いを修復しようとするように見えます。 someの場合、これは間違いなく優れた有用な解決策ですが、すべての場合に確実に役立つわけではありません。

15
JensG

(古い質問のためにリングに帽子を投げました;))

Nullの特定の問題は、静的型付けが無効になることです。

_Thing t_がある場合、コンパイラはt.doSomething()を呼び出せることを保証できます。まあ、UNLESS tは実行時にnullになります。今、すべての賭けはオフです。コンパイラーは大丈夫だと言ったが、後でtが実際にはdoSomething()ではないことを知った。そのため、コンパイラーを信頼して型エラーをキャッチできるのではなく、ランタイムがエラーをキャッチするまで待つ必要があります。 Pythonを使うのもいいかもしれません!

したがって、ある意味では、nullは静的型付けシステムに動的型付けを導入し、期待される結果をもたらします。

ゼロによる除算や負の対数などの違いは、i = 0の場合でもintであることです。コンパイラはその型を保証できます。問題は、ロジックがその値をロジックによって許可されていない方法で誤って適用することです...しかし、ロジックがそうした場合、それはバグの定義とほぼ同じです。

(コンパイラーcanは、これらの問題のいくつかをキャッチします。ところで、コンパイル時に_i = 1 / 0_のような式にフラグを立てるようなものです。関数を追跡し、パラメーターがすべて関数のロジックと一致していることを確認するコンパイラー)

実際的な問題は、多くの余分な作業を行い、実行時に自分を保護するためにnullチェックを追加することですが、それを忘れた場合はどうでしょうかコンパイラーは、以下の割り当てを停止します。

文字列s = new Integer(1234);

それでは、なぜsへの逆参照を解除する値(null)への割り当てを許可する必要があるのでしょうか。

コードで「値なし」と「型付き」参照を混在させることで、プログラマーに余分な負担をかけます。また、NullPointerExceptionが発生すると、それらを追跡するのにさらに時間がかかる可能性があります。静的型付けに頼って「this is期待されるものへの参照」と言うのではなく、言語に「this たぶん期待される何かへの参照」と言わせます。 」

14
Rob

そして代わりは何ですか?

オプションのタイプとパターンマッチング。私はC#を知らないので、ここにScala#と呼ばれ​​る架空の言語のコードがあります:-)

Customer.GetByLastName("Goodman")    // returns Option[Customer]
match
{
    case Some(customer) =>
    Console.WriteLine(customer.FirstName + " " + customer.LastName + " is awesome!");

    case None =>
    Console.WriteLine("There was no customer named Goodman.  How lame!");
}
8
fredoverflow

Null参照は、意味のないコードを許可するため、間違いです。

foo = null
foo.bar()

型システムを活用する場合、代替案があります。

Maybe<Foo> foo = null
foo.bar() // error{Maybe<Foo> does not have any bar method}

一般的なアイデアは、変数をボックスに入れることです。そして、あなたができる唯一のことは、それを箱から出すことです。

Haskellは最初からそれを持っています(Maybe)、C++ではboost::optional<T>を利用できますが、未定義の動作を取得できます...

8
Matthieu M.

Nullの問題は、nullを許可する言語では、それに対して防御的にプログラミングすることを余儀なくされることです。それを確認するには、多くの努力が必要です(防御的なifブロックを使用するよりもはるかに多く)。

  1. 期待するオブジェクトnotがnullであることは確かにnullになることはありません。
  2. あなたの防御メカニズムが実際にすべての潜在的なNPEを効果的に処理すること。

したがって、実際、ヌルは最終的にはコストがかかるものになります。

6
luis.espinal

プログラミング言語がプログラムを実行する前に、プログラムの正当性をどの程度証明しようとするかという問題。静的型付け言語では、正しい型があることを証明します。 nullを許可しない参照のデフォルトに(オプションでnullを許可する参照を使用して)移動することにより、nullが渡され、渡されるべきではない多くの場合を排除できます。問題は、null可能ではない参照を処理するための余分な労力がプログラムの正確さという点でメリットに値するかどうかです。

4
Winston Ewert

問題はそれほどnullではなく、多くの現代の言語ではnull以外の参照型を指定できないことです。

たとえば、コードは次のようになります。

public void MakeCake(Egg egg, Flour flour, Milk milk)
{
    if (Egg == null) { throw ... }
    if (flour == null) { throw ... }
    if (milk == null) { throw ... }

    Egg.Crack();
    MixingBowl.Mix(Egg, flour, milk);
    // etc
}

// inside Mixing bowl class
public void Mix(Egg egg, Flour flour, Milk milk)
{
    if (Egg == null) { throw ... }
    if (flour == null) { throw ... }
    if (milk == null) { throw ... }

    //... etc
}

クラス参照が渡されると、防御テストでは、特にテスト中の再利用可能なユニットを構築するときに、事前にnullをチェックしたばかりの場合でも、すべてのパラメーターを再度nullにチェックすることをお勧めします。同じ参照は、コードベース全体でnullを10回簡単にチェックできます!

ものを取得するときに通常のnull許容型があり、nullを処理し、nullをチェックせずにすべての小さなヘルパー関数とクラスにnull不可型として渡した方がいいのではないでしょうか。時間?

それがオプションソリューションのポイントです。エラー状態をオンに切り替えることはできませんが、null引数を暗黙的に受け入れることができない関数を設計することができます。タイプの「デフォルト」の実装がnull可能かnull不可かは、両方のツールを使用できる柔軟性よりも重要ではありません。

http://twistedoakstudios.com/blog/Post330_non-nullable-types-vs-c-fixing-the-billion-dollar-mistake

これは、C#の#2の最も投票された機能としてもリストされています-> https://visualstudio.uservoice.com/forums/121579-visual-studio/category/30931-languages-c

4
Michael Parker

ヌルは悪です。ただし、ヌルの欠如はより大きな悪になる可能性があります。

問題は、現実の世界では、データがない状況がよくあることです。 nullのないバージョンの例は、まだ爆発する可能性があります-論理ミスを犯し、グッドマンをチェックするのを忘れたか、またはチェックマンとルックアップの間にグッドマンが結婚した可能性があります。 (Moriartyがコードのすべてのビットを監視し、つまずきを試みることを理解している場合、これはロジックの評価に役立ちます。)

彼女がいないときにグッドマンのルックアップは何をしますか? nullがないと、何らかのデフォルトの顧客を返す必要があります-そして今、あなたはそのデフォルトに商品を販売しています。

基本的には、コードが何に関係なく機能するか、または正しく機能するかがより重要かどうかにかかっています。不適切な動作が動作しないより望ましい場合は、nullは必要ありません。 (これが正しい選択の例として、Ariane Vの最初の打ち上げを考えてみてください。キャッチされていない/ 0エラーが原因でロケットがハードターンし、これが原因でバラバラになったときに自己破壊が発生しました。値ブースターが点火された瞬間には、実際には計算されずに目的が果たされませんでした-ルーチンがゴミを返すにもかかわらず軌道に乗っていたでしょう)

100のうち少なくとも99回はnullを使用することを選択します。しかし、私はそれらの自己のバージョンに注意してジャンプして喜びます。

3
Loren Pechtel

最も一般的なケースに合わせて最適化します。

常にnullをチェックするのは面倒です。単にCustomerオブジェクトを取得して操作できるようにしたいだけです。

通常の場合、これは問題なく機能するはずです。ルックアップを行い、オブジェクトを取得して使用します。

例外的なケースでは、(ランダムに)名前で顧客を検索していて、そのレコード/オブジェクトが存在するかどうかがわからない場合は、これが失敗したことを示す必要があります。この状況での答えはRecordNotFound例外をスローすることです(または、その下にあるSQLプロバイダーにこれを実行させます)。

おそらくデータがユーザーによって入力されたために、入ってくるデータ(パラメーター)を信頼できるかどうかわからない場合は、「TryGetCustomer(name、out customer)」を指定することもできます。パターン。 int.Parseおよびint.TryParseとの相互参照。

1
JBRWilkinson

例外は評価されません!

彼らは理由があります。コードが正しく実行されていない場合、exceptionと呼ばれる天才パターンがあり、何かが間違っていることを示しています。

Nullオブジェクトの使用を回避することで、これらの例外の一部を非表示にします。私は彼がnullポインター例外を適切に型付けされた例外に変換するOPの例について急上昇していません。これは読みやすさを向上させるため、実際には良いことかもしれません。 @Jonasが指摘したようにOption typeソリューションを使用する場合、例外を非表示にします。

本番アプリケーションよりも、ボタンをクリックして空のオプションを選択したときに例外がスローされるのではなく、何も起こりません。 nullポインターの代わりに例外がスローされ、おそらくその例外のレポートを取得し(多くの本番アプリケーションでは、そのようなメカニズムがあります)、実際に修正することはできません。

例外を回避してコードを完全に保護することは悪い考えです。例外を修正してコードを完全に保護することは、私が選択する方法です。

0
Ilya Gazman

番号。

言語がnullのような特別な値を考慮していないことが言語の問題です。 Kotlin または Swift のような言語を見ると、ライターは遭遇するたびにnull値の可能性を明示的に処理する必要があります。危険はありません。

問題のような言語では、この言語ではnullを任意の値にすることができます-Hは タイプしますが、後でそれを確認するための構造を提供しません。これにより、予期せずnullになることになり(特に他の場所から渡された場合)、クラッシュします。 それは悪いことです:対処せずにnullを使用できるようにします。

0
Ben Leggiero

Nullsは明示的にチェックする必要があるため問題がありますが、コンパイラーはチェックするのを忘れたことを警告できません。時間のかかる静的解析だけがそれを教えてくれます。幸いにも、いくつかの良い代替案があります。

変数をスコープ外にしてください。頻度が高すぎるため、nullは、プログラマーが変数を宣言するのが早すぎる場合や、変数を長すぎる場合にプレースホルダーとして使用されます。最善の方法は、単に変数を取り除くことです。有効な値を入力するまで宣言しないでください。これはあなたが考えるほど難しい制限ではなく、コードをよりきれいにします。

nullオブジェクトパターンを使用します。場合によっては、欠損値がシステムの有効な状態です。 nullオブジェクトパターンは、ここでの良い解決策です。常に明示的なチェックを行う必要はありませんが、それでもnull状態を表すことができます。一部の人々が主張するように、これは「エラー状態の説明」ではありません。なぜなら、この場合null状態は有効な意味論的状態だからです。とはいえ、null状態が有効な意味論的状態ではない場合は、このパターンを使用しないでください。変数をスコープから外すだけです。

Maybe/Optionを使用します。まず、これにより、不足している値を確認する必要があることをコンパイラーに警告しますが、あるタイプの明示的なチェックを別のタイプに置き換えるだけではありません。 Optionsを使用すると、コードをチェーンし、値が存在するかのように実行できます。実際に最後の最後まで確認する必要はありません。 Scalaでは、サンプルコードは次のようになります。

val customer = customerDb getByLastName "Goodman"
val awesomeMessage =
  customer map (c => s"${c.firstName} ${c.lastName} is awesome!")
val notFoundMessage = "There was no customer named Goodman.  How lame!"
println(awesomeMessage getOrElse notFoundMessage)

2行目では、素晴らしいメッセージを作成していますが、顧客が見つかったかどうかを明示的に確認することはありません。美しさは必要ありませんOptionNoneである場合に何が起こるかを明確に考えるのは、最後の行(後で何行も続く場合や、別のモジュールの場合もある)までです。それだけでなく、それを忘れてしまった場合は、型チェックが行われません。それでも、明示的なifステートメントなしで、非常に自然な方法で行われます。

それをnullと比較すると、型チェックは問題なく、使用中に運が良ければ、実行時にすべてのステップまたはアプリケーション全体が明示的にチェックされる必要があります。あなたのユニットテストの練習をしてください。面倒な価値はありません。

0
Karl Bielefeldt

NULLの中心的な問題は、システムの信頼性を低下させることです。 1980年に、チューリング賞に捧げられた論文でトニー・ホアレは次のように書いています。

そのため、ADAの作成者および設計者に対する私の最高のアドバイスは無視されました。 …。現在の状態のこの言語を、原子力発電所、巡航ミサイル、早期警報システム、対弾道ミサイル防衛システムなどの信頼性が重要なアプリケーションで使用できないようにします。プログラミング言語のエラーの結果として次に迷うロケットは、金星への無害な旅の探査宇宙ロケットではないかもしれません。それは私たち自身の都市の1つで爆発する核弾頭かもしれません。信頼性の低いプログラムを生成する信頼性の低いプログラミング言語は、安全でない自動車、有毒な農薬、または原子力発電所での事故よりも、環境および社会に対してはるかに大きなリスクを構成します。リスクを増やすのではなく、減らすように用心してください。

それ以来、ADA言語は大きく変化しましたが、そのような問題はJava、C#、および他の多くの一般的な言語にまだ存在しています。

クライアントとサプライヤーの間で契約を結ぶことは開発者の義務です。たとえば、C#では、Javaと同様に、Genericsを使用して、読み取り専用NullableClass<T>(2つのオプション)を作成することにより、Null参照の影響を最小限に抑えることができます。

class NullableClass<T>
{
     public HasValue {get;}
     public T Value {get;}
}

そしてそれを次のように使用します

NullableClass<Customer> customer = dbRepository.GetCustomer('Mr. Smith');
if(customer.HasValue){
  // one logic with customer.Value
}else{
  // another logic
} 

または、C#拡張メソッドで2つのオプションスタイルを使用します。

customer.Do(
      // code with normal behaviour
      ,
      // what to do in case of null
) 

違いは重要です。メソッドのクライアントとして、あなたは何を期待すべきか知っています。チームはルールを持つことができます:

クラスがNullableClass型でない場合、そのインスタンスはnullであってはなりません。

チームは Design by Contract とコンパイル時の静的チェックを使用して、たとえば次の前提条件を使用して、このアイデアを強化できます。

function SaveCustomer([NotNullAttribute]Customer customer){
     // there is no need to check whether customer is null 
     // it is a client problem, not this supplier
}

または文字列

function GetCustomer([NotNullAndNotEmptyAttribute]String customerName){
     // there is no need to check whether customerName is null or empty 
     // it is a client problem, not this supplier
}

これらのアプローチは、アプリケーションの信頼性とソフトウェアの品質を大幅に向上できます。 Design by Contractは Hoare logic の例です。これは、1988年にBertrand Meyerが著したObject-Oriented Software Construction本とEiffel言語で入力されましたが、現代のソフトウェアクラフトでは無効に使用されていません。

0
Artru