Diese Präsentation wurde erfolgreich gemeldet.
Wir verwenden Ihre LinkedIn Profilangaben und Informationen zu Ihren Aktivitäten, um Anzeigen zu personalisieren und Ihnen relevantere Inhalte anzuzeigen. Sie können Ihre Anzeigeneinstellungen jederzeit ändern.

C# 8.0 null許容参照型

1.052 Aufrufe

Veröffentlicht am

Visual Studio Users Community Japan #1 にて登壇。
https://vsuc.connpass.com/event/143114/

C# 8.0の目玉機能の1つ、null許容参照型について解説。

Veröffentlicht in: Software
  • Als Erste(r) kommentieren

C# 8.0 null許容参照型

  1. 1. C# 8.0 null許容参照型 岩永 信之
  2. 2. 今日の話 • C# 8.0と堅牢性 • そもそもnullはなぜあって、何が問題か • null許容参照型
  3. 3. C# 8.0 本題の前にさらっと
  4. 4. Robustness (堅牢性) • C# 8.0の大きなテーマは堅牢性の向上 • コンパイラーによるチェックの強化で人的ミスを減らす • 今日の本題(null許容参照型)もその一環
  5. 5. 例えば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個
  6. 6. 例えばswitch • パターン マッチングの拡充 • 網羅性チェックも賢くなってる static int CompareTo(int? x, int? y) => (x, y) switch { (null, null) => 0, (null, { }) => -1, ({ }, null) => 1, }; パターン足りてないよ?(警告) (足りてないものが来た ときには実行時例外)
  7. 7. 例えばswitch • パターン マッチングの拡充 • 網羅性チェックも賢くなってる static int CompareTo(int? x, int? y) => (x, y) switch { (null, null) => 0, (null, null) => -1, ({ }, null) => 1, ({ } x1, { } y1) => x1.CompareTo(y1), }; パターン被ってるよ?(エラー)
  8. 8. 例えばswitch • パターン マッチングの拡充 • 網羅性チェックも賢くなってる static int CompareTo(int? x, int? y) => (x, y) switch { (null, null) => 0, (null, { }) => -1, ({ }, null) => 1, ({ } x1, {} y1) => x1.CompareTo(y1), }; 正しくはこう
  9. 9. 例えば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; }
  10. 10. 参考 • 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)
  11. 11. null nullとは nullで何が問題になるか
  12. 12. nullとは • null = 無効なことが絶対に保証できるポインター • 未定義動作よりは即死の方がマシ string s = ""; Unsafe.As<string, IntPtr>(ref s) = (IntPtr)123456789; やろうと思えばC#でも不正な場所を参照できる よくわからない適当な値 アクセス違反 未定義動作になる • OSが怒ってくれればまだマシな方 • 下手するとセキュリティホール • ダメなものを読み書きできる
  13. 13. nullとは • null = 無効なことが絶対に保証できるポインター • 未定義動作よりは即死の方がマシ string s = ""; Unsafe.As<string, IntPtr>(ref s) = (IntPtr)0; アドレス0は無効とする よくわからない値よりは よく知った無効な値が好ましい • 無効な場所を参照したことを確実に検知 • セキュリティホールよりはマシ ぬるぽ
  14. 14. (おまけ)読めちゃいけない場所を読む • アクセス違反を即座には起こさない例 • ただし、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と同じ構造の データを用意 そこを参照
  15. 15. 無効だとわかるならそれでいいのか • 例外発生場所と真の原因が遠い 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); 真犯人 事件現場 おまえのせいか? おまえ? やっぱ、おまえ? いや、おまえ? すまん、俺だわ
  16. 16. 過剰nullチェック • 過剰防衛になりがち void A(string s) { if (s != null) B(s); } void B(string s) { if(s != null) Console.WriteLine(s.Length); } nullが来てまずいかどうか、 外から見てわからない だから自衛のためにnullチェック でも実は中でもnullチェックしてた
  17. 17. 過剰nullチェック対策 • 「中身」は変わる可能性がある void B(string s) { if(s != null) Console.WriteLine(s.Length); } シグネチャ (signature) • 外から見える部分 • ここは変えた時点で即「破壊的変更」 中身 (body) • 外からは見えない • ここは変えても使ってる側にコンパイル エラーが出ない
  18. 18. 過剰nullチェック対策 • 「中身」は変わる可能性がある • シグネチャだけ見て「nullかどうか」がわからないとダメ void B(string s) { if(s != null) Console.WriteLine(s.Length); } 見えない 「この s はnullを受け付けます/受け付けません」 という注釈(annotation)が欲しい
  19. 19. null許容性 • メソッドシグネチャにnull許容性の注釈が必要 string A(string s); string? A(string s); string A(string? s); string? A(string? s); nullを 受け付けません nullを 受け付けます nullを 返しません nullを 返します
  20. 20. null許容参照型 概要と有効化方法
  21. 21. 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を受け付ける シグネチャだけでわかる
  22. 22. 有効化(C#ソースコード単位) • #nullable ディレクティブ • #if や #warning と一緒で、そこから下の行に影響 #nullable enable int.Parse(null); #nullable disable int.Parse(null); stringにnullを渡すと警告 (新挙動) 警告なし (従来挙動)
  23. 23. 有効化(プロジェクト単位) • csprojに以下の行を追加 • 将来的に、デフォルトでこの行が入る可能性あり • 既存プロジェクトをうかつに変えると怖いけど、新規プロジェクトなら平気 <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netcoreapp3.0</TargetFramework> <Nullable>enable</Nullable> </PropertyGroup> </Project>
  24. 24. 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 = 忘れてる、気付かない
  25. 25. 注釈のみ、警告のみ • #nullable enable/disableにはさらに以下のオプションあり • annotations : 注釈のみつける • warnings : 警告だけは出す (移行期、ライブラリ作者のために機能分割) void M(string s) { int.Parse(s); string local = null; int.Parse(local); } 例えばこんなコードがあったとして ちなみに、int.Parse の引数はnot null
  26. 26. 注釈のみ、警告のみ(両方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だとわかってても警告対象外
  27. 27. 注釈のみ、警告のみ(両方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なことが わかっているのでここでも警告 新挙動に完全に対応しました
  28. 28. 注釈のみ、警告のみ • #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 シグネチャ部分には影響する 中身には影響しない 一切警告が出ない 差し当たって対応したふり 中身の保証ないけど
  29. 29. 注釈のみ、警告のみ • #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なことが わかっているのでここで警告 対応しきった自信はない 中身の保証はしてるんだけど
  30. 30. フロー解析 nullチェックの掛かり方
  31. 31. フロー解析 • ソースコードの処理の流れ(フロー)を追ってエラーを見つける • C# 1.0時代から、変数の代入漏れのフロー解析あり string s; // 初期化しないまま s を使ったのでエラー。 Console.WriteLine(s.Length);
  32. 32. フロー解析 • ソースコードの処理の流れ(フロー)を追ってエラーを見つける • 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時の 初期化がない
  33. 33. null許容参照型もフロー解析で実装 • ソースコードの流れを追ってnullかどうかを判定 • 出所がnull許容かどうか • nullチェックをしたかどうか string? s; s = "abc"; Console.WriteLine(s.Length); s = null; Console.WriteLine(s.Length); 非nullな値を代入していれば 警告が出なくなる nullを代入すれば 警告が出るようになる null許容で宣言していても
  34. 34. null許容参照型もフロー解析で実装 • ソースコードの流れを追ってnullかどうかを判定 • 出所がnull許容かどうか • nullチェックをしたかどうか var p = typeof(string).GetProperty("Length"); Console.WriteLine(p.PropertyType); 戻り値がnull許容 なので警告が出る
  35. 35. null許容参照型もフロー解析で実装 • ソースコードの流れを追ってnullかどうかを判定 • 出所がnull許容かどうか • nullチェックをしたかどうか var p = typeof(string).GetProperty("Length"); if (p is null) return; Console.WriteLine(p.PropertyType); nullチェックを挟めば 警告が消える
  36. 36. null許容参照型もフロー解析で実装 • ソースコードの流れを追ってnullかどうかを判定 • == でnull許容性が伝搬したり void Equality(string x, string? y) { if (x == y) { Console.WriteLine(y.Length); } else { Console.WriteLine(y.Length); } } 非nullなものと一致 警告なし 警告あり
  37. 37. 注意: 値型と参照型 • 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) { } ⭕ ❌
  38. 38. 注意: 特にジェネリックなとき面倒 • 型引数に対するT? • 型制約なしだとコンパイル エラーに • .NETの型システムのレベルで改修入れない限り無理 void M<T>(T? x) { } Nullable<T> x [Nullable(2)] T x 値型? 参照型?
  39. 39. 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な値型しか受け付けない (他はエラーに)
  40. 40. 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許容参照型は受け付けない(警告)
  41. 41. 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? { } エラー
  42. 42. 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 { } エラー
  43. 43. 注意: 既定値にも甘い • コンストラクターがないと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が来るけど無警告
  44. 44. 後置き ! 演算子 • フロー解析には限界あり • 循環があるときは解析できない • 理屈的に可能としても、コスト的に無理なことも • 徐々に解析できる範囲が広がる可能性はあり • 過剰に警告が出ることが多いので無視する手段が必要 • !を付けると警告抑止 #nullable enable string NotNull = null!; not nullなフィールドにnullを入れられる 「後でちゃんとした値入れるから今は見逃して」 var l = s!.Length; // (s!).Length の意味 var b = s !is null; // (s!) is null の意味 ※ ちょっと誤解されそうな書き方もできるので注意
  45. 45. 関連属性 • T?記法だけでは対応できないものがある • ジェネリックな型 • get/setでnull許容性が違うプロパティ • ref引数で「nullを受け付けるけど返さない」 • TryParseみたいな条件付き非null • 属性で対応
  46. 46. 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を 渡しても平気
  47. 47. 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 こっちは警告
  48. 48. 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があり得る(警告)
  49. 49. 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は返ってこない
  50. 50. 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 (警告)
  51. 51. 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
  52. 52. 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 (警告)
  53. 53. 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保証あり
  54. 54. 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
  55. 55. 特殊対応 • いくつかのメソッドには特別扱いあり • 属性を使って汎用化するほどの要求がないので特殊対応 • 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 の時と同じ ルールでフロー解析 警告なし 警告あり
  56. 56. まとめ • C# 8.0の大きなテーマは堅牢性の向上 • その中でも一番大きいのがnull許容参照型 • 参照型でもTとだけ書くと非null、T?でnull許容 • ?だけでは表現できないもの向けの属性あり • 破壊的変更を避けるためにopt-in • #nullableディレクティブ / Nullableコンパイル オプション • フロー解析で実装 • null許容値型と差があるので注意 • 解析が難しい場面あり • ジェネリクス、既定値など • 回避策として ! 演算子(警告握り潰し)

×