C# 8.0
null許容参照型
岩永 信之
今日の話
• C# 8.0と堅牢性
• そもそもnullはなぜあって、何が問題か
• null許容参照型
C# 8.0
本題の前にさらっと
Robustness (堅牢性)
• C# 8.0の大きなテーマは堅牢性の向上
• コンパイラーによるチェックの強化で人的ミスを減らす
• 今日の本題(null許容参照型)もその一環
例えばrange
• これまで
• C# 8.0
var y = x.Slice(a, b);
aからbまでの意味だっけ?
b自体は含む?その1個手前まで?
aからb個の意味だっけ?
var y = x[a..b];
文法的に意味が確定
• aからbまでの意味
• b自体は含まない
var y = x.Slice(a); aから後ろの意味だっけ?
先頭からa個の意味だっけ?
var y = x[a..]; • aから後ろ
var y = x[..a]; • 先頭からa個
例えばswitch
• パターン マッチングの拡充
• 網羅性チェックも賢くなってる
static int CompareTo(int? x, int? y)
=> (x, y) switch
{
(null, null) => 0,
(null, { }) => -1,
({ }, null) => 1,
};
パターン足りてないよ?(警告)
(足りてないものが来た
ときには実行時例外)
例えばswitch
• パターン マッチングの拡充
• 網羅性チェックも賢くなってる
static int CompareTo(int? x, int? y)
=> (x, y) switch
{
(null, null) => 0,
(null, null) => -1,
({ }, null) => 1,
({ } x1, { } y1) => x1.CompareTo(y1),
};
パターン被ってるよ?(エラー)
例えばswitch
• パターン マッチングの拡充
• 網羅性チェックも賢くなってる
static int CompareTo(int? x, int? y)
=> (x, y) switch
{
(null, null) => 0,
(null, { }) => -1,
({ }, null) => 1,
({ } x1, {} y1) => x1.CompareTo(y1),
};
正しくはこう
例えばswitch
• パターン マッチングの拡充
• そもそもC# 7.3の頃まではこんな書き方になってた
static int CompareTo(int? x, int? y)
{
if (x is int x1)
if (y is int y1) return x1.CompareTo(y1);
else return 1;
else
if (y is int y1) return -1;
else return 0;
}
参考
• Preview版の頃から割と安定していた機能は4月の登壇を参照
Visual Studio 2019 Launch
(https://connpass.com/event/122145/)
C# 8.0 Preview in Visual Studio 2019 (16.0)
(https://www.slideshare.net/ufcpp/c-80-preview-in-visual-studio-2019-160)
null
nullとは
nullで何が問題になるか
nullとは
• null = 無効なことが絶対に保証できるポインター
• 未定義動作よりは即死の方がマシ
string s = "";
Unsafe.As<string, IntPtr>(ref s) = (IntPtr)123456789;
やろうと思えばC#でも不正な場所を参照できる
よくわからない適当な値
アクセス違反 未定義動作になる
• OSが怒ってくれればまだマシな方
• 下手するとセキュリティホール
• ダメなものを読み書きできる
nullとは
• null = 無効なことが絶対に保証できるポインター
• 未定義動作よりは即死の方がマシ
string s = "";
Unsafe.As<string, IntPtr>(ref s) = (IntPtr)0;
アドレス0は無効とする
よくわからない値よりは
よく知った無効な値が好ましい
• 無効な場所を参照したことを確実に検知
• セキュリティホールよりはマシ
ぬるぽ
(おまけ)読めちゃいけない場所を読む
• アクセス違反を即座には起こさない例
• ただし、GCが起きるとぶっ壊れる
• (ExecutionEngineException)
string s = null;
byte* p = stackalloc byte[20];
*(int*)(p + 8) = 3;
*(long*)(p + 12) = 0x0043_0042_0041;
Unsafe.As<string, IntPtr>(ref s) = (IntPtr)(void*)p;
Console.WriteLine(s[0]); // A (U+0041)
Console.WriteLine(s[1]); // B (U+0042)
Console.WriteLine(s[2]); // C (U+0043)
適当にstringと同じ構造の
データを用意
そこを参照
無効だとわかるならそれでいいのか
• 例外発生場所と真の原因が遠い
void A() => B(null);
void B(string s) => C(s);
void C(string s) => D(s);
void D(string s) => E(s);
void E(string s) => F(s);
void F(string s) => Console.WriteLine(s.Length);
真犯人
事件現場
おまえのせいか?
おまえ?
やっぱ、おまえ?
いや、おまえ?
すまん、俺だわ
過剰nullチェック
• 過剰防衛になりがち
void A(string s)
{
if (s != null) B(s);
}
void B(string s)
{
if(s != null) Console.WriteLine(s.Length);
}
nullが来てまずいかどうか、
外から見てわからない
だから自衛のためにnullチェック
でも実は中でもnullチェックしてた
過剰nullチェック対策
• 「中身」は変わる可能性がある
void B(string s)
{
if(s != null)
Console.WriteLine(s.Length);
}
シグネチャ (signature)
• 外から見える部分
• ここは変えた時点で即「破壊的変更」
中身 (body)
• 外からは見えない
• ここは変えても使ってる側にコンパイル
エラーが出ない
過剰nullチェック対策
• 「中身」は変わる可能性がある
• シグネチャだけ見て「nullかどうか」がわからないとダメ
void B(string s)
{
if(s != null)
Console.WriteLine(s.Length);
}
見えない
「この s はnullを受け付けます/受け付けません」
という注釈(annotation)が欲しい
null許容性
• メソッドシグネチャにnull許容性の注釈が必要
string A(string s);
string? A(string s);
string A(string? s);
string? A(string? s);
nullを
受け付けません
nullを
受け付けます
nullを
返しません
nullを
返します
null許容参照型
概要と有効化方法
null許容型
• C# 2.0からnull許容値型がある
• 本来は「無効なポインター」だったものが単に「無効な値」に
• C# 8.0からnull許容参照型ができる
• これまでと「string」の意味が変わる
• 既存コードを壊さないためにオプションで切り替え方式(opt-in)
void A(int x);
void B(int? x);
nullを受け付けない
nullを受け付ける
void A(string x);
void B(string? x);
nullを受け付けない
nullを受け付ける
シグネチャだけでわかる
有効化(C#ソースコード単位)
• #nullable ディレクティブ
• #if や #warning と一緒で、そこから下の行に影響
#nullable enable
int.Parse(null);
#nullable disable
int.Parse(null);
stringにnullを渡すと警告
(新挙動)
警告なし
(従来挙動)
有効化(プロジェクト単位)
• csprojに以下の行を追加
• 将来的に、デフォルトでこの行が入る可能性あり
• 既存プロジェクトをうかつに変えると怖いけど、新規プロジェクトなら平気
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
not null, maybe null, oblivious
• 型には3つの状態ができる
#nullable enable
string x = "";
string? x = null;
#nullable disable
string x = null;
not null: 絶対nullではない
maybe null: nullがあり得る
oblivious※: 無効になってるのでnull
かどうか知りようがない
(違反があっても一切警告が出ない)
※ oblivious = 忘れてる、気付かない
注釈のみ、警告のみ
• #nullable enable/disableにはさらに以下のオプションあり
• annotations : 注釈のみつける
• warnings : 警告だけは出す
(移行期、ライブラリ作者のために機能分割)
void M(string s)
{
int.Parse(s);
string local = null;
int.Parse(local);
}
例えばこんなコードがあったとして
ちなみに、int.Parse
の引数はnot null
注釈のみ、警告のみ(両方disable)
• #nullable enable/disableのオプション
• #nullable disable とだけ書くと両方とも無効化
#nullable disable
void M(string s)
{
int.Parse(s);
string local = null;
int.Parse(local);
}
disable (既存挙動のまま)の時
oblivious
obliviousなので警告対象外
oblivious
nullだとわかってても警告対象外
注釈のみ、警告のみ(両方enable)
• #nullable enable/disableのオプション
• #nullable enable とだけ書くと両方とも有効化
#nullable enable
void M(string s)
{
int.Parse(s);
string local = null;
int.Parse(local);
}
enable の時
not null
not nullをnot nullに渡しているのでOK
nullをnot nullに渡しているので警告
たどっていくとnullなことが
わかっているのでここでも警告
新挙動に完全に対応しました
注釈のみ、警告のみ
• #nullable enable/disableのオプション
• #nullable enable annotations で注釈だけ有効化
• #nullable disable annotations で注釈だけ無効化
#nullable enable annotations
void M(string s)
{
int.Parse(s);
string local = null;
int.Parse(local);
}
enable annotations (注釈だけ付ける)の時
not null シグネチャ部分には影響する
中身には影響しない
一切警告が出ない
差し当たって対応したふり
中身の保証ないけど
注釈のみ、警告のみ
• #nullable enable/disableのオプション
• #nullable enable warnings で警告だけ有効化
• #nullable disable warnings で警告だけ無効化
#nullable enable warnings
void M(string s)
{
int.Parse(s);
string local = null;
int.Parse(local);
}
enable warnings (警告だけ出す)の時
oblivious シグネチャ部分には影響しない
中身には影響する
obliviousなので警告対象外
nullをoblivious渡しても平気
たどっていくとnullなことが
わかっているのでここで警告
対応しきった自信はない
中身の保証はしてるんだけど
フロー解析
nullチェックの掛かり方
フロー解析
• ソースコードの処理の流れ(フロー)を追ってエラーを見つける
• C# 1.0時代から、変数の代入漏れのフロー解析あり
string s;
// 初期化しないまま s を使ったのでエラー。
Console.WriteLine(s.Length);
フロー解析
• ソースコードの処理の流れ(フロー)を追ってエラーを見つける
• C# 1.0時代から、変数の代入漏れのフロー解析あり
• ちゃんと分岐を見る
string s;
if (true) s = "abc";
Console.WriteLine(s.Length);
string s;
if (false) s = "abc";
Console.WriteLine(s.Length);
static void M2(bool flag)
{
string s;
if (flag) s = "abc";
else s = "def";
Console.WriteLine(s.Length);
}
static void M(bool flag)
{
string s;
if (flag) s = "abc";
Console.WriteLine(s.Length);
}
絶対通らない
else時の
初期化がない
null許容参照型もフロー解析で実装
• ソースコードの流れを追ってnullかどうかを判定
• 出所がnull許容かどうか
• nullチェックをしたかどうか
string? s;
s = "abc";
Console.WriteLine(s.Length);
s = null;
Console.WriteLine(s.Length);
非nullな値を代入していれば
警告が出なくなる
nullを代入すれば
警告が出るようになる
null許容で宣言していても
null許容参照型もフロー解析で実装
• ソースコードの流れを追ってnullかどうかを判定
• 出所がnull許容かどうか
• nullチェックをしたかどうか
var p = typeof(string).GetProperty("Length");
Console.WriteLine(p.PropertyType);
戻り値がnull許容
なので警告が出る
null許容参照型もフロー解析で実装
• ソースコードの流れを追ってnullかどうかを判定
• 出所がnull許容かどうか
• nullチェックをしたかどうか
var p = typeof(string).GetProperty("Length");
if (p is null) return;
Console.WriteLine(p.PropertyType);
nullチェックを挟めば
警告が消える
null許容参照型もフロー解析で実装
• ソースコードの流れを追ってnullかどうかを判定
• == でnull許容性が伝搬したり
void Equality(string x, string? y)
{
if (x == y)
{
Console.WriteLine(y.Length);
}
else
{
Console.WriteLine(y.Length);
}
}
非nullなものと一致
警告なし
警告あり
注意: 値型と参照型
• null許容値型は明確に別の型
• Nullable<T>型(System名前空間)
• オーバーロードにも使える
• null許容参照型は単なる注釈
• 型情報的には属性だけの差
• オーバーロードできない
void M(int? x) { }
void M(Nullable<int> x) { }
void M(string? x) { }
void M([Nullable(2)] string x) { }
void M(int x) { }
void M(int? x) { }
void M(string x) { }
void M(string? x) { }
⭕ ❌
注意: 特にジェネリックなとき面倒
• 型引数に対するT?
• 型制約なしだとコンパイル エラーに
• .NETの型システムのレベルで改修入れない限り無理
void M<T>(T? x) { }
Nullable<T> x [Nullable(2)] T x
値型? 参照型?
struct制約
• struct制約 → T?はnull許容値型(C# 2.0の頃からの挙動)
static void M<T>(T? x)
where T : struct { }
static void Main()
{
M<int>(0);
M<int?>(0);
M<string>("");
M<string?>(null);
}
Nullable<T>の意味
非nullな値型しか受け付けない
(他はエラーに)
class制約
• class制約 → not nullな参照型の意味に。T?と書ける
static void M<T>(T? x)
where T : class { }
static void Main()
{
M<int>(0);
M<int?>(0);
M<string>("");
M<string?>(null);
}
[Nullable(2)] Tの意味
値型は受け付けない(エラー)
null許容参照型は受け付けない(警告)
class?制約
• class?制約 → nullableの意味に。T?とは書けない
static void M<T>(T x)
where T : class? { }
static void Main()
{
M<string>("");
M<string?>(null);
}
ちなみに、コンパイル結果は
void M<[Nullable(2)] T>(T x)
OK
static void M<T>(T? x)
where T : class? { }
エラー
notnull制約
• notnull制約 → not nullの意味に。T?とは書けない
static void M<T>(T x)
where T : notnull { }
static void Main()
{
M<int>(0);
M<int?>(0);
M<string>("");
M<string?>(null);
}
ちなみに、コンパイル結果は
void M<[Nullable(1)] T>(T x)
?が付いた型を渡すと警告
static void M<T>(T? x)
where T : notnull { }
エラー
注意: 既定値にも甘い
• コンストラクターがないとnullチェックができない
• 既定値が絡むとnullチェックが漏れてる
struct S { public string Name; }
static int M(S s) => s.Name.Length;
static void Main() => M(default);
例: 構造体のdefault(T)
nullが来るけど無警告
var array = new string[1];
Console.WriteLine(array[0].Length);
例: 配列
nullが来るけど無警告
後置き ! 演算子
• フロー解析には限界あり
• 循環があるときは解析できない
• 理屈的に可能としても、コスト的に無理なことも
• 徐々に解析できる範囲が広がる可能性はあり
• 過剰に警告が出ることが多いので無視する手段が必要
• !を付けると警告抑止
#nullable enable
string NotNull = null!;
not nullなフィールドにnullを入れられる
「後でちゃんとした値入れるから今は見逃して」
var l = s!.Length; // (s!).Length の意味
var b = s !is null; // (s!) is null の意味
※ ちょっと誤解されそうな書き方もできるので注意
関連属性
• T?記法だけでは対応できないものがある
• ジェネリックな型
• get/setでnull許容性が違うプロパティ
• ref引数で「nullを受け付けるけど返さない」
• TryParseみたいな条件付き非null
• 属性で対応
AllowNull
• ?が付いていなくてもnullを受け付ける
public class TextWriter
{
public virtual string NewLine
{
get;
[AllowNull] set;
}
}
例: TextWrite.NewLine (System.IO)
setだけnullable
(nullを渡すとEnvironment.NewLineに置き換える仕様)
var t = new StreamWriter(path);
Console.WriteLine(t.NewLine.Length);
t.NewLine = null;
getはnot null
setはnullを
渡しても平気
DisallowNull
• ?が付いていてもnullを受け付けない
public interface IEqualityComparer<in T>
{
bool Equals([AllowNull] T x, [AllowNull] T y);
int GetHashCode([DisallowNull] T obj);
}
例: IEqualityComparer (System.Collections.Generic)
同じ型引数に対して
メソッドごとにnull許容性が違う
var c = EqualityComparer<string>.Default;
c.Equals("", null);
var h = c.GetHashCode(null); これはOK
こっちは警告
MaybeNull
• ?が付いていなくてもnullを返すことがある
public class Array
{
[return: MaybeNull]
public static T Find<T>(T[] array, Predicate<T> match);
}
例: Array.Find (System)
条件を満たす要素がなかったらdefaultを返す
var array = new[] { "a", "bc" };
var s = Array.Find<string>(array, s => s.Length == 3);
Console.WriteLine(s.Length);
型はstring (not null)でもnullがあり得る(警告)
NotNull
• ?が付いていてもnullを返さない
public class Array
{
public static void Resize<T>([NotNull] ref T[]? array, int newSize);
}
例: Array.Resize (System)
nullを受け付けるけど、
メソッドを抜けるまでに非nullで上書き
int[]? array = null;
Array.Resize(ref array, 1);
Console.WriteLine(array.Length);
nullを渡しても平気
でもnullは返ってこない
MaybeNullWhen
• 戻り値の真偽次第ではmaybe null
例: Dictionary.TryGetValue (System.Collections.Generic)
public class Dictionary<TKey, TValue>
{
public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value);
}
戻り値がfalseの時だけvalueがmaybe null
if (map.TryGetValue(1, out var s)) Console.WriteLine(s.Length);
else Console.WriteLine(s.Length);
戻り値がtrueなのでnot null
戻り値がfalseなのでmaybe null (警告)
NotNullWhen
• 戻り値の真偽次第ではnot null
例: string.IsNullOrEmpty
public class String
{
public bool IsNullOrEmpty([NotNullWhen(false)] string value);
}
戻り値がfalseの時だけvalueがnot null保証あり
if (string.IsNullOrEmpty(s)) Console.WriteLine(s.Length);
else Console.WriteLine(s.Length);
戻り値がtrueなのでmaybe null (警告)
戻り値がfalseなのでnot null
NotNullIfNotNull
• 引数がnot nullの時だけ戻り値がnot null
例: File.GetFileName (System.IO)
※ 命名規約的に
• when : 戻り値に応じて引数を判定
• if : 引数に応じて戻り値を判定
public static class Path
{
[return: NotNullIfNotNull("path")]
public static string GetFileName(string path);
}
path引数のnull許容性がそのまま戻り値に伝搬
var l1 = Path.GetFileName("sample.txt").Length;
var l2 = Path.GetFileName(null).Length;
引数がnullなので戻り値もnull (警告)
DoesNotReturn
• メソッドを呼んだが最後、戻ってこない
例: Environment.FailFast (System)
public static class Environment
{
[DoesNotReturn]
public static void FailFast(string message);
}
プログラムを即停止。絶対戻ってこない
string? s = null;
if (flag) s = "abc";
else Environment.FailFast("fail");
Console.WriteLine(s.Length);
分岐の片方は値の代入あり
もう片方は戻ってこない
結果的にnot null保証あり
DoesNotReturnIf
• 特定の引数でメソッドを呼んだら戻ってこない
例: Debug.Assert (System.Diagnostics)
public static class Debug
{
public static void Assert([DoesNotReturnIf(false)] bool condition)
}
この引数にfalse渡して呼ぶと即停止
Debug.Assert(s != null);
Console.WriteLine(s.Length);
falseだと即停止
s != null が成立しているはず
結果的にsはnot null
特殊対応
• いくつかのメソッドには特別扱いあり
• 属性を使って汎用化するほどの要求がないので特殊対応
• Equals系(object.Equals, IEquatable, IEqualityComparerなど)
void Equality(string x, string? y)
{
if (EqualityComparer<string>.Default.Equals(x, y))
{
Console.WriteLine(y.Length);
}
else
{
Console.WriteLine(y.Length);
}
}
x == y の時と同じ
ルールでフロー解析
警告なし
警告あり
まとめ
• C# 8.0の大きなテーマは堅牢性の向上
• その中でも一番大きいのがnull許容参照型
• 参照型でもTとだけ書くと非null、T?でnull許容
• ?だけでは表現できないもの向けの属性あり
• 破壊的変更を避けるためにopt-in
• #nullableディレクティブ / Nullableコンパイル オプション
• フロー解析で実装
• null許容値型と差があるので注意
• 解析が難しい場面あり
• ジェネリクス、既定値など
• 回避策として ! 演算子(警告握り潰し)

C# 8.0 null許容参照型