ヌルポインターとヌルオブジェクトパターン


27

帰属:これは関連するP.SEの質問から生まれました

私のバックグラウンドはC / C ++ですが、Javaでかなりの仕事をしており、現在C#をコーディングしています。私のCのバックグラウンドのために、渡されたポインターと返されたポインターのチェックは間接的ですが、私はそれが私の視点にバイアスをかけることを認めています。

私は最近、オブジェクトが常に返されるという考え方のNull Object Patternの言及を見ました。通常の場合は、予想されるデータが設定されたオブジェクトを返し、エラーの場合は、nullポインターの代わりに空のオブジェクトを返します。呼び出し元の関数には常にアクセスするオブジェクトがあるため、NULLアクセスメモリ違反を回避するという前提があります。

  • それでは、Null Object Patternを使用する場合とNullチェックを行う場合の長所と短所は何ですか?

NOPを使用すると、よりクリーンな呼び出しコードを見ることができますが、それ以外の場合には発生しない隠れた障害がどこで発生するかを確認することもできます。サイレントミスが野生に逃げるのではなく、開発中にアプリケーションをハードに失敗させます(別名例外)。

  • Null Object Patternには、nullチェックを実行しないのと同様の問題はありませんか?

私が扱ったオブジェクトの多くは、独自のオブジェクトまたはコンテナを保持しています。メインオブジェクトのすべてのコンテナに独自の空のオブジェクトがあることを保証するために、特別なケースが必要になるようです。ネストの複数の層でこれがwithいように思える。


「エラーケース」ではなく、「すべてのケースで有効なオブジェクト」。

@ThorbjørnRavnAndersen-良い点、そしてそれを反映するために質問を編集しました。まだNOPの前提を見逃しているのかどうか教えてください。

6
簡単な答えは、「ヌルオブジェクトパターン」は誤った名前であるということです。より正確には、「ヌルオブジェクトアンチパターン」と呼ばれます。通常、「エラーオブジェクト」を使用する方が適切です。これは、クラスのインターフェイス全体を実装する有効なオブジェクトですが、これを使用すると、大声で突然死に至ります。
ジェリーコフィン

1
@JerryCoffinそのケースは例外によって示されるべきです。アイデアは、決して nullを取得できないため、nullをチェックする必要がないということです。

1
私はこの「パターン」が好きではありません。しかし、おそらく、すべての外部キーが完全にnullでない最終更新前に、編集可能なデータのステージング中に外部キーの特別な「null行」を参照する方が簡単な過剰制約リレーショナルデータベースに根ざしています。

回答:


24

致命的な障害が発生したため、null(またはNull Object)が返される場所ではNull Object Patternを使用しません。それらの場所では、nullを返し続けます。場合によっては、回復がなければ、少なくともクラッシュダンプは問題が発生した場所を正確に示すため、クラッシュする可能性があります。そのような場合、独自のエラー処理を追加すると、プロセスを強制終了します(再び、回復できない場合についても言いました)が、エラー処理はクラッシュダンプが提供する非常に重要な情報をマスクします。

Null Object Patternは、オブジェクトが見つからない場合に取られるデフォルトの動作がある場所に適しています。たとえば、次のことを考慮してください。

User* pUser = GetUser( "Bob" );

if( pUser )
{
    pUser->SetAddress( "123 Fake St." );
}

NOPを使用する場合、次のように記述します。

GetUser( "Bob" )->SetAddress( "123 Fake St." );

このコードの動作は「ボブが存在する場合、彼のアドレスを更新したい」ことに注意してください。明らかに、アプリケーションでBobの存在が必要な場合は、黙って成功したくありません。しかし、このタイプの動作が適切な場合があります。そして、そのような場合、NOPははるかに簡潔で簡潔なコードを生成しませんか?

ボブなしでは本当に生きられない場所では、GetUser()にアプリケーション例外(つまり、アクセス違反などではない)を投げさせて、より高いレベルで処理し、一般的な操作の失敗を報告します。この場合、NOPの必要はありませんが、NULLを明示的にチェックする必要もありません。IMO、これらのNULLのチェックは、コードを大きくするだけで読みやすくします。一部のインターフェイスでは、NULLの確認が依然として適切な設計上の選択肢ですが、一部の人々が考えるほど多くはありません。


