null は「値がない」ことを意味します。参照型(クラス、文字列、配列)は昔からそれを保持できました。値型(int、decimal、bool、DateTime、構造体)は、? でnull許容にしない限り保持できません。C#には、欠けているかもしれない値を扱うための小さな演算子の集まりがあります:??、??=、?. です。
null許容値型:int?
値型に ? を付けると null を許すようになります。int? は構造体 Nullable<int> の省略形で、値と、値があるかどうかを示すフラグを保持します。
出力:
False
True
True
5.50
0
3
Rated 4 stars
4
null許容値型の典型的な出どころは、省略可能なフォームの項目、クエリパラメーター、データベースのNULLの列です。DateTime? ShippedAt を持つ Order は、値がnullの間は出荷されていません。代わりに -1 や DateTime.MinValue のような特別な値を使うと、本物のデータのように見え、遅かれ早かれ本物として使われてしまいます。
int? から int への変換は失敗しうるので、明示的にしかできません。そこから、null許容値型が投げる唯一の例外につながります。
「Nullable object must have a value」
値がないときに .Value を読んだり、nullの int? を int にキャストしたりすると、InvalidOperationException が投げられます。
出力:
InvalidOperationException: Nullable object must have a value.
0
0
not stocked
stock is int count はパターンで、値があるかを判定し、1ステップで count に取り出します。HasValue と Value の組み合わせより読みやすくなります。他の形はパターンマッチングのページにあります。
nullとの演算と比較
null許容値型に対する演算子はリフトされます。どちらかのオペランドがnullなら、結果はnullです。順序の比較(<、>、<=、>=)は例外で、nullではなく false を返します。(もう1つの例外は & と | を使う bool? で、null & false は false です。)例を示します。
出力:
True
[]
False
False
True
True
a > 5 と a <= 5 のどちらもfalseなので、a がnullのとき、!(a > 5) は a <= 5 と同じではありません。if (!(age >= 18)) Deny(); のようなコードは、年齢がないことを未成年として扱いますが、それが意図どおりかどうかはわかりません。まず、通常は ?? で、nullが何を意味するかを決めてから比較します。
null合体演算子??
a ?? b は、a がnullでなければ a に、そうでなければ b に評価されます。null許容値型にも参照型にも使え、左から右に連結できます。
出力:
dark
16px
20
ANONYMOUS
右辺は必要なときにだけ評価されるので、cache ?? LoadFromDatabase() は、キャッシュに値があればデータベースにアクセスしません。?? は throw 式も受け付けるので、簡潔なガードになります(throwを参照)。
this.repository = repository ?? throw new ArgumentNullException(nameof(repository));
??=(C# 8)
x ??= y は、x がnullの場合にだけ y を x に代入します。遅延初期化の定番の書き方です。
private List<string> tags;
public void AddTag(string tag)
{
tags ??= new List<string>(); // C# 8 and later
tags.Add(tag);
}
C# 8より前は、tags = tags ?? new List<string>(); か if (tags == null) tags = new List<string>(); と書きます。
null条件演算子?.
a?.Member は、a がnullでない場合にだけ Member を評価し、そうでなければ式全体がnullになります。nullチェックの連鎖を1つの式にします。
出力:
Braga
(no city)
(no customer)
0
True
handler: saved
知っておくべき細部:
- 演算子は連鎖の残りを短絡します。
nobody?.Address?.Cityでは、nobodyがnullになった時点で、右側は何も実行されません。 - メンバーが値型を返す場合、結果はnull許容になります。
Orders?.Countはint?なので、再びintにするには?? 0が必要です。 - C# 14より前は、
?.を代入の対象にできません。それより前のバージョンではcustomer?.Name = "x"はコンパイルできません。 handler?.Invoke(...)は、イベントを発生させたり、省略可能なコールバックを呼んだりする標準的な方法です。
?. はnullが正当な状態である場所で使います。例外を避けるためにあちこちに振りまくと、予期しないnullを生んだバグが隠れ、プログラムは問題のある場所で失敗せずに、データが欠けたまま続行してしまいます。
nullのチェック
if (customer == null) { ... } // classic
if (customer is null) { ... } // C# 7 constant pattern
if (customer != null) { ... }
if (customer is not null) { ... } // C# 9
if (customer is { } c) { ... } // C# 8 property pattern: not null, and named
is null は常に本物のnullチェックを行いますが、== は型に演算子のオーバーロードがあればそれを呼ぶので、出来の悪いオーバーロードによって x == null が嘘をつくことがあります。nullであってはならない引数では、早めに失敗させます。
public void Ship(Order order)
{
ArgumentNullException.ThrowIfNull(order); // .NET 6+
// ...
}
null許容参照型(C# 8)
値型に ? を付けると型が変わります。C# 8以降、参照型に ? を付けると、変わるのはコンパイラが確認することだけです。機能が有効な場合(プロジェクトファイルの <Nullable>enable</Nullable>、.NET 6以降の新しいプロジェクトの既定)、string は「決してnullであるべきでない」、string? は「nullかもしれない」を意味し、コードがそれに合わないとコンパイラが警告します。
#nullable enable
public class User
{
public string Email { get; set; } // warning CS8618: Non-nullable property 'Email'
// must contain a non-null value when exiting constructor.
public string? Nickname { get; set; } // allowed to be null
public int NicknameLength() => Nickname.Length;
// warning CS8602: Dereference of a possibly null reference.
public int SafeLength() => Nickname?.Length ?? 0; // no warning
}
string? input = Console.ReadLine();
string name = input; // warning CS8600: Converting null literal or possible
// null value to non-nullable type.
string checkedName = input ?? "guest"; // no warning
int len = input!.Length; // ! tells the compiler "trust me": no warning
コンパイラはメソッドの中でチェックを追跡します。if (input != null) の後では、input はnullでないとみなされます。!(null免除)演算子は、分析より自分のほうがよく知っている場合に警告を消します。1つ1つがコンパイラには検証できない主張なので、まれにとどめます。
混乱のほとんどを解消する2つの事実があります。第一に、これは警告だけです。プログラムは同じように実行され、注釈のないコードから来た string は実行時にnullを保持することがあります。第二に、string? は Nullable<string> ではなく、HasValue はありません。
よくある間違い
- 確認なしの
.Value。 nullのときInvalidOperationExceptionが投げられます。??、GetValueOrDefault()、is int nを使います。 !(x > 5)がx <= 5を意味すると思う。xがnullならそうではありません。nullとの順序の比較はすべてfalseです。- すべてのnullを
?.で覆う。 症状を消すだけで、失敗を原因から遠ざけます。 - null許容参照の警告を雑音として扱う。 1つ1つが
NullReferenceExceptionを投げうる行を示しています。流れを直すか、?で意図を示します。 - nullの代わりに特別な値を使う。 「不明」を表す
-1、0、DateTime.MinValueは本物のデータに見えます。int?とDateTime?を使います。
よくある質問
C#のint?とはどういう意味ですか?
int? は Nullable<int> の省略形で、null にもなりうる int です。int、decimal、bool、DateTime などの値型は通常は常に値を持っていますが、? を付けると「値がない」状態を持てるようになり、未入力のフォームの項目やデータベースのNULLの列に必要なのがこれです。HasValue か != null で確認し、Value か GetValueOrDefault() で読みます。
C#の??は何をしますか?
a ?? b はnull合体演算子で、a がnullでなければ a に、そうでなければ b に評価されます。string name = input ?? "guest"; は1つの式で代替値を与えます。連結でき(a ?? b ?? c は最初のnullでない値を取ります)、b は必要なときにだけ評価されます。??=(C# 8)は、変数がnullのときだけ右辺を代入します。
C#の?.は何をしますか?
a?.B はnull条件演算子で、a がnullなら式全体がnullになり、B は評価されないので、NullReferenceException は起きません。連結でき(order?.Customer?.Address?.City)、メソッドの呼び出し(logger?.Log(...))、イベント(Changed?.Invoke(...))、インデクサー(items?[0])にも使えます。既定値には ?? と組み合わせます:order?.Customer?.Name ?? "unknown"。
「Nullable object must have a value」の原因は何ですか?
nullであるnull許容値型の .Value を読むか、null非許容の型にキャストする((int)maybe)と、そのメッセージの InvalidOperationException が投げられます。先に HasValue を確認するか、GetValueOrDefault()、?? fallback、またはパターンを使います:if (maybe is int n) { ... }。
C#でnullをチェックするには?
x == null と x is null のどちらも動きます。is null(C# 7)は == をオーバーロードした型の影響を受けないので、多くのコードベースはこちらを好みます。否定は x != null、C# 9では x is not null です。nullの引数を拒否するには、ArgumentNullException.ThrowIfNull(x)(.NET 6)、古いコードでは _ = x ?? throw new ArgumentNullException(nameof(x)); を使います。
C#のnull許容参照型とは何ですか?
C# 8のコンパイラの機能で、<Nullable>enable</Nullable>(.NET 6以降の新しいプロジェクトでは既定)で有効になります。有効にすると、string は「決してnullでない」、string? は「nullかもしれない」を意味し、nullを逆参照するかもしれない場合(CS8602)や、null非許容の変数にnullを代入する場合(CS8600)にコンパイラが警告します。変わるのは警告だけで、実行時の動作は変わりません。