LoginSignup
5
6

More than 1 year has passed since last update.

[雑記] nullについて(C#)

Posted at

nullについて

null許容参照型 について整理したかったのですが、C# 7.3 以前のプログラムとの共存問題をどのように手を付けて良いか悩んでしまったので、まずはnullについて雑記としてつらつらと書き記す次第です。
(記事内容は特に整理されたものではありません)

null

参照型の変数において、インスタンスへの参照が設定されていない状態。

参照型と値型

  • 値型 : intbool 等の組み込み値型(単純型とも呼ばれる) や 構造体、列挙型 が値型に分類される。
    値型の変数には値そのものが格納され、C#においては値が格納されていない状態が存在しない(値型には null という状態が無い)。1
    変数に値そのものが格納されているので、変数へ代入を行う度に値が複製される。
  • 参照型 : stringobject の他、クラスやインターフェイスで定義されたいわゆるオブジェクトが参照型に分類される。
    参照型の変数にはオブジェクトの実体(インスタンス)そのものが格納されるわけでは無く、メモリ上に展開されたインスタンスのデータへの参照2が格納される。
    この参照が格納されていない状態を null と呼んでいる。
    変数にはオブジェクトそのものは格納されていないので、オブジェクトが代入の都度複製(クローン)されることは無い(変数に格納された参照のみ複製して格納される)。

nullの説明について

null の説明として「何も無い状態」といった記述を見かけるが、これを「データそのものが存在しない状態」として誤解されるケースが見受けられた(特に新入社員等の若手)。
C#の変数に着目した場合、null と言うのは「データへの参照が無い状態」で、言い換えれば「データへアクセスできない状態」と言った方が良いと思う(ちなみに、Wikipediaでは「何も示さないもの」と紹介されている)。

例えば、以下のようなコードの場合、

var info = new FileInfo(@"略"); // 1
info = null; // 2
  • 1 の時点で、FileInfoクラス のインスタンスが新しく作成され、メモリ上にそのデータが展開される。
    info という変数にはそのインスタンスへの参照が格納される。
  • 2 の時点で、info変数に設定されていたインスタンスへの参照が破棄される(変数はどのインスタンスへの参照も格納されていない状態)。
    この時点では、info変数 を通して 1 で作成されたインスタンスにアクセスすることは出来なくなっているが、インスタンスそのものは確かに存在しているので、厳密に「何も無い状態」になったとは言えない。3

NullReferenceException

参照型の変数を通してインスタンスメンバー(メソッド、プロパティ等)を利用することになる。
変数.メンバー と記述した場合、変数に格納された参照先に存在するインスタンスのメンバーにアクセスすることになるが、変数が null の場合はメンバーを持つインスタンスに対してアクセスが出来ない状態となるので、例外がスローされる。
この時にスローされる例外が NullReferenceException である。4

NullReferenceException をスローさせない為には、変数の状態が null であるか事前に判断する必要があり、これを nullチェック と呼んでいる。

var hoge = /* 略 */; // nullかも知れない参照型変数

if ( hoge == null )
{
    // 変数がnullの場合はメソッドの実行を行わない
}
else
{
    hoge.Exec(); // nullで無い場合のみメソッドを実行する
}

null条件演算子 と null合体演算子

参照型の変数が null の場合に処理の振り分けを行う機能がある。
if文 でも同様の処理が記述できるが、nullか否か に着目するケースではより簡潔に書けるようになる場合がある。

null条件演算子

?. で表される演算子。変数が null であれば、null を返す。

  • 変数?.メンバー
    • 変数null で無い 場合、演算子は 変数.メンバー の結果を返す
    • 変数null の場合、演算子は null を返す

以下の if文 と同様の処理となる。

if ( 変数 != null )
{
    return 変数.メンバー;
}
else
{
    return null;
}

null合体演算子

?? で表される演算子。変数が null であれば、指定した値 を返す。

  • 対象 ?? 指定値
    • 対象null で無い 場合、演算子は 対象 を返す
    • 対象null の場合、演算子は 指定値 を返す
if ( 対象 != null )
{
    return 対象;
}
else
{
    return 指定値;
}

上記を組み合わせる

以下のように組み合わせて使用することも多い。5

  • 変数?.メンバー ?? 指定値
    • 変数null で無い 場合、変数.メンバー を実行する
      • 変数.メンバー の結果が null で無い場合、変数.メンバー の結果を返す
      • 変数.メンバー の結果が null の場合、指定値 を返す
    • 変数null の場合、指定値 を返す

null許容値型

通常、値型は null と言う状態にはならず、フィールド等で明示的な値を代入しなかった場合に初期値(int 型の場合 0)が設定されてしまう。
この仕様により、意図的に初期値と同じ値を代入した場合 と 何ら値を代入していない場合 が区別できず困る場合がある。
あるいは データベース 等の値が格納されていない状態を NULL として区別するアプリケーションを利用する場合に「値型だけどnullと言う状態(値が存在しない状態)」を表現したい場合がある。

これらに null許容値型(Nullable型)が利用できる。
null許容値型は 値型 T に対して T? と記述する(例えば、int型 なら int?)。
null許容値型 は Nullable<T>構造体 と等価で、T?型 と Nullable<T>型 は同じ型の変数となる。
また、構造体(値型) なので null許容値型 の変数そのものが null になることは無い。

Nullable<T>構造体 は以下のプロパティを持ち、これを利用して「nullと言う状態を持つ値型」が定義できる。

  • HasValue : null許容値型 の変数に値が格納されている場合 true、そうで無い場合(null に相当)は false を返す。
  • Value : null許容値型 の変数に格納された値を返す。
    ただし、値が格納されていない場合(HasValue プロパティが false の場合)は InvalidOperationException 例外がスローされる。

null許容参照型

C# 8.0 で導入された新しい概念。
参照型は null という状態が存在し、通常の値型は null非許容値型 と言うならば 参照型は null許容参照型 と言えるので、普通のことを言っているように思える。

この概念は「参照型でも安易に null を許容しない方が良い」と言う考え方に基づき、参照型の変数でも「null を許容しない」ことを基本としていく考え方と認識している。
と言うのも、プログラムを書く上で null の扱いは非常に難しく、先述の nullチェック を適切に行わなければ意図せず例外がスローされることになり、つまりバグを生む原因となりうるからである。
言い換えれば「null であること」を適切に管理しなければバグにつながるので、基本的には参照型の変数であっても null を許容しない方が都合が良いとも考えられる(逆に言えば、null であることを意図する場合においてのみ null を許容する)。

null許容参照型 を導入すると、以下のように 参照型 の扱いが変わる。

  • 参照型 T の変数が null の状態になるコードが警告対象となる。
    • 変数に null を代入した場合(または null となる可能性があるメソッド等の結果を代入した場合)
    • フィールドやプロパティを初期化しない場合
  • 参照型 T に対して T? と記述することで、null許容参照型 を宣言できる。
    • null許容参照型の変数に null を代入しても警告とはならない(従来の参照型と同様)。
  • null の可能性がある参照型変数に対して null チェックせずにそのメンバーにアクセスするコードが警告対象となる。
    • 適切な nullチェック を行うことで警告とならなくなる。
string str; // 参照型の変数
str = null; // null を代入すると警告となる
class Hoge
{
    public string Fuga { get; set; }
    public Hoge()
    {
        // Fuga を初期化しない場合、警告となる
    }
}
string? str; // null許容参照型の変数
str = null;  // null を代入しても警告とならない
string? str = /* 略 */;  // nullの可能性があるメソッド等の戻り値を格納

int i1 = str.Length; // nullに対してメンバーのアクセスを行うと警告

int i2 = default;
if ( str != null )
{
    i2 = str.Length; // nullチェックを行えば警告とならない
}

意図せず null を代入してしまうことや、nullチェック の漏れが警告によって検出されるので非常に便利な機能だが、これを単純に導入してしまうと C# 7.3 以前のコードが警告だらけになる。6
既存のアプリケーションを有効活用しつつ C# 8.0 以降でコーディングする為に null許容参照型 とどのように付き合っていくか悩み中(本当はここをまとめたかった)。

その他、細かいこと

レコード型

C# 9.0 で導入された レコード型(record) は参照型となる。
C# 10.0 で レコード構造体型(record struct) が導入され、これは値型となる。

必須メンバー

C# 11.0 で導入された 必須メンバー(required) は、オブジェクト初期化子で初期値を指定することを強制する仕組み。
初期値が指定されない場合はエラーとなるが、null を指定してもエラーは解消される。
(そのメンバーが null を許容しない参照型であれば、警告は表示される)

まとめ(では無い)

null許容参照型 との自分なりの付き合い方が見つかれば、追記修正するか新たに記事を書き起こすつもりです。

  1. int型 であれば 0bool型 であれば false といったように、明示的に値を格納しない場合の初期値が定められている。

  2. メモリ上のアドレスのような情報。言語によってポインタとも呼ばれる(厳密にはポインタとは異なる概念のようですが)。

  3. このインスタンスについて、どの変数からも参照されていない(インスタンスへの参照が格納された変数が存在しない)状態になった場合、ガベージコレクションによってメモリ上からデータが抹消される。
    この時点で「何も無い状態」になるが、null とは別の概念と考えた方が良い。

  4. Java では同様の例外が NullPointerException にあたり、これを略して「ぬるぽ」と呼ぶことがある。C# なら「ぬるり」とでもなりそうだが、大抵「ぬるぽ」と呼ばれている気がする。

  5. 変数null の場合と 変数.メンバー の結果が null の場合 のいずれも 指定値 を返すことに注意。

  6. C# 7.3 以前では 参照型 T の変数は null を代入することが可能で警告も出なかったが、C# 8.0 以降で単に T型 として変数の宣言をすると null に対しての警告が表示されるようになった為。
    なお、このように既存のコード体系と考え方が変わってしまう為、明示的に null許容参照型 の機能を有効化した場合に限り、警告が出るように設計されている(有効化しなければ C# 7.3 以前のコードが警告だらけになることはない)。

5
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6