【C#】【EntityFrameworkCore】 null 許容参照型を試す
はじめに
気になりつつも試せていなかった、 null 許容参照型 (nullable reference types) を試してみます。
プロジェクトは前回のものを使います。
Environments
- .NET 5: ver.5.0.100-preview.7.20366.6
- Microsoft.EntityFrameworkCore: ver.5.0.0-preview.7.20365.15
- Microsoft.EntityFrameworkCore.Design: ver.5.0.0-preview.7.20365.15
- Npgsql.EntityFrameworkCore.PostgreSQL: ver.5.0.0-preview7-ci.20200722t163648
- Microsoft.EntityFrameworkCore.Abstractions: ver.5.0.0-preview.7.20365.15
- Microsoft.EntityFrameworkCore.Relational: ver.5.0.0-preview.7.20365.15
- Microsoft.AspNetCore.Mvc.NewtonsoftJson: ver.5.0.0-preview.7.20365.19
準備
null 許容参照型を使うにはプロジェクトの C# バージョンを 8.0 以上にすること、また Nullable を有効にする必要があります。
CodeFirstSample.csproj
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net5.0</TargetFramework> <LangVersion>preview</LangVersion> <Nullable>enable</Nullable> </PropertyGroup> ...
Nullable を有効にすると、大量の Warning が出てきます。
null 許容参照型について
C# 8 以前、 null 許容にできるのは値型だけでした。
int nonNullNumber = 0; nonNullNumber = null; // <- compile error int? nullableNumber = 0; nullableNumber = null; // <- OK // null 許容の値型を通常の値型に代入するにはキャストが必要 nonNullNumber = (int)nullableNumber;
c# 8 以降は参照型についても null になりうるか、そうでないかを区別できるようになりました。
using System; using Models; namespace CSharpEightSample { class Program { static void Main(string[] args) { // Nullable instance User? nullableUser = null; // <- OK // Non-nullable instance User user = null; // <- Warning } } }
残念ながら(後方互換性のため)コンパイルエラーではなく Warning のみではありますが。
IL
さて、この null 許容型と非 null 許容型のインスタンスですが、実行時の違いはあるのでしょうか。
ILSpy Visual Studio Code Extension( https://github.com/icsharpcode/ilspy-vscode )を使って IL を覗いてみます。
.class /* 02000005 */ private auto ansi beforefieldinit CSharpEightSample.Program extends [System.Runtime]System.Object { // Methods .method /* 06000005 */ private hidebysig static void Main ( string[] args ) cil managed { .custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = ( 01 00 01 00 00 ) // Method begins at RVA 0x2094 // Code size 6 (0x6) .maxstack 1 .entrypoint .locals /* 11000001 */ init ( [0] class Models.User, [1] class Models.User ) IL_0000: nop IL_0001: ldnull IL_0002: stloc.0 IL_0003: ldnull IL_0004: stloc.1 IL_0005: ret } // end of method Program::Main ...
ローカル変数においては特に違いがなさそうです。
プロパティはどうでしょうか。
namespace CSharpEightSample { public class Sample { public string Name { get; set; } = ""; public string? NullableName {get; set; } } class Program { static void Main(string[] args) { } } }
.class /* 02000005 */ public auto ansi beforefieldinit CSharpEightSample.Sample extends [System.Runtime]System.Object { .custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = ( 01 00 01 00 00 ) .custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = ( 01 00 00 00 00 ) // Fields .field /* 04000003 */ private string '<Name>k__BackingField' .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) .custom instance void [System.Runtime]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [System.Runtime]System.Diagnostics.DebuggerBrowsableState) = ( 01 00 00 00 00 00 00 00 ) .field /* 04000004 */ private string '<NullableName>k__BackingField' .custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = ( 01 00 02 00 00 ) .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) .custom instance void [System.Runtime]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [System.Runtime]System.Diagnostics.DebuggerBrowsableState) = ( 01 00 00 00 00 00 00 00 ) // Methods .method /* 06000005 */ public hidebysig specialname instance string get_Name () cil managed { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) // Method begins at RVA 0x2092 // Code size 7 (0x7) .maxstack 8 IL_0000: ldarg.0 IL_0001: ldfld string CSharpEightSample.Sample::'<Name>k__BackingField' /* 04000003 */ IL_0006: ret } // end of method Sample::get_Name .method /* 06000006 */ public hidebysig specialname instance void set_Name ( string 'value' ) cil managed { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) // Method begins at RVA 0x209a // Code size 8 (0x8) .maxstack 8 IL_0000: ldarg.0 IL_0001: ldarg.1 IL_0002: stfld string CSharpEightSample.Sample::'<Name>k__BackingField' /* 04000003 */ IL_0007: ret } // end of method Sample::set_Name .method /* 06000007 */ public hidebysig specialname instance string get_NullableName () cil managed { .custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = ( 01 00 02 00 00 ) .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) // Method begins at RVA 0x20a3 // Code size 7 (0x7) .maxstack 8 IL_0000: ldarg.0 IL_0001: ldfld string CSharpEightSample.Sample::'<NullableName>k__BackingField' /* 04000004 */ IL_0006: ret } // end of method Sample::get_NullableName .method /* 06000008 */ public hidebysig specialname instance void set_NullableName ( string 'value' ) cil managed { .custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = ( 01 00 02 00 00 ) .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) // Method begins at RVA 0x20ab // Code size 8 (0x8) .maxstack 8 IL_0000: ldarg.0 IL_0001: ldarg.1 IL_0002: stfld string CSharpEightSample.Sample::'<NullableName>k__BackingField' /* 04000004 */ IL_0007: ret } // end of method Sample::set_NullableName .method /* 06000009 */ public hidebysig specialname rtspecialname instance void .ctor () cil managed { // Method begins at RVA 0x20b4 // Code size 19 (0x13) .maxstack 8 IL_0000: ldarg.0 IL_0001: ldstr "" /* 70000001 */ IL_0006: stfld string CSharpEightSample.Sample::'<Name>k__BackingField' /* 04000003 */ IL_000b: ldarg.0 IL_000c: call instance void [System.Runtime]System.Object::.ctor() /* 0A00000F */ IL_0011: nop IL_0012: ret } // end of method Sample::.ctor // Properties .property /* 17000001 */ instance string Name() { .get instance string CSharpEightSample.Sample::get_Name() .set instance void CSharpEightSample.Sample::set_Name(string) } .property /* 17000002 */ instance string NullableName() { .custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = ( 01 00 02 00 00 ) .get instance string CSharpEightSample.Sample::get_NullableName() .set instance void CSharpEightSample.Sample::set_NullableName(string) } } // end of class CSharpEightSample.Sample
「NullableName」の方には「System.Runtime.CompilerServices.NullableAttribute」が付与されていますね。
default!
例えば非 null 許容の string 型のインスタンスがあるとして、デフォルト値として空文字を設定するか、「default!」を渡すことで Warning を回避できます。
string sample = ""; // <- OK string sample2 = default! // <- OK
さて、「sample2」にはデフォルトで何が入っているのか?というと、やっぱり null でした。
ということで、「default!」はただ Warning を抑えるためだけに使える、といった印象を受けました。
Generics
Generics はどうでしょうか。
まず、下記のように書くことはできません。
... public class Sample<T> where T: class { public T Name { get; set; } = default!; public T? NullableName {get; set; } // <- compile error } ...
「T」に対して class または struct の制約がつけられるのであれば、「T?」を使うことができます。
... public class Sample<T> where T: class { public T Name { get; set; } = default!; public T? NullableName {get; set; } // <- OK } ...
また「T」に null 許容の型を指定したい場合は「class ?」を使うことができます。
... public class Sample<T> where T: class ? { public T Name { get; set; } = default!; public T NullableName {get; set; } = default!; } class Program { static void Main(string[] args) { var s = new Sample<string?>(); // <- OK } } ...
- Constraints on type parameters - C# Programming Guide | Microsoft Docs
- null 許容参照型 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C
またプロパティやメソッドに対しては属性を使う、といった方法もあります。
... public class Sample<T> { public T Name { get; set; } = default!; [AllowNull] public T NullableName {get; set; } = default!; } class Program { static void Main(string[] args) { var s = new Sample<string>(); s.NullableName = null; // <- OK } ...
... public class Sample<T> { public T Name { get; set; } = default!; [AllowNull] public T NullableName {get; set; } = default!; [return: MaybeNull] public T GetNull() { return default(T); } } class Program { static void Main(string[] args) { var s = new Sample<string>(); s.NullableName = null; Console.WriteLine((s.GetNull() == null)); // <- true } ...
Entity FrameWork Core
DbContext
DbContext では、「DbSet<T>」のインスタンスに対して 「Set<T>()」を使い、 null 許容を避けることができます。
using Microsoft.EntityFrameworkCore; namespace Models { public class CodeFirstSampleContext: DbContext { public CodeFirstSampleContext(DbContextOptions<CodeFirstSampleContext> options) : base(options) { } public DbSet<Workflow> Workflows => Set<Workflow>(); public DbSet<WorkflowReader> WorkflowReaders => Set<WorkflowReader>(); } }
Model
ではモデルクラスはどうでしょうか。
こちらは特に null 許容を避けるメソッドなどが用意されているわけではないようなので、 Database のテーブルなどから null 許容にすべきかどうかを判断する、という必要があります。
null 許容・非 null 許容参照型はマイグレーションファイルに影響を与えるか
答えはイエスです。
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace Models { public class Sample { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } public string? Name { get; set; } = "hello"; } }
これでマイグレーションファイルを作ると、デフォルト値を設定していても「Name」カラムは null 許容となります。
... protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name: "Samples", columns: table => new { Id = table.Column<int>(type: "integer", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), Name = table.Column<string>(type: "text", nullable: true) }, constraints: table => { table.PrimaryKey("PK_Samples", x => x.Id); }); } ...
今のところ、できる限り全プロパティを非 null 許容にすべきか?というところはわかっていないのですが、まずはできる限り null 許容を避けてみようかな、とは思っています。
参照
- Nullable reference types | Microsoft Docs
- Design with nullable reference types | Microsoft Docs
- Nullable value types - C# reference | Microsoft Docs
- Essential .NET - C# 8.0 and Nullable Reference Types | Microsoft Docs
- Constraints on type parameters - C# Programming Guide | Microsoft Docs
- null 許容参照型 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C
- C# Reserved attributes: Nullable static analysis | Microsoft Docs
- Working with nullable reference types - EF Core | Microsoft Docs