230

プログラマーが null エラー/例外について不平を言うと、null なしで何をするのかと尋ねられることがよくあります。

オプション型のカッコ良さについての基本的な考え方はある程度持っていますが、それを表現する知識や語学力がありません。平均的なプログラマーに親しみやすい方法で書かれた、以下の優れた説明は何ですか?

  • デフォルトで参照/ポインターが null 可能であることの望ましくないこと
  • 次のようなnullケースのチェックを容易にする戦略を含む、オプションタイプの仕組み
    • パターンマッチングと
    • 単項内包表記
  • メッセージを食べる nil などの代替ソリューション
  • (私が見逃した他の側面)
4

11 に答える 11

439

null が望ましくない理由の簡潔な要約は、意味のない状態は表現可能であってはならないということだと思います。

ドアをモデリングしているとします。開いている、閉じているがロックされていない、閉じてロックされているの 3 つの状態のいずれかになります。これで、次のようにモデル化できます

class Door
    private bool isShut
    private bool isLocked

3 つの状態をこれら 2 つのブール変数にマッピングする方法は明らかです。しかし、これにより、4 番目の望ましくない状態が利用可能になりますisShut==false && isLocked==true。表現として選択した型はこの状態を許容するため、クラスがこの状態にならないように (おそらく不変条件を明示的にコーディングして) 精神的な努力を払わなければなりません。対照的に、定義できる代数データ型またはチェックされた列挙型の言語を使用していた場合、

type DoorState =
    | Open | ShutAndUnlocked | ShutAndLocked

それから私は定義することができました

class Door
    private DoorState state

そしてもう心配はありません。型システムは、 のインスタンスが存在する可能性のある状態が 3 つだけであることを保証しますclass Door。これが型システムの得意とするところです。コンパイル時にクラス全体のエラーを明示的に排除します。

問題nullは、すべての参照型が、通常は望ましくない空間でこの余分な状態を取得することです。変数は、任意のstring文字列である可能性があります。またはnull、問題のドメインにマップされないこの狂った余分な値である可能性があります。Triangleオブジェクトには 3 つのPoints があり、それ自体にXY値がありますが、残念ながら、Points またはTriangleそれ自体は、私が取り組んでいるグラフ作成ドメインにとって意味のないこのクレイジーな null 値である可能性があります。

存在しない可能性のある値をモデル化する場合は、明示的にオプトインする必要があります。私が人々をモデル化しようとしている方法が、すべての人Personが aFirstNameと aLastNameを持っているが、一部の人だけがMiddleNames を持っているということである場合、次のように言いたいと思います。

class Person
    private string FirstName
    private Option<string> MiddleName
    private string LastName

ここstringで、null 非許容型であると想定されます。NullReferenceException次に、誰かの名前の長さを計算しようとするときに、確立するのが難しい不変条件や予期しないs はありません。型システムは、 を扱うすべてのコードMiddleNameが である可能性を説明することを保証しますがNone、 を扱うすべてのコードFirstNameはそこに値があると安全に想定できます。

たとえば、上記の型を使用して、このばかげた関数を作成できます。

let TotalNumCharsInPersonsName(p:Person) =
    let middleLen = match p.MiddleName with
                    | None -> 0
                    | Some(s) -> s.Length
    p.FirstName.Length + middleLen + p.LastName.Length

心配なく。対照的に、文字列のような型の null 許容参照を持つ言語では、

class Person
    private string FirstName
    private string MiddleName
    private string LastName

あなたは次のようなものをオーサリングすることになります

let TotalNumCharsInPersonsName(p:Person) =
    p.FirstName.Length + p.MiddleName.Length + p.LastName.Length

入ってくる Person オブジェクトが、すべてが非 null であるという不変式を持っていない場合、これは爆発します。または

let TotalNumCharsInPersonsName(p:Person) =
    (if p.FirstName=null then 0 else p.FirstName.Length)
    + (if p.MiddleName=null then 0 else p.MiddleName.Length)
    + (if p.LastName=null then 0 else p.LastName.Length)

または多分

let TotalNumCharsInPersonsName(p:Person) =
    p.FirstName.Length
    + (if p.MiddleName=null then 0 else p.MiddleName.Length)
    + p.LastName.Length