4
nullオブジェクトパターンのユースケースに同意します。しかし、壊滅的な障害が発生したときにnullを返すのはなぜですか?例外をスローしないのはなぜですか?
Apoorv Khurasia

2
@MonsterTruck:その逆。コードを書くときは、外部コンポーネントから受け取った入力の大部分を検証するようにします。ただし、内部クラス間で1つのクラスの関数がNULLを返さないようにコードを記述した場合、呼び出し側では「より安全に」するためだけにNULLチェックを追加しません。値がNULLになる可能性がある唯一の方法は、アプリケーションで壊滅的な論理エラーが発生した場合です。その場合、素敵なクラッシュダンプファイルを取得し、スタックとオブジェクトの状態を逆にして論理エラーを特定するため、プログラムをその場で正確にクラッシュさせます。
DXM

2
@MonsterTruck:「逆に」という意味では、壊滅的なエラーを特定したときにNULLを明示的に返さないでください。ただし、予期しない壊滅的なエラーが原因でのみNULLが発生することを想定しています。
DXM

これで壊滅的なエラー、つまりオブジェクトの割り当て自体が失敗する原因とは何かを理解できました。右?編集:ニックネームの長さが3文字しかないため、@ DXMを実行できません。
Apoorv Khurasia

@MonsterTruck:「@」のことについて奇妙です。私は他の人が以前にそれをやったことを知っています。彼らは最近ウェブサイトを微調整したのだろうか。割り当てである必要はありません。クラスを記述し、APIの意図が常に有効なオブジェクトを返すことである場合、呼び出し側にnullチェックを行わせません。しかし、壊滅的なエラーは、プログラマーエラー(NULLが返される原因となるタイプミス)、またはオブジェクトをその時間より前に消去する競合状態、またはおそらくスタック破損のようなものです。基本的に、NULLが返される原因となった予期しないコードパス。それが起こるときあなたが望む
...-DXM

7

それでは、Null Object Patternを使用する場合とNullチェックを行う場合の長所と短所は何ですか?

長所

  • nullチェックはより多くのケースを解決するため、より適切です。すべてのオブジェクトに適切なデフォルトまたはノーオペレーションの動作があるわけではありません。
  • nullチェックはより強固です。健全なデフォルトを持つオブジェクトでさえ、健全なデフォルトが無効な場所で使用されます。失敗する場合、コードは根本原因の近くで失敗するはずです。失敗する場合、コードは明らかに失敗するはずです。

短所

  • 正常なデフォルトは通常、よりクリーンなコードになります。
  • 正常なデフォルトは、それらがワイルドになった場合、通常、壊滅的なエラーが少なくなります。

この最後の「プロ」は、(私の経験では)それぞれを適用すべきときの主な差別化要因です。「失敗は騒々しいでしょうか?」。場合によっては、障害をハードかつ即時にしたいことがあります。どういうわけか決して起こらないはずのシナリオが何らかの形で起こった場合。重要なリソースが見つからなかった場合など...場合によっては、実際にはエラーではないため、正常なデフォルトで問題ありません。たとえば、辞書から値を取得しても、キーがありません。

他の設計上の決定と同様に、ニーズに応じて利点と欠点があります。


1
長所セクション:最初の点に同意します。2番目のポイントは、設計者のせいです。なぜなら、nullであっても使用すべきでない場所で使用できるからです。パターンについて悪いことを言っていないだけで、パターンを誤って使用した開発者に反映されます。
Apoorv Khurasia

より深刻な障害は何ですか?送金できない、または受取人が明示的なチェックの前にそれを知らないまま送金できないため、送金できなくなりますか?
user470365

