web-dev-qa-db-ja.com

nullが悪い場合、なぜ現代の言語はそれを実装するのですか?

JavaまたはC#のような言語の設計者は、null参照の存在に関連する問題を知っていたと思います( null参照は本当に悪いことですか? を参照してください)。また、オプションの型は、null参照ほど複雑ではありません。

なぜ彼らはとにかくそれを含めることに決めたのですか? null参照の欠如は、言語の作成者とユーザーの両方から、より良い品質のコード(特に、より良いライブラリの設計)を促進(または強制さえ)するだろうと私は確信しています。

それは単に保守主義のせいでしょうか-「他の言語にもあります、私たちにも持っている必要があります...」?

84
mrpyo

免責事項:私は個人的に言語デザイナーを知りませんので、私があなたに与えるどんな答えも推測です。

Tony Hoare 自身:

私はそれを10億ドルの間違いと呼んでいます。それは1965年にnull参照の発明でした。そのとき、私はオブジェクト指向言語(ALGOL W)での参照用の最初の包括的な型システムを設計していました。私の目標は、コンパイラーによって自動的に実行されるチェックにより、参照のすべての使用が絶対的に安全であることを確認することでした。 しかし、実装が非常に簡単だったという理由だけで、null参照を挿入するという誘惑に抵抗できませんでした。これにより、無数のエラー、脆弱性が発生しました、およびシステムクラッシュ。これはおそらく過去40年間で10億ドルの痛みと損傷を引き起こしています。

鉱山を強調します。

当然それは当時彼にとって悪い考えのようには思われませんでした。同じ理由で一部継続している可能性があります-チューリング賞を受賞したクイックソートの発明者にとって良い考えのように思えたとしても、多くの人がまだしないことは驚くに値しませんなぜそれが悪なのか理解できません。また、マーケティングと学習曲線の両方の理由から、新しい言語が古い言語に似ていると便利な場合もあります。適例:

「私たちはC++プログラマーの後にいました。私たちはそれらの多くをLISPの約半分までドラッグすることに成功しました。」 -Guy Steele、Java仕様の共著者