最初/最後が存在することを保証すると仮定しpますが、中間はnullになる可能性があります。または、さまざまな種類の例外をスローするチェックを行うか、誰が何を知っているかを確認します。これらのクレイジーな実装の選択と考えるべきことはすべて、あなたが望まない、または必要としないこのばかげた表現可能な値があるためです。

Null は通常、不必要な複雑さを追加します。 複雑さはすべてのソフトウェアの敵であり、合理的な範囲で複雑さを軽減するよう努めるべきです。

(これらの単純な例でさえ、より複雑であることに注意してください。たとえ aFirstNameが でなくてもnull、aは (空の文字列)stringを表すことができます。""これはおそらく、モデル化しようとしている人の名前でもありません。 null 許容文字列の場合でも、「意味のない値を表現している」場合があります. 繰り返しますが、実行時に不変条件と条件付きコードを使用するか、型システムを使用して (たとえば、NonEmptyString型を持つ)、これと戦うことを選択できます。後者はおそらく賢明ではありません (「良い」型は、一連の一般的な操作に対して「閉じられている」ことが多く、たとえば、NonEmptyString閉じられていません.SubString(0,0))、しかし、それは計画空間でより多くの点を示しています。結局のところ、特定の型システムには、取り除くのが非常に得意な複雑さと、本質的に取り除くのが難しい他の複雑さがあります。このトピックの重要な点は、ほぼすべての型システムで、「デフォルトで null 許容参照」から「デフォルトで null 非許容参照」への変更は、ほとんどの場合単純な変更であり、複雑さとの戦いにおいて型システムを大幅に改善するということです。特定の種類のエラーや無意味な状態を除外します。したがって、非常に多くの言語がこのエラーを何度も繰り返し続けるのは非常にクレイジーです。)

于 2010-10-21T18:38:59.367 に答える
66

オプション型の良いところは、それらがオプションであることではありません。他のすべてのタイプはそうではないということです。

場合によっては、一種の「null」状態を表現できる必要があります。「値なし」オプションと、変数が取り得る他の値を表す必要がある場合があります。したがって、これを完全に禁止する言語は、少し不自由になります。

しかし、多くの場合、それは必要ありません。そのような「null」状態を許可すると、あいまいさと混乱が生じるだけです。.NET で参照型変数にアクセスするたびに、それが null である可能性があることを考慮する必要があります。

多くの場合、実際には null になることはありません。これは、プログラマがコードを構造化して、それが決して起こらないようにするためです。しかし、コンパイラはそれを確認できず、それを見るたびに、「これは null でしょうか? ここで null をチェックする必要がありますか?」と自問する必要があります。

理想的には、null が意味をなさない多くの場合、許可されるべきではありません

ほとんどすべてが null になる可能性がある .NET では、これを実現するのは困難です。呼び出しているコードの作成者が 100% 規律と一貫性を持ち、null にできるものとできないものを明確に文書化することに頼る必要があります

ただし、型がデフォルトで nullable でない場合は、型が null かどうかを確認する必要はありません。コンパイラ/型チェッカーが強制するため、null になることはありません。

そして、null 状態を処理する必要があるまれなケースに備えて、バックドアが必要です。次に、「オプション」タイプを使用できます。次に、「値なし」のケースを表現できるようにする必要があるという意識的な決定を下した場合に null を許可し、それ以外のすべてのケースでは、値が決して null にならないことを知っています。

他の人が言及したように、たとえば C# や Java では、null は次の 2 つのいずれかを意味します。

  1. 変数は初期化されていません。これは、理想的には決して起こらないはずです。変数は、初期化されない限り存在すべきではありません。
  2. 変数には「オプションの」データが含まれています。データがない場合を表すことができる必要があります。これは時々必要です。おそらく、リスト内のオブジェクトを見つけようとしていて、それがそこにあるかどうかを前もって知らない場合があります。次に、「オブジェクトが見つからなかった」ことを表現できる必要があります。

2 番目の意味は保持する必要がありますが、最初の意味は完全に排除する必要があります。そして、2 番目の意味でさえデフォルトであってはなりません。これは、必要なときにオプトインできるものです。ただし、何かをオプションにする必要がない場合は、型チェッカーでそれが決して null にならないことを保証する必要があります。