短所:正しいデフォルトを使用して、新しいオブジェクトを作成できます。null以外のオブジェクトが返された場合、別のオブジェクトを作成するときにその属性の一部を変更できます。nullオブジェクトが返された場合、それを使用して新しいオブジェクトを作成できます。どちらの場合でも、同じコードが機能します。コピー変更用と新しいコピー用に別々のコードは必要ありません。これは、すべてのコード行がバグの別の場所であるという哲学の一部です。行を減らすとバグが減ります。
shawnhcorey

@shawnhcorey-何?
Telastyn

nullオブジェクトは、実際のオブジェクトであるかのように使用可能でなければなりません。たとえば、IEEEのNaN(数値ではない)を考えてみましょう。方程式がうまくいかない場合は、返されます。また、それを操作するとNaNが返されるため、さらに計算するために使用できます。算術演算のたびにNaNをチェックする必要がないため、コードが簡素化されます。
shawnhcorey

6

私は客観的な情報の良い情報源ではありませんが、主観的には:

  • 私のシステムが死ぬことを望まない。私にとっては、システムの設計が不十分であるか、不完全なシステムの兆候です。完全なシステムは、考えられるすべてのケースを処理し、予期しない状態になることはありません。これを実現するには、すべてのフローと状況を明示的にモデル化する必要があります。ヌルを使用することは、とにかく明示的ではなく、1つのウムレラの下にさまざまなケースをグループ化します。NonExistingUserオブジェクトをnullよりも、NaNをnullを好む。このようなアプローチにより、これらの状況とその処理を必要なだけ詳細に明示できますが、nullではnullオプションのみが残ります。Javaのような環境では、どのオブジェクトもnullになる可能性があるので、一般的なケースのプールで特定のケースも隠すのはなぜですか?
  • null実装の動作はオブジェクトとは大幅に異なり、ほとんどの場合、すべてのオブジェクトのセットに接着されます(理由、理由、理由)。私にとって、このような劇的な違いは、明示的に処理されない限り、システムが死ぬ可能性が最も高いケースを示すヌルの設計の結果のようです(多くの人が実際に彼らのシステムが死ぬことを好むので、私は驚いています); 私にとってそれはルートの欠陥のあるアプローチです-明示的に気をつけない限り、デフォルトでシステムが死ぬことを可能にします、それは非常に安全ではありませんか?しかし、いずれの場合でも、null値とオブジェクトを同じ方法で処理するコードを書くことは不可能になります-基本的な組み込み演算子(assign、equal、paramなど)はわずかしか機能せず、他のすべてのコードは失敗します。 2つの完全に異なるパス。
  • Null Objectを使用すると、より適切にスケーリングされます。必要なだけの情報と構造をNullオブジェクトに入れることができます。NonExistingUserは非常に良い例です-受信しようとしたユーザーの電子メールを含むことができ、その情報に基づいて新しいユーザーを作成することを提案できますが、nullソリューションでは、人々がアクセスしようとした電子メールを保持する方法を考える必要がありますnull結果処理に近い;

JavaやC#などの環境では、明示性の主な利点は得られません-ソリューションの安全性が完全であるため、システム内のオブジェクトの代わりにnullを受け取ることができ、Javaには手段がありません(Beanのカスタムアノテーションを除く)など)を明示的なifs以外から保護します。しかし、null実装のないシステムでは、問題解決にそのような方法で(例外的なケースを表すオブジェクトを使用して)アプローチすると、前述のすべての利点が得られます。だから、主流の言語以外のことを読んでみてください。そうすれば、コーディングの方法が変わってしまうでしょう。

一般に、Null / Errorオブジェクト/戻り値の型は非常に堅実なエラー処理戦略であり、数学的に非常に健全であると考えられますが、JavaまたはCの例外処理戦略は「die ASAP」戦略の1つ上のステップのようになります。開発者またはメンテナーのシステムの不安定性および予測不能性。

この戦略よりも優れていると考えられるモナドもあります(最初は理解の複雑さにおいても優れています)が、型の外部の側面をモデル化する必要がある場合については、Nullオブジェクトが内部の側面をモデル化しています。したがって、おそらくNonExistingUserは、monad maybe_existingとしてより適切にモデル化されます。

