本サイトからスピンオフした姉妹サイトです。C#2017年1月10日Cの系譜を継ぐC#ではnullが長らく使い続けられてきたが、最近ではその存在が大きな問題だと認識されている。前後編でこの問題を取り上げ、今回(前編)はnullを取り巻く事情について考察する。mistake(10億ドル規模の損失をもたらす過ち)と呼ばれるくらい忌避されるものになっている。nullは、低コストでそこそこ安全に参照を扱えるという意味で悪くない妥協ではあるが、技術が進歩した現在ではもう少し賢い参照の扱い方があるはずである。C#のように、これまでnullを認めてしまっているプログラミング言語で、今からそれを完全になくすというのは現実的ではないが、nullに起因する問題を少しでも避ける手段はこれからでも追加していけるだろう。今回は、nullが生まれるに至った背景から始め、nullが抱える問題や、nullを避けるに当たっての課題などについて説明していく。nullに対するC#の現在の取り組み状況について触れる。初期化処理とnullnull自体の話の前段階として、変数やフィールドの初期化についての話から始めたい。不定動作メモリというのは確保した時点ではどういう状態になっているか分からず、誰かが適切に初期化しなければ不定な動作を招く。C#ではあまり気にすることはないが、それは.NETランタイムやC#コンパイラーが適切に初期化作業をしてくれているからである。説明のために、あえて不定動作を起こしてみよう。C#でも、unsafeコンテキストでは不定動作を起こすことができる。リスト1のようなコードを書いたとしよう。AllocHGlobalメソッドでメモリ確保したての領域に入っている値を出力している。不定動作を起こす例このコードは実行するたびに異なる値が表示される。確保したてのメモリ領域には決まった値が入っていないのである。この領域が以前使われたときに入っていた値がそのまま残っているが、いつ誰がどう使ったものなのかを知るすべはなく、実質的には不定な値といってよい。この状態を「未初期化」と呼ぶ。安全性の観点からいうと、未初期化領域を残すことはトラブルの原因となる。不定動作なため、たまたまテストをすり抜けてしまったバグが、本番環境で顕在化するといったこともあり得る。話を通常の(safeな)コンテキストに戻そう。通常、C#ではこのような不定動作は起きない。以下の2つのルールがあり、変数やフィールドは必ず初期化されるようになっている。フィールドや配列の要素は0初期化するまず、ローカル変数では、何も代入していない状態の変数から値を読み出すことができない。図1に例を示すように、コンパイラーがフロー解析(=ソースコードの制御フローを追って変数の利用状況を調べる)をして、値を代入していない変数の読み出しがあればコンパイルエラーにする。value)に初期化される。既定値というのは要するに0初期化のことで、0、false、'\0'、nullなど、(C#では)内部表現的には全てのビットが0の値で初期化される。本稿の主題であるnullが出てきたわけである。要するに、nullというのは、不定動作と比べれば0初期化の方がマシという妥協の産物といえる。少なくとも「無効な参照」ということが確実に分かって、決定的に「null参照エラー」を起こせるという点では有益である。パフォーマンス的にも、0初期化であれば耐えられないほどの負担にはならない。有効な値の確実な代入ここで1つの疑問が生じる。フロー解析で確実な代入を調べられるのなら、「有効な値を代入した」というのもフロー解析で調べられるはずである。して、既定値(=0初期化。0が無効な値なこともあり得る)による初期化を必要とするのだろうか。理由は単純で、有効な値で初期化できない場面がどうしても残るからである。有名なものは以下の2つだろう。大き目のバッファー確保は、要するにList<T>クラス(System.Collections.Generic名前空間)などが内部で行っていることである。最低限の説明のために必要な部分を抜き出すと、リスト2のような状態である。今現在使う分だけの配列を作るのでは、Addするたびに配列の確保し直しが発生して、パフォーマンス的にかなり厳しい。そこで、この例のように事前に大き目の配列を作ってしまって、満杯になるまでは同じ配列に値を追加して(配列のサイズを超えたら、そこで新たに配列を確保し直して)いくという手法がとられる。一つの循環参照は、リスト3のような状況である。循環参照の例2つのインスタンスが互いを参照している。2つ目に作ったbの方は、newの時点でRefプロパティに有効な値を渡すことができるが、1つ目のaの方は原理的に不可能である。bが作られるまで、a.Refを有効な値で埋めることはできない。2例ほど紹介したが、これらの状況下でも不定動作は起こさないようにするためにあるのが、既定値(null)による初期化である。後述するが、nullを完全になくそうとすると過剰なコストが発生する場面もある。C#のように現在nullを持ってしまっている言語からnullを取り除くというのは現実的ではない。いえ、nullは多くの場面で必要なく、むしろ「nullが来ることを期待していないのにnullが来る」というバグの原因になっている。少なくとも、nullの許容/拒否(nullability)を区別できる必要があるだろう。nullの許容/拒否は型で表現すべきC#は「静的な型付けの言語」や「コンパイル型の言語」などといわれている。いうタイプのプログラミング言語は、リスト4に示すように、以下のような利点を持っている。コンパイル型・静的型付けの言語の強み。早い段階で間違いが分かるところが、nullの許容/拒否の判定に関しては、上で述べた「静的な型付け」「コンパイル型」の利点から漏れてしまっている。リスト5に示すように、nullの許容/拒否はメソッドシグネチャに表れず、実行してみないとエラーかどうか分からない状態である。こちらはnull拒否。こちらはnull許容。nullの許容/拒否に関してはコンパイル型・静的型付けの言語の強みを生かせていないこれは、静的な型付けの言語としては好ましくない状況である。本来であれば、int型とstring型を区別できるのと同程度に、nullを許可するか拒否するかも型を見て区別できるべきだろう。ちなみに、シグネチャだけ見て分かるというのは結構重要なポイントとなる。コンパイル済みのライブラリだけで(=ソースコードなし)で知ることができる情報はこのシグネチャの部分だけである。Studio上で、メソッドF1、F2を参照すると図2のようなヒントが表示されるが、ここにはメソッドのシグネチャしか表示されない。メソッド呼び出しの際に表示されるヒントそして、シグネチャを見て分からない/実行するまで分からないことによって、「過剰防衛」が発生することも多い。リスト6のように、呼ぶ側と呼ばれる側の両方で同じnullチェックを繰り返すことがある。これは完全に無駄な処理で、nullの許容/拒否が分かりにくいことによって発生するコストである。type)という機能が存在する。int型など、値型と呼ばれる本来はnullがあり得ない型に対してnullの代入を認める機能である。リスト7に示すように、型名の後ろに?(疑問符)を付けることで、「null+本来の値」を代入できる型を作れる。C#のnull許容型これによって、値型の場合にはnullの許容/拒否の区別を型で表現できている。問題は、値型の場合だけでしか表現できていないという点だ。参照型の場合は常にnullを認めてしまっていて、拒否する手段が用意されていない。図3の上段に示すような非対称が発生している。2.0以来ずっといわれ続けている。値型との一貫性を考えると、図3の下段のように、「T」でnullを拒否(non-nullable)、「T?」で許可(nullable)とすべきだろう。挙動を変更するためには、既存ソースコードを壊さないよう、旧仕様(Tのみ)と新仕様(TとT?を区別)の切り替えオプションが必要になる。いう形でオプション指定できるようにするかや、新旧世界に分かれてしまうことの是非などが問われている。まとめnull(要するに0初期化による「不定動作」除け)は、低コストで比較的安全な動作を得られるため、妥協的に重宝されている。その一方で、意図してnullを必要とする場面はそう多くない。意図的にnullを使っているのか、何らかのミスでnullが残っているだけなのかが分からなくて困るといった問題が出ている。C#でもこの問題を解消してほしいという要望はかねてから出ているが、既存ソースコードを壊しかねない問題であるため、慎重な姿勢を示している。いつまでも避けて通れるものではない。1.0がプレビュー版だった頃からC#によるプログラミング入門を公開していて、C#とともに今年で15年になる。最近の自己紹介は「C#でググれ」。※以下では、本稿の前後を合わせて5回分(第9回~第13回)のみ表示しています。INDEX]を参照してください。Task-likeC#の進化の中でも「非同期メソッド」はコーディング方法を大きく変えるほど革新的だったが、そこにはまだ課題もある。7~将来のC#で、非同期処理はどう進化するのか、前後編で見ていこう。Basic)が切り開いた非同期処理の新たな世界。そこにはまだ課題もある。7でどうなるかを見てみよう。null参照問題(前編)Cの系譜を継ぐC#ではnullが長らく使い続けられてきたが、最近ではその存在が大きな問題だと認識されている。前後編でこの問題を取り上げ、今回(前編)はnullを取り巻く事情について考察する。null参照問題(後編)最近のC#ではnullの存在が大きな問題となっている。前回(前編)で説明したnullの事情を踏まえ、今回(後編)は、将来のC#がnullをどう取り扱っていくのかを見ていく。C#への「インターフェースのデフォルト実装」の導入(前編)C#におけるインターフェースとは、ある型が持つべきメソッドを示す「契約」であり、実装は持てない。このことが大きな問題となりつつある。今回から全3回に分けて、C#がこの問題にどう対処しようとしているかを見ていく。機械学習エンジニアを目指す人~初中級者のための技術情報サイトです。「機械学習&AI」「クラウド&ビッグデータ」「IoTデバイス」のネット記事情報を共有中。開発者のためのSlackチーム、作りました:「機械学習&AI」「クラウド&ビッグデータ」「IoTデバイス」のネット記事情報を共有中。2020最適なビューへの切り替えを推奨します:スマートフォン用表示に。