于 2010-10-21T19:09:31.417 に答える
45

これまでのすべての回答は、なぜ悪いことなのか、特定の値が決してnullにならないnullことを言語が保証できる場合、どのように便利なのかに焦点を当てています。

さらに、すべての値に対して非 null 可能性を強制すると、非常に優れたアイデアになることが示唆されます。これは、定義された値を常に持っているとは限らない型を表現したり表現しOptionたりするような概念を追加することで実現できます。Maybeこれは、Haskell が採用したアプローチです。

それはすべて良いものです!ただし、同じ効果を得るために、明示的に null 許容型 / 非 null 型を使用することを妨げるものではありません。では、なぜ Option は依然として優れているのでしょうか。結局のところ、Scala は null 許容値をサポートしていますが (そうしなければならないので、Java ライブラリで動作します)、Options同様にサポートしています。

Q.では、言語から null を完全に削除できる以外に、どのような利点がありますか?

A.構成

null 認識コードから素朴な翻訳を行う場合

def fullNameLength(p:Person) = {
  val middleLen =
    if (null == p.middleName)
      p.middleName.length
    else
      0
  p.firstName.length + middleLen + p.lastName.length
}

オプション認識コードへ

def fullNameLength(p:Person) = {
  val middleLen = p.middleName match {
    case Some(x) => x.length
    case _ => 0
  }
  p.firstName.length + middleLen + p.lastName.length
}

あまり違いはありません!しかし、Options を使用するのはひどい方法でもあります...このアプローチははるかにクリーンです:

def fullNameLength(p:Person) = {
  val middleLen = p.middleName map {_.length} getOrElse 0
  p.firstName.length + middleLen + p.lastName.length
}

あるいは:

def fullNameLength(p:Person) =       
  p.firstName.length +
  p.middleName.map{length}.getOrElse(0) +
  p.lastName.length

オプションのリストを扱い始めると、さらに良くなります。peopleリスト自体がオプションであると想像してください。

people flatMap(_ find (_.firstName == "joe")) map (fullNameLength)

これはどのように作動しますか?

//convert an Option[List[Person]] to an Option[S]
//where the function f takes a List[Person] and returns an S
people map f

//find a person named "Joe" in a List[Person].
//returns Some[Person], or None if "Joe" isn't in the list
validPeopleList find (_.firstName == "joe")

//returns None if people is None
//Some(None) if people is valid but doesn't contain Joe
//Some[Some[Person]] if Joe is found
people map (_ find (_.firstName == "joe")) 

//flatten it to return None if people is None or Joe isn't found
//Some[Person] if Joe is found
people flatMap (_ find (_.firstName == "joe")) 

//return Some(length) if the list isn't None and Joe is found
//otherwise return None
people flatMap (_ find (_.firstName == "joe")) map (fullNameLength)

null チェック (または elvis ?: 演算子でさえも) を含む対応するコードは、非常に長くなります。ここでの本当の秘訣は flatMap 操作です。これにより、null 許容値では実現できない方法で、Options とコレクションのネストされた理解が可能になります。

于 2010-10-28T14:41:15.270 に答える
38

人々はそれを見逃しているように見えるので:nullはあいまいです.

アリスの生年月日はnullです。どういう意味ですか?

ボブの死亡日はnullです。どういう意味ですか?

「合理的な」解釈は、アリスの生年月日は存在するが不明であるのに対し、ボブの死亡日は存在しない (ボブはまだ生きている) というものかもしれません。しかし、なぜ異なる答えにたどり着いたのでしょうか?


別の問題:nullエッジ ケースです。

  • ですかnull = null
  • ですかnan = nan
  • ですかinf = inf
  • ですか+0 = -0
  • ですか+0/0 = -0/0

答えは通常、それぞれ「はい」、「いいえ」、「はい」、「はい」、「いいえ」、「はい」です。クレイジーな「数学者」は、NaN を「無効」と呼び、それはそれ自体と等しいと言いました。SQL は null を何にも等しくないものとして扱います (したがって、NaN のように動作します)。±∞、±0、および NaN を同じデータベース列に格納しようとするとどうなるか疑問に思うことがあります (2 53個の NaN があり、その半分は "負" です)。