そして最後に、Null Objectパターンとエラー状態を静かに処理することとの間に関係があるとは思いません。それは他の方法であるべきです-それはあなたにそれぞれのエラーケースを明示的にモデル化し、それに応じてそれを処理することを強制する必要があります。もちろん、あなたは(どんな理由であれ)ずさんで、エラーケースを明示的に処理することをスキップするが、一般的にそれを処理することを決めるかもしれません-それはあなたの明示的な選択でした。


9
予期せぬことが起こったときにアプリケーションが失敗するのが好きな理由は、予期せぬことが起こったことです。どうやってそうなった?どうして?クラッシュダンプを取得し、潜在的に危険な問題を調査して修正することができます。コードで非表示にするのではありません。C#では、当然、予期しない例外をすべてキャッチしてアプリケーションを回復しようとしますが、それでも問題が発生した場所をログに記録し、それが別の処理を必要とするものかどうかを確認しますこのエラーをログに記録します。実際に予想され、回復可能です」)。
ルアーン

3

Nullオブジェクトの実行可能性は、言語が静的に入力されているかどうかによって少し異なるように見えることに注意するのは興味深いと思います。Rubyは、Nullの(またはNil言語の用語で)オブジェクトを実装する言語です。

初期化されていないメモリへのポインタを戻す代わりに、言語はオブジェクトを返しますNil。これは、言語が動的に型付けされるという事実によって促進されます。関数は常にを返すとは限りませんint。それがNilする必要がある場合、それは戻ることができます。静的に型付けされた言語でこれを行うことはより複雑で困難になります。これは、nullが参照によって呼び出される可能性のあるすべてのオブジェクトに適用できることを当然期待しているためです。nullになる可能性のあるオブジェクトのnullバージョンの実装を開始する必要があります(またはScala / Haskellルートに進み、何らかの「Maybe」ラッパーを使用します)。

MrListerは、C ++では、最初に作成したときに参照/ポインターの初期化が保証されないという事実を持ち出しました。生のnullポインタの代わりに専用のnullオブジェクトを持つことにより、いくつかの素晴らしい追加機能を得ることができます。RubyのはNil含まれてto_s空白のテキストでの結果という(のtoString)関数を。これは、文字列でnullが返されることを気にせず、クラッシュせずにレポートをスキップする場合に便利です。また、オープンクラスを使用するとNil、必要に応じて出力を手動でオーバーライドできます。

確かにこれはすべて一粒の塩でとることができます。動的言語では、nullオブジェクトは正しく使用すると非常に便利になると思います。残念ながら、それを最大限に活用するには、その基本機能を編集する必要がある場合があります。これにより、より標準的なフォームで作業することに慣れている人々にとって、プログラムの理解と保守が難しくなります。


1

いくつかの追加の視点については、Objective-Cをご覧ください。Nullオブジェクトの代わりに、値nil kind-ofがオブジェクトのように機能します。nilオブジェクトに送信されたメッセージは効果がなく、メソッドの戻り値の型が何であれ、0 / 0.0 / NO(false)/ NULL / nilの値を返します。これは、MacOS XおよびiOSプログラミングで非常に一般的なパターンとして使用され、通常、0を返すnilオブジェクトが妥当な結果になるようにメソッドが設計されます。

たとえば、文字列に1つ以上の文字が含まれているかどうかを確認する場合は、次のように記述できます。

aString.length > 0

aString == nilの場合、存在しない文字列には文字が含まれないため、妥当な結果が得られます。人々はそれに慣れるのに時間がかかる傾向がありますが、他の言語のどこでもnil値をチェックすることに慣れるにはおそらく時間がかかるでしょう。

(クラスNSNullのシングルトンオブジェクトもありますが、動作はまったく異なり、実際にはあまり使用されません)。


Objective-Cは、Null Object / Null Pointerを本当にぼやけさせました。nil取得#define「NULLとNULLのオブジェクトのように振る舞うのdは。これはランタイム機能であり、C#/ JavaではNULLポインターがエラーを出力します。
マックスソンチャン

0

