Tony Hoareの 10億ドルの間違い はnull
の発明でした。その後、ソフトウェア開発者が初期化されていない変数を使用(逆参照)しようとすると、多くのコードがnullポインター例外(segfaults)で謎に陥ります。
1989年、 Wirfs-Brock and Wikerson は次のように書いています:
変数を直接参照すると、既存のクラスを洗練するプログラマの能力が大幅に制限されます。ここで説明するプログラミング規則は、再利用可能な設計を促進するための変数の使用を構造化しています。すべてのオブジェクト指向言語のユーザーは、これらの規則に従うことをお勧めします。さらに、オブジェクト指向言語の設計者には、無制限の変数参照が再利用性に与える影響を考慮することを強くお勧めします。
多くのソフトウェアは、特にJavaで、おそらくC#およびC++で、次のパターンを使用することがよくあります。
_public class SomeClass {
private String someAttribute;
public SomeClass() {
this.someAttribute = "Some Value";
}
public void someMethod() {
if( this.someAttribute.equals( "Some Value" ) ) {
// do something...
}
}
public void setAttribute( String s ) {
this.someAttribute = s;
}
public String getAttribute() {
return this.someAttribute;
}
}
_
コードベース全体でnull
をチェックすることにより、バンドエイドソリューションが使用される場合があります。
_ public void someMethod() {
assert this.someAttribute != null;
if( this.someAttribute.equals( "Some Value" ) ) {
// do something...
}
}
public void anotherMethod() {
assert this.someAttribute != null;
if( this.someAttribute.equals( "Some Default Value" ) ) {
// do something...
}
}
_
バンドエイドは、常にヌルポインターの問題を回避するわけではありません。競合状態が存在します。競合状態は以下を使用して軽減されます。
_ public void anotherMethod() {
String someAttribute = this.someAttribute;
assert someAttribute != null;
if( someAttribute.equals( "Some Default Value" ) ) {
// do something...
}
}
_
それでも、クラススコープの変数を使用して有効であることを確認するたびに、2つのステートメント(ローカルコピーへの割り当てとnull
のチェック)が必要です。
ケンアウアーの自己カプセル化による再利用性(プログラム設計のパターン言語、アディソンウェスリー、ニューヨーク、pp。505-516、1994)は自己カプセル化を提唱しました遅延初期化と組み合わせる。結果は、Javaでは次のようになります。
_public class SomeClass {
private String someAttribute;
public SomeClass() {
setAttribute( "Some Value" );
}
public void someMethod() {
if( getAttribute().equals( "Some Value" ) ) {
// do something...
}
}
public void setAttribute( String s ) {
this.someAttribute = s;
}
public String getAttribute() {
String someAttribute = this.someAttribute;
if( someAttribute == null ) {
someAttribute = createDefaultValue();
setAttribute( someAttribute );
}
return someAttribute;
}
protected String createDefaultValue() { return "Some Default Value"; }
}
_
null
の重複チェックはすべて不要です。getAttribute()
は、値が含まれるクラス内の単一の場所でnull
にならないようにします。
効率性の引数はかなり根拠のないものである必要があります。最新のコンパイラと仮想マシンは、可能な場合にコードをインライン化できます。
変数が直接参照されない限り、これにより Open-Closed Principle を適切に適用することもできます。
もしあれば、自己カプセル化の欠点は何ですか?
(理想的には、自己カプセル化を使用したり使用しない、同様に複雑なシステムの堅牢性を対比する研究への参照を参照したいのですが、これはかなり簡単なテスト可能な仮説として私を襲います。)
不利な点は、指摘したように追加の間接参照の非効率性と、コンパイラーが強制しないという事実です。必要なのは、カプセル化されていない1つの参照を使用して、最悪のプログラマがメリットを破壊することだけです。
また、nullポインターの問題を解決する正しい方法は、本質的に同じ特性を持つnull以外のデフォルト値に置き換えることではありません。 nullポインター逆参照の問題は、セグメンテーション違反が発生することではありません。それは単なる症状です。問題は、プログラマーが予期しないデフォルト/初期化されていない値を常に処理するとは限らないことです。この問題は、引き続き自己カプセル化パターンで個別に処理する必要があります。
Nullポインターの問題を解決する正しい方法は、意味的に有効な非null値を属性に入れることができるまでオブジェクトを作成せず、属性をnullに設定する必要がある前にオブジェクトを破棄することです。ポインタがnullになる可能性がまったくない場合は、チェックする必要はありません。
通常、属性はnullである必要があると考える場合、1つのクラスで多くのことを実行しようとしています。多くの場合、コードを2つのクラスに分割すると、コードがよりクリーンになります。関数を分割して、null割り当てを回避することもできます。 こちら 別の質問の例。問題のあるnull割り当てを回避するために関数をリファクタリングしました。
不変オブジェクトへの参照を保持するが、動作がas不変オブジェクトであるデータ型を定義できると便利な場合があります。なので:
thing.foo(bar);
静的メソッドの呼び出しとしてコンパイルされます:
classOfThing.do_foo(thing, bar);
ここで、静的メソッドは、最初の引数がnull
である場合を、適切と思われる方法で処理できます。 Javaの多くの文字列処理コードは、String
がそのような型である場合、よりクリーンである可能性があります。その場合、string
の初期化されていない変数は、 null参照ではなく空の文字列。そのような型とObject
の間の変換は少しトリッキーだったかもしれません[このような型はそれぞれ、その型のインスタンスのデフォルト値を表すシングルトンオブジェクトを定義できるため、デフォルトの変換-valued string
からタイプObject
またはString
への参照はString.defaultInstance
への参照を生成し、null参照のString
への変換はNPEを生成します]しかし、そのようなタイプは、いくつかのことをはるかにきれいにすることができた。