さらに悪いことに、データベースは NULL の処理方法が異なり、そのほとんどは一貫していません (概要については、SQLite での NULL 処理を参照してください)。それはかなり恐ろしいです。


そして今、義務的な話のために:

最近、5 つの列を持つ (sqlite3) データベース テーブルを設計しましたa NOT NULL, b, id_a, id_b NOT NULL, timestamp。これは、かなり恣意的なアプリの一般的な問題を解決するために設計された一般的なスキーマであるため、次の 2 つの一意性制約があります。

UNIQUE(a, b, id_a)
UNIQUE(a, b, id_b)

id_a既存のアプリ デザインとの互換性のためにのみ存在し (部分的には、より良い解決策を考え出していないため)、新しいアプリでは使用されません。NULL が SQL で機能する方法により、最初の一意性制約を挿入(1, 2, NULL, 3, t)(1, 2, NULL, 4, t)、違反しないようにすることができます (なぜなら(1, 2, NULL) != (1, 2, NULL))。

これは、ほとんどのデータベースの一意性制約で NULL がどのように機能するかによって特に機能します (おそらく、「現実世界」の状況をモデル化する方が簡単です。たとえば、2 人が同じ社会保障番号を持つことはできませんが、すべての人が同じ社会保障番号を持つわけではありません)。


FWIW、最初に未定義の動作を呼び出さないと、C++ 参照は null を「指す」ことができず、初期化されていない参照メンバー変数を使用してクラスを構築することはできません (例外がスローされた場合、構築は失敗します)。

補足: 場合によっては、相互に排他的なポインターが必要になることがあります (つまり、そのうちの 1 つだけが非 NULL になる可能性があります)。たとえば、仮想の iOStype DialogState = NotShown | ShowingActionSheet UIActionSheet | ShowingAlertView UIAlertView | Dismissedなどです。代わりに、私は次のようなことをすることを余儀なくされていますassert((bool)actionSheet + (bool)alertView == 1)

于 2010-10-21T20:03:08.547 に答える
16

デフォルトで参照/ポインターを null 可能にすることの望ましくないこと。

これがnullの主な問題だとは思いません.nullの主な問題は、次の2つのことを意味する可能性があることです。

  1. 参照/ポインターが初期化されていません: ここでの問題は、一般的な可変性と同じです。1 つには、コードの分析がより困難になります。
  2. 変数が null であることは実際には何かを意味します。これは Option 型が実際に形式化する場合です。

Option 型をサポートする言語は、通常、初期化されていない変数の使用も禁止または推奨しません。

パターン マッチングなどの null ケースのチェックを容易にする戦略を含む、オプション タイプのしくみ。

有効にするためには、Option 型を言語で直接サポートする必要があります。そうしないと、それらをシミュレートするために多くの定型コードが必要になります。パターン マッチングと型推論は、Option 型の操作を容易にする 2 つの重要な言語機能です。例えば:

F# の場合:

//first we create the option list, and then filter out all None Option types and 
//map all Some Option types to their values.  See how type-inference shines.
let optionList = [Some(1); Some(2); None; Some(3); None]
optionList |> List.choose id //evaluates to [1;2;3]

//here is a simple pattern-matching example
//which prints "1;2;None;3;None;".
//notice how value is extracted from op during the match
optionList 
|> List.iter (function Some(value) -> printf "%i;" value | None -> printf "None;")

ただし、Option 型を直接サポートしない Java のような言語では、次のようになります。

//here we perform the same filter/map operation as in the F# example.
List<Option<Integer>> optionList = Arrays.asList(new Some<Integer>(1),new Some<Integer>(2),new None<Integer>(),new Some<Integer>(3),new None<Integer>());
List<Integer> filteredList = new ArrayList<Integer>();
for(Option<Integer> op : list)
    if(op instanceof Some)
        filteredList.add(((Some<Integer>)op).getValue());

メッセージを食べる nil などの代替ソリューション