(出典: http://www.paulgraham.com/icad.html

そしてもちろん、Cにはnullがあるため、C++にもnullがあり、Cの歴史的な影響を考慮する必要はありません。 C#は、MicrosoftによるJavaの実装であるJ ++に取って代わりました。また、Windows開発の選択言語としてC++にも取って代わったため、どちらからでも入手できました。

[〜#〜] edit [〜#〜]これは、検討する価値のあるHoareからの別の引用です:

プログラミング言語は全体として、以前よりもはるかに複雑です。オブジェクト指向、継承、およびその他の機能は、一貫性のある科学的な観点からはまだ十分に検討されていません。 私が科学者として一生ずっと追求してきた私の元の仮定は、正確さの基準を、まともなプログラミング言語の設計—ユーザーにトラップを設定しないもの、およびプログラムのさまざまなコンポーネントが仕様のさまざまなコンポーネントに明確に対応しているため、構成について推論することができます。 [...]コンパイラを含むツールは、正しいプログラムを書くことが何を意味するかのいくつかの理論に基づいている必要があります。 -2002年7月17日、イギリスのケンブリッジ、Philip L. Franaによるオーラルヒストリーインタビュー;ミネソタ大学チャールズバベッジインスティチュート。[ http://www.cbi.umn.edu/oh/display.phtml?id=343]

再び、私の鉱山を強調します。 Sun/OracleとMicrosoftは企業であり、どの企業の収益もお金です。 nullを使用することのメリットは、短所を上回った可能性があります。または、期限が厳しすぎて問題を完全に検討できなかった可能性があります。おそらく締め切りが原因で発生した別の言語の失敗の例として:

Cloneableが壊れているのは残念ですが、それは起こります。 元のJava AP​​Iは、市場の締め切りに間に合うように厳しい期限内で非常に迅速に実行されました。元のJavaチームは素晴らしい仕事をしましたが、すべてのAPIが完璧なわけではありません。 Cloneableは弱点であり、人々はその限界に注意する必要があると思います。 -ジョシュ・ブロッホ

(出典: http://www.artima.com/intv/bloch13.html

97
Doval

JavaまたはC#などの言語の設計者は、null参照の存在に関連する問題を知っていました。

もちろん。

また、オプション型の実装は、null参照よりもはるかに複雑ではありません。

失礼ですが同意できません! C#2でnull許容値型に含まれる設計上の考慮事項は、複雑で、物議を醸し、困難でした。彼らは言語とランタイムの両方の設計チームに数か月にわたる議論、プロトタイプの実装などを取りました。実際、nullable boxingのセマンティクスは、C#2.0の出荷に非常に近く変更されました。

なぜ彼らはとにかくそれを含めることに決めたのですか?

すべての設計は、多くの微妙かつ全体的に互換性のない目標の中から選択するプロセスです。考慮される要因のほんの一部については、簡単なスケッチしか提示できません。

  • 言語機能の直交性は、一般的に良いことと考えられています。 C#には、null許容値型、null不可値型、null許容参照型があります。 null化できない参照型が存在しないため、型システムが非直交になります。

  • C、C++、およびJava=の既存のユーザーに対する知識が重要です。

  • COMとの簡単な相互運用性は重要です。

  • 他のすべての.NET言語との容易な相互運用性が重要です。

  • データベースとの容易な相互運用性は重要です。

  • セマンティクスの一貫性は重要です。 TheKingOfFranceがnullに等しい場合、それは常に「現在、フランス王は存在しない」という意味ですか、それとも「フランス王は確かに存在します。今、誰なのかわからない」という意味ですか?それとも、「フランスに王を持つこと自体が無意味なので、質問すらしないでください!」という意味でしょうか。 nullは、これらすべてのことを意味し、C#ではさらに多くのことを意味します。これらの概念はすべて有用です。

  • パフォーマンスコストは重要です。

  • 静的分析を受け入れることが重要です。

  • 型システムの一貫性は重要です。 常に null可能ではない参照がneverの下でanyが無効であると認められる状況であることを知っているでしょうか?参照型のnull不可フィールドを持つオブジェクトのコンストラクターはどうですか?そのようなオブジェクトのファイナライザでは、オブジェクトがファイナライズされるとどうでしょう参照を埋めるはずのコードが例外をスローしたため?保証についてあなたにうそをついている型システムは危険です。

  • そして、意味論の一貫性はどうですか? Null values使用すると伝播しますが、null references使用すると例外がスローされます。それは矛盾しています。その矛盾はいくつかの利益によって正当化されますか?

  • 他の機能を壊すことなく機能を実装できますか?機能が他に考えられる将来の機能にはどのようなものがありますか?

  • あなたはあなたが望む軍ではなく、あなたが持っている軍と戦争に行きます。 C#1.0にはジェネリックがなかったので、Maybe<T>代替として、完全な非スターターです。ランタイムチームがジェネリックを追加している間に、.NETが2年間遅れて、null参照を排除しただけなのでしょうか。

  • 型システムの一貫性はどうですか? Nullable<T>すべての値タイプ-いいえ、待って、それは嘘です。言うことはできませんNullable<Nullable<T>>。できますか?もしそうなら、その望ましいセマンティクスは何ですか?型システム全体に、この機能のためだけに特別なケースを持たせることは価値がありますか?

等々。これらの決定は複雑です。

121
Eric Lippert

Nullは、価値の欠如を表す非常に有効な目的を果たします。

私はヌルの乱用と、特に自由に使用した場合に引き起こされる可能性のあるすべての頭痛と苦痛について私が知っている最も声高な人だと言います。

私の個人的なスタンスは、人々がそれが必要かつ適切であると正当化できる場合、人々はnullを使用するかもしれないということですonly.

nullを正当化する例:

Date of Deathは通常、null入力可能なフィールドです。死亡の日付には3つの状況が考えられます。人が死亡し、日付がわかっている、人が死亡して日付が不明である、または人が死亡していないために死亡日が存在しない。

Date of DeathもDateTimeフィールドであり、「不明」または「空」の値はありません。使用する言語に応じて異なる新しい日時を作成したときに表示されるデフォルトの日付はありますが、技術的にはその時点で人が亡くなり、もしそうした場合には「空の値」としてフラグを立てる可能性がありますデフォルトの日付を使用します。

データは状況を適切に表す必要があります。

人物は死者の死亡日がわかっている(1984年3月9日)

シンプル、「1984年3月9日」

人は死者の死亡日は不明です

だから何が一番いいの? Null、 '0/0/0000'、または'01/01/1869 '(またはデフォルト値は何ですか?)

個人は死者の日付ではありません該当なし

だから何が一番いいの? Null、 '0/0/0000'、または'01/01/1869 '(またはデフォルト値は何ですか?)

では、それぞれの値について考えてみましょう...

  • Null、それはあなたが注意する必要がある影響と懸念を持ち、最初にnullでないことを確認せずに誤って操作しようとすると、たとえば例外がスローされます、しかし、それはまた、実際の状況を最もよく表します...人が死んでいない場合、死の日付は存在しません...それは何もありません...それはnullです...
  • 0/0/0000、これは一部の言語では問題ない場合があり、日付がないことの適切な表現である場合もあります。残念ながら、一部の言語および検証では、これを無効な日時として拒否するため、多くの場合、これは失敗します。
  • 1/1/1869(またはデフォルトの日時値が何であれ)、ここでの問題は、処理が難しくなることです。それをあなたの価値の欠如として使用することができますが、死の日付を持たないすべての私のレコードを除外したい場合はどうなりますか?データの整合性の問題を引き起こす可能性があるその日に実際に死亡した人々を簡単に除外できました。

事実は時々あなたはDoは何も表現する必要がなく、時々変数の型がそのためにうまく機能することは確かですが、多くの場合変数の型は何も表現できないことが必要です。

リンゴがない場合、リンゴは0個ですが、リンゴの数がわからない場合はどうなりますか?

どうしてもnullは悪用され、潜在的に危険ですが、場合によっては必要になります。私が値を提供するまで、値の欠如と何かがそれを表す必要があるので、それは多くの場合のデフォルトにすぎません。 (ヌル)

28
RualStorge

ジョーンズに遅れを取らないように、「他の言語にもあるし、私たちにも持っている必要があります...」までは行きません。新しい言語の重要な機能は、他の言語の既存のライブラリと相互運用できることです(Cを参照)。 Cはnullポインターを持っているので、相互運用性レイヤーには必然的にnullの概念(またはそれを使用するときに爆発する他の「存在しない」同等の概念)が必要です。

言語設計者は オプションの種類 を使用することを選択し、ヌルパスがどこでもヌルパスになる可能性があることを強制することができます。そして、それはほぼ間違いなくバグの減少につながります。

ただし(特にJavaとC#の場合、導入のタイミングと対象読者のために)、この相互運用性レイヤーにオプションタイプを使用すると、採用を魚雷で処理しなければ害が生じる可能性があります。オプションタイプのいずれか90年代半ばから後半のC++プログラマーの地獄に迷惑をかける-または、相互運用性レイヤーはnullに遭遇すると例外をスローし、90年代中期から後半のC++プログラマーの地獄に迷惑をかける...

10
Telastyn

まず第一に、私たちは皆、空の概念が必要であることに同意できると思います。情報の不在を表す必要がある状況がいくつかあります。

null参照(およびポインタ)を許可することは、この概念の1つの実装にすぎず、問題があることがわかっているものの、おそらく最も一般的です。C、Java、Python、Ruby、PHP、JavaScriptなど...同様のnull

どうして ?さて、代替手段は何ですか?

Haskellなどの関数型言語では、OptionまたはMaybeタイプがあります。ただし、これらは以下に基づいて構築されています。

  • パラメトリックタイプ
  • 代数的データ型

現在、元のC、Java、Python、RubyまたはPHPはこれらの機能のいずれかをサポートしていますか?いいえ。Javaの欠陥のあるジェネリックは最近)です言語の歴史のなかで、私はどういうわけか他の人がそれらを実装することすら疑っています。

そこにあります。 nullは簡単で、パラメトリック代数データ型はより困難です。人々は最も単純な代替案を選びました。

7
Matthieu M.

Null/nil/none自体は悪ではありません。

彼の誤解を招くように名付けられた有名なスピーチ「10億ドルの間違い」を見ると、Tony Hoareがany変数にnullを保持できるようにすることは非常に巨大だったと語っています間違い。別の方法-オプションを使用する-は実際にはnull参照を取り除きます。代わりに、nullを保持できる変数とそうでない変数を指定できます。

実際のところ、適切な例外処理を実装する最新の言語では、null逆参照エラーは他の例外と何の違いもありません。それを見つけて修正します。 null参照へのいくつかの代替手段(たとえば、nullオブジェクトパターン)はエラーを非表示にし、非常に遅くまで黙って失敗します。私の意見では、それは fail fail よりはるかに優れています。

では、問題はなぜ言語がオプションの実装に失敗するのかということです。実際のところ、間違いなく最も人気のある言語であるC++には、NULLを割り当てられないオブジェクト変数を定義する機能があります。これは、スピーチで述べられた「ヌル問題」の解決策です。次に人気のある型付き言語であるJavaにないのはなぜですか?なぜ 非常に多くの欠陥 があるのか​​、特にその型システムになぜあるのかと思う人もいるかもしれません。言語が体系的にこの間違いを犯しているとは、本当に言えるとは思いません。する人もいれば、しない人もいます。

5
B T

なぜなら、プログラミング言語は一般に、技術的に正しいというよりは実際に役立つように設計されているからです。実際のところ、null状態は、データの不良または欠落、またはまだ決定されていない状態が原因でよく発生します。技術的に優れたソリューションは、単にnull状態を許可してプログラマーがミスを犯すという事実を吸い込むよりも扱いにくいものです。

たとえば、ファイルを操作する簡単なスクリプトを作成する場合、次のような疑似コードを作成できます。

file = openfile("joebloggs.txt")

for line in file
{
  print(line)
}

joebloggs.txtが存在しない場合は、失敗します。問題は、おそらく大丈夫な単純なスクリプトの場合であり、より複雑なコードの多くの状況では、それが存在し、エラーが発生しないことがわかっているため、チェックを強制することで時間を無駄にします。より安全な代替手段は、潜在的な障害状態を正しく処理するように強制することで安全を実現しますが、多くの場合、それをしたくないので、ただ進みたいだけです。

4
Jack Aidley

NULL(またはnil、またはNil、またはnull、またはNothingなど)の明確で実用的な使用法があります。優先言語で呼び出されます)ポインタ。

例外システム(Cなど)を持たない言語の場合、ポインターを返す必要があるときに、NULLポインターをエラーのマークとして使用できます。例えば:

_char *buf = malloc(20);
if (!buf)
{
    perror("memory allocation failed");
    exit(1);
}
_

ここでは、malloc(3)から返されたNULLが失敗のマーカーとして使用されています。

メソッド/関数の引数で使用すると、引数のデフォルトの使用を示すか、出力引数を無視できます。以下の例。

例外メカニズムを備えたこれらの言語でも、特に例外処理にコストがかかる場合(Objective-Cなど)は、nullポインタをソフトエラー(つまり、回復可能なエラー)の指標として使用できます。

_NSError *err = nil;
NSString *content = [NSString stringWithContentsOfURL:sourceFile
                                         usedEncoding:NULL // This output is ignored
                                                error:&err];
if (!content) // If the object is null, we have a soft error to recover from
{
    fprintf(stderr, "error: %s\n", [[err localizedDescription] UTF8String]);
    if (!error) // Check if the parent method ignored the error argument
        *error = err;
    return nil; // Go back to parent layer, with another soft error.
}
_

ここで、ソフトエラーがキャッチされない場合、プログラムはクラッシュしません。これにより、Javaのようなクレイジーな試行錯誤がなくなります。ソフトエラーは中断されないため、プログラムフローをより適切に制御できます(また、残りのいくつかのハード例外は通常回復できず、捕捉されずに残ります)。

4
Maxthon Chan

2つの関連しますが、少し異なる問題があります。

  1. nullは存在する必要がありますか?または、常に_Maybe<T>_を使用する必要がありますか?
  2. すべての参照はnull可能である必要がありますか?そうでない場合、どちらをデフォルトにする必要がありますか?

    Null可能な参照型を_string?_または同様のものとして明示的に宣言する必要がある場合、プログラマーが慣れているものとあまり違いなく、nullが引き起こす問題のほとんど(すべてではない)を回避できます。

すべての参照がnull可能である必要があるわけではないことに少なくとも同意します。しかし、nullを回避することには、その複雑さがないわけではありません。

.NETは、マネージコードによって最初にアクセスされる前に、すべてのフィールドを_default<T>_に初期化します。つまり、参照型にはnullまたは同等のものが必要であり、値型はコードを実行せずに何らかのzeroに初期化できるということです。これらの両方に深刻な欠点がありますが、default初期化の単純さはこれらの欠点を上回った可能性があります。

  • インスタンスフィールドの場合は、thisポインタをマネージコードに公開する前にフィールドの初期化を要求することで、これを回避できます。 Spec#は、C#と比較してコンストラクターチェーンとは異なる構文を使用して、この方法を採用しました。

  • 静的フィールドの場合、thisポインターを単純に非表示にすることはできないため、フィールド初期化子で実行できるコードの種類に強い制限を課さない限り、これはより困難になります。

  • 参照型の配列を初期化する方法は?長さよりも大きい容量の配列によって裏打ちされた_List<T>_を考えてみます。残りの要素にはsome値が必要です。

もう1つの問題は、何も見つからない場合にbool TryGetValue<T>(key, out T value)valueとして返すdefault(T)のようなメソッドを許可しないことです。この場合、そもそもoutパラメータが悪い設計であると主張するのは簡単であり、このメソッドは区別する共用体または多分を返す必要があります。

これらの問題はすべて解決できますが、「nullを禁止し、すべてが正常である」ほど簡単ではありません。

4
CodesInChaos

最も有用なプログラミング言語では、データ項目を任意の順序で書き込んだり読み取ったりすることができるため、プログラムが実行される前に読み取りと書き込みが発生する順序を静的に決定できないことがよくあります。コードが実際に有用なデータをすべてのスロットに格納してから読み取る場合が多くありますが、それを証明するのは困難です。したがって、コードが少なくとも理論的にはまだ有用な値で書かれていないものを読み取ろうとすることが可能なプログラムを実行する必要があることがよくあります。コードがそうすることが合法であるかどうかにかかわらず、コードが試行を止める一般的な方法はありません。唯一の問題は、それが起こったときに何が起こるべきかということです。

言語やシステムが異なれば、アプローチも異なります。

  • 1つのアプローチは、書き込まれていないものを読み取ろうとすると、即座にエラーが発生すると言うことです。

  • 2番目のアプローチは、格納された値が意味的に役立つ方法がない場合でも、コードを読み取って読み取る前に、すべての場所で値を提供するようコードに要求することです。

  • 3番目のアプローチは、問題を単に無視して、「自然に」何が起こるかを単に発生させることです。

  • 4番目のアプローチは、すべてのタイプにデフォルト値が必要であり、他に何も書き込まれていないスロットにはデフォルトでその値が設定されるということです。

アプローチ#4はアプローチ#3よりもはるかに安全であり、一般的にアプローチ#1および#2よりも安価です。次に、参照タイプのデフォルト値をどうするかという問題が残ります。不変の参照型の場合、多くの場合、デフォルトのインスタンスを定義することは意味があり、その型の変数のデフォルトはそのインスタンスへの参照であると言うことができます。ただし、変更可能な参照型の場合は、あまり役に立ちません。変更可能な参照型が書き込まれる前にそれを使用しようとした場合、一般に、使用を試みた時点でトラップする以外に、安全な方法はありません。

意味的に言えば、_Customer[20]_型の配列customersがあり、_Customer[4]_に何も格納せずにCustomer[4].GiveMoney(23)を試みると、実行はトラップする必要があります。コードがGiveMoneyを試みるまで待つのではなく、_Customer[4]_を読み取ろうとするとすぐにトラップする必要があると主張することもできますが、スロットを読み取ることが役立つ場合は十分にあります。 t値を保持し、その情報を利用する場合、読み取り試行自体が失敗することは、多くの場合大きな迷惑になります。

一部の言語では、特定の変数にnullを含めないように指定できます。nullを格納しようとすると、すぐにトラップがトリガーされます。これは便利な機能です。ただし、一般に、プログラマーが参照の配列を作成できるようにする言語では、ヌル配列要素の可能性を考慮に入れるか、配列要素の初期化を意味のないデータに強制する必要があります。

2
supercat