回避できる場合は、どちらも使用しないでください。オプションタイプ、タプル、または専用の合計タイプを返すファクトリ関数を使用します。つまり、デフォルトとならないことが保証されている値とは異なるタイプで、デフォルトとなる可能性がある値を表します。

最初に、desiderataをリストしてから、これをいくつかの言語(C ++、OCaml、Python)でどのように表現できるかを考えてみましょう。

  1. コンパイル時にデフォルトオブジェクトの不要な使用をキャッチします。
  2. コードの読み取り中に、指定された値が潜在的にデフォルトであるかどうかを明確にします。
  3. 該当する場合、タイプごとに1回、適切なデフォルト値を選択します。すべての呼び出しサイトで潜在的に異なるデフォルトを絶対に選ばないでください。
  4. 静的分析ツールまたは人間がgrep潜在的な間違いを簡単に探し出すことができるようにします。
  5. 一部のアプリケーションでは、予期しないデフォルト値が与えられた場合、プログラムは正常に続行します。他のアプリケーションの場合、デフォルト値が与えられた場合、理想的には有益な方法で、プログラムは直ちに停止する必要があります。

Null Object Patternとnullポインターの間の緊張は(5)に起因すると思います。ただし、エラーを十分早期に検出できれば、(5)は意味がなくなります。

この言語を言語ごとに考えてみましょう。

C ++

私の意見では、C ++クラスは、ライブラリとのやり取りを容易にし、コンテナ内でクラスを使用しやすくするため、通常はデフォルトで構築可能である必要があります。また、どのスーパークラスコンストラクターを呼び出すかを考える必要がないため、継承が簡素化されます。

ただし、これは、typeの値がMyClass「デフォルト状態」にあるかどうかを確実に知ることができないことを意味します。bool nonempty実行時にデフォルトの状態を表示するために、または同様のフィールドに入力することに加えて、ユーザーにチェックを強いる方法での新しいインスタンスを生成するMyClassことが最善です。

可能であればstd::optional<MyClass>std::pair<bool, MyClass>またはへのr値参照を返すファクトリ関数を使用することをお勧めしますstd::unique_ptr<MyClass>

ファクトリー関数がdefault-constructed MyClassとは異なるある種の「プレースホルダー」状態を返すようにする場合は、a std::pairを使用し、関数がこれを行うことを文書化してください。

ファクトリー関数に一意の名前がある場合、grepその名前を見つけて、だらしのないものを探すのは簡単です。ただし、grepプログラマーがファクトリー関数を使用すべきであるが使用しなかった場合は困難です。

OCaml

OCamlのような言語を使用している場合は、optionタイプ(OCaml標準ライブラリ内)またはeitherタイプ(標準ライブラリ内ではなく、独自のロールを作成しやすい)を使用できます。またはdefaultableタイプ(私は用語を構成していますdefaultable)。

type 'a option =
    | None
    | Some of 'a

type ('a, 'b) either =
    | Left of 'a
    | Right of 'b

type 'a defaultable =
    | Default of 'a
    | Real of 'a

defaultableユーザマストパターンマッチを抽出するため、上記のように、ペアよりも優れている'aと単にペアの最初の要素を無視することはできません。

defaultable上記のタイプと同等のものは、std::variant同じタイプの2つのインスタンスを持つa を使用してC ++で使用std::variantできますが、両方のタイプが同じ場合、APIの一部は使用できません。またstd::variant、その型コンストラクターには名前が付けられていないため、これは奇妙な使用法です。

Python

とにかく、Pythonのコンパイル時チェックは行われません。しかし、動的に型指定されるため、一般に、コンパイラーをなだめるために何らかの型のプレースホルダーインスタンスが必要な状況はありません。

デフォルトのインスタンスを作成せざるを得ない場合は、例外をスローすることをお勧めします。

それが受け入れられない場合は、ファクトリ関数からDefaultedMyClass継承しMyClassて返す関数を作成することをお勧めします。必要に応じて、デフォルトのインスタンスで「スタブアウト」機能に関して柔軟性が得られます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.