Objective-C の「nil を食べるメッセージ」は、null チェックの頭の痛い問題を軽減する試みとしての解決策ではありません。基本的に、null オブジェクトでメソッドを呼び出そうとすると実行時例外をスローする代わりに、式自体が null に評価されます。不信感をぶちまけて、まるで各インスタンス メソッドが で始まるかのようにif (this == null) return null;。ただし、情報が失われます。有効な戻り値であるためメソッドが null を返したのか、それともオブジェクトが実際に null であるためかはわかりません。これは例外の飲み込みによく似ており、前に概説した null に関する問題への対処は進んでいません。

于 2010-10-21T16:01:18.147 に答える
11

アセンブリは、型指定されていないポインターとも呼ばれるアドレスをもたらしました。C はそれらを型付きポインターとして直接マップしましたが、すべての型付きポインターと互換性のある一意のポインター値として Algol の null を導入しました。C における null の大きな問題は、すべてのポインターが null になる可能性があるため、手動でチェックしないと安全にポインターを使用できないことです。

高水準言語では、null を持つことは厄介です。これは、実際には 2 つの異なる概念を伝えるためです。

  • 何かがundefinedであることを伝える。
  • 何かがオプションであることを伝える。

未定義の変数を持つことはほとんど役に立たず、未定義の動作が発生するたびに発生します。物事を未定義にすることは何としてでも避けるべきだということに誰もが同意すると思います。

2 番目のケースはオプションであり、たとえばオプション typeを使用して明示的に提供するのが最適です。


私たちは運送会社にいて、ドライバーのスケジュールを作成するためのアプリケーションを作成する必要があるとしましょう。ドライバーごとに、所有している運転免許証や緊急時の電話番号など、いくつかの情報を保存します。

C では次のようになります。

struct PhoneNumber { ... };
struct MotorbikeLicence { ... };
struct CarLicence { ... };
struct TruckLicence { ... };

struct Driver {
  char name[32]; /* Null terminated */
  struct PhoneNumber * emergency_phone_number;
  struct MotorbikeLicence * motorbike_licence;
  struct CarLicence * car_licence;
  struct TruckLicence * truck_licence;
};

ご覧のとおり、ドライバーのリストに対する処理では、null ポインターをチェックする必要があります。コンパイラは役に立ちません。プログラムの安全性はあなたの肩にかかっています。

OCaml では、同じコードは次のようになります。

type phone_number = { ... }
type motorbike_licence = { ... }
type car_licence = { ... }
type truck_licence = { ... }

type driver = {
  name: string;
  emergency_phone_number: phone_number option;
  motorbike_licence: motorbike_licence option;
  car_licence: car_licence option;
  truck_licence: truck_licence option;
}

ここで、すべてのドライバーの名前とトラックの免許番号を出力したいとしましょう。

C:

#include <stdio.h>

void print_driver_with_truck_licence_number(struct Driver * driver) {
  /* Check may be redundant but better be safe than sorry */
  if (driver != NULL) {
    printf("driver %s has ", driver->name);
    if (driver->truck_licence != NULL) {
      printf("truck licence %04d-%04d-%08d\n",
        driver->truck_licence->area_code
        driver->truck_licence->year
        driver->truck_licence->num_in_year);
    } else {
      printf("no truck licence\n");
    }
  }
}

void print_drivers_with_truck_licence_numbers(struct Driver ** drivers, int nb) {
  if (drivers != NULL && nb >= 0) {
    int i;
    for (i = 0; i < nb; ++i) {
      struct Driver * driver = drivers[i];
      if (driver) {
        print_driver_with_truck_licence_number(driver);
      } else {
        /* Huh ? We got a null inside the array, meaning it probably got
           corrupt somehow, what do we do ? Ignore ? Assert ? */
      }
    }
  } else {
    /* Caller provided us with erroneous input, what do we do ?
       Ignore ? Assert ? */
  }
}

OCaml では次のようになります。

open Printf

(* Here we are guaranteed to have a driver instance *)
let print_driver_with_truck_licence_number driver =
  printf "driver %s has " driver.name;
  match driver.truck_licence with
    | None ->
        printf "no truck licence\n"
    | Some licence ->
        (* Here we are guaranteed to have a licence *)
        printf "truck licence %04d-%04d-%08d\n"
          licence.area_code
          licence.year
          licence.num_in_year

(* Here we are guaranteed to have a valid list of drivers *)
let print_drivers_with_truck_licence_numbers drivers =
  List.iter print_driver_with_truck_licence_number drivers

この簡単な例でわかるように、安全なバージョンには複雑なことは何もありません:

  • より簡潔です。
  • はるかに優れた保証が得られ、null チェックはまったく必要ありません。
  • コンパイラは、オプションを正しく処理したことを確認しました

一方、C では、null チェックを忘れてブームが発生する可能性があります...

注 : これらのコード サンプルはコンパイルされていませんが、アイデアが得られたことを願っています。

于 2010-10-21T16:50:05.643 に答える
5

Microsoft Researchには、という興味深いプロジェクトがあります。

スペック#

これは、null以外のタイプのC#拡張機能であり、オブジェクトがnullでないかどうかをチェックするメカニズムですが、IMHOでは、契約原則による設計の適用がより適切であり、null参照によって引き起こされる多くの厄介な状況に役立つ場合があります。

于 2010-10-30T21:04:12.103 に答える
4

.NET のバックグラウンドを持っているので、null には意味があり、便利だといつも思っていました。構造体について知り、多くのボイラープレート コードを回避して構造体を操作するのがいかに簡単かを知るまでは。Tony Hoareは 2009 年の QCon London で講演し、null 参照を発明したことを謝罪しました。彼を引用するには:

私はそれを私の10億ドルの間違いと呼んでいます。それは 1965 年のヌル参照の発明でした。その当時、私はオブジェクト指向言語 (ALGOL W) での参照のための最初の包括的な型システムを設計していました。私の目標は、参照のすべての使用が完全に安全であることを保証することであり、チェックはコンパイラによって自動的に実行されます。しかし、実装がとても簡単だったという理由だけで、null 参照を挿入したいという誘惑に抵抗できませんでした。これにより、無数のエラー、脆弱性、およびシステム クラッシュが発生し、過去 40 年間でおそらく 10 億ドルの痛みと損害が発生しました。近年、Microsoft の PREfix や PREfast などの多くのプログラム アナライザーが参照のチェックに使用されており、参照が null でない可能性がある場合は警告が表示されます。Spec# などの最近のプログラミング言語では、非 null 参照の宣言が導入されています。これは、私が 1965 年に拒否した解決策です。

プログラマーでこの質問も参照してください

于 2013-02-03T23:10:07.450 に答える
4

Robert Nystrom は、ここで素晴らしい記事を提供しています。

http://journal.stuffwithstuff.com/2010/08/23/void-null-maybe-and-nothing/

Magpieプログラミング言語に不在と失敗のサポートを追加するときの彼の思考プロセスを説明します。

于 2010-10-21T21:37:23.900 に答える
1

私は常に Null (または nil) をvalue の不在と見なしてきました。

これが必要な場合もあれば、必要でない場合もあります。使用しているドメインによって異なります。不在が意味のある場合 (ミドル ネームがない場合)、アプリケーションはそれに応じて動作します。一方、null 値があってはならない場合: 最初の名前が null の場合、開発者はことわざの午前 2 時に電話を受けます。

また、null のチェックでオーバーロードされ、過度に複雑になっているコードも見てきました。私にとって、これは次の 2 つのいずれかを意味します:
a) アプリケーション ツリーの上位にあるバグ
b) 悪い/不完全な設計

良い面としては、Null はおそらく、何かが存在しないかどうかをチェックするためのより有用な概念の 1 つであり、null の概念を持たない言語は、データ検証を行うときに物事を過度に複雑にすることになります。この場合、新しい変数が初期化されていない場合、言語は通常、変数を空の文字列、0、または空のコレクションに設定します。ただし、空の文字列、0、または空のコレクションがアプリケーションの有効な値である場合は、問題があります。

初期化されていない状態を表すために、フィールドに特別な/奇妙な値を発明することで、これを回避することが時々ありました。しかし、善意のユーザーが特別な値を入力するとどうなるでしょうか? そして、これによってデータ検証ルーチンが混乱することは避けましょう。言語が null の概念をサポートしていれば、すべての問題は解消されます。

于 2010-10-25T20:53:01.777 に答える
0

ベクトル言語は、null を持たなくてもうまくいく場合があります。

この場合、空ベクトルは型指定された null として機能します。

于 2010-10-21T20:30:21.550 に答える