はじめに
C# の null 許容参照型
C# 8.0以降、C# のプログラミングでは「null 許容参照型」という機能が使えるようになりました。
これにより、C# でより堅牢なアプリケーションを構築することができます。
この機能を有効にする方法のひとつとして、C# のプロジェクトファイル(.csproj)に <Nullable>enable</Nullabl>
MSBuild プロパティ値を追加する方法があります。
<!-- これは C# プロジェクトファイル (.csproj) -->
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
...
<!-- 👇 この行を追加 -->
<Nullable>enable</Nullable>
....
Blazor プログラミングにおけるコードビハインド
Blazor プログラミングでは、コンポーネントを実装するために、通常、HTMLマークアップと C# コードとを混ぜて Razorファイル(.razor)に記述し、C# コードは @code { }
ブロック内に配置します。
<p>Here is an HTML markup.</p>
@code {
// ここに C# コードを書く
}
そしてもう一つの方法として、「コードビハインド」という方法があります。
この方法では、通常の C# ソースファイル (.cs) に、C# コードを、パーシャルクラスとして記述します。
public class partial MyComponent
{
// .razor ファイルから分離して、C# コードをここに書く
}
「コードビハインド」スタイルは、Blazor コンポーネントを読みやすく、メンテナンスしやすくするために便利な場合があります。
Blazor コンポーネントの "コードビハインド" での依存性注入
DI コンテナからサービスを取得するには、Blazor コンポーネント(.razor)で @inject
ディレクティブを使用する必要があります。
<!-- 👇 "IJSRuntime" サービスのインスタンスが、DI コンテナから注入されます。 -->
@inject IJSRuntime JS
@inject
ディレクティブは、プロパティ・インジェクションとして機能します。
つまり、「コードビハインド」の C# ソースファイルにてサービスを注入したい場合は、@inject
ディレクティブではなく、[Inject]
属性でアノテーションされたプロパティを使って注入することができます。
public class partial MyComponent
{
// 👇 "IJSRuntime" サービスのインスタンスが、DI コンテナから注入されます。
[Inject]
private IJSRuntime JS { get; set; }
...
これら 3 つのプログラミングスタイルを混ぜるとどうなるのか?
C# の null 許容参照型、コードビハインドスタイル、コードビハインドでのプロパティインジェクション、これら3つのプログラミング要素を混ぜるとどうなるでしょうか?
答えは、「CS8618」という恐ろしい警告が発生します。
"Warning CS8618 Non-nullable property 'JS' must contain a non-null value when exiting constructor. Consider declaring the property as nullable."
しかし、DI コンテナから注入されるプロパティは、アプリケーション開発者が書くコードの場所では決して null にならないはず、と思います。
そのため、この CS8618 の警告は意味をなさないと感じます。
この警告を避けるためにはどうすればよいでしょうか?
思いついた 3つのアイデア(しかしいずれも不完全)+ [2021/05/02 追記] コメントで教えてもらった方法
今のところ、この問題の完璧な解決策についてはわかりません。
しかし、この問題を軽減するための3つのアイデアを思いつきました。
[2021/05/02 追記] コメントにて、なるほどこれはスマート! と自分には思われる方法を教えてもらいましたので、4つめの解決方法として追記しました。
1. プロパティを null 許容にする。
アイデアの1つは、プロパティを null 許容にすることです。
そのためには、型名の最後に「?」記号を付けます。
[Inject] // 👇 "?" はこのプロパティ値が null になり得ることを示します.
public IJSRuntime? JS { get; set; }
その代わりに、プロパティを参照する前に必ず null チェックを行う必要があります。
// 👇 null チェックが必要
if (this.JS != null)
{
await this.JS.InvokeVoidAsync("foo");
}
もし null チェックをさぼると、C# コンパイラが「CS8604」という別の警告を出します。
この解決法は厳密には正しい方法です。
しかし、先に述べたように、注入されたプロパティが開発者のコードで null になることは現実問題としてありません。
そのため、毎度の null チェックが必要になってしまうこの解決策は冗長すぎると感じます。
また、毎回 null チェックする以外の別の方法として、「!」マークを使って、C# コンパイラに変数が null かどうかの条件を無視させることもできます。
// 👇 "!" 記号で、null 状態確認を無視させる。
await this.JS!.InvokeVoidAsync("foo");
とはいえ、この方法はまったくもってお勧めしません。
なぜなら、この方法では何のために null 許容参照型の機能を有効にしているのか、意味不明になってしまうからです。
2. [AllowNull]
属性の追加
次のアイデアは、プロパティに [AllowNull]
属性を付加することです。
// 👇 [AllowNull] 属性を追加
[Inject, AllowNull]
public IJSRuntime JS { get; set; }
この解決策は、概ね良好に機能します。
ただし、プロパティが null 許容ではないにも関わらず、null を設定することができてしまいます。
// 👇 null を設定してもコンパイラから何も言われない 😥
this.JS = null;
3. #pragma waning disable
を使う
最後のアイデアは、プロパティを "warning disable "というプラグマで囲むことです。
// 👇 "warning disable" プラグマで当該プロパティを囲う
# pragma warning disable CS8618
[Inject]
public IJSRuntime JS { get; set; }
# pragma warning restore CS8618
この解決策も問題なく動作します。
そしてこの方法はある意味、開発者が最も直接的にやりたいこと = "警告を抑止したい" を反映していると思います。
しかし、この方法ではC#コードのインデントが崩れ、記述量が多くなり、コードの読みやすさが損なわれてしまいます。
このため、自分はこの方法は好きではありません。
4. [2021/05/02 追記] コメント欄にて教えてもらった、より賢い方法
コメント欄にて、以下のようにプロパティを初期化するコードを書くとよいのではないか、と教えてもらいました。
[Inject]
public IJSRuntime JS { get; set; } = null!;
// 👆 「!」記号付きの null で初期化
「!」記号を末尾に付記した null であれば、非 null 許容参照型の変数に、強制的(?)に null を代入することができ、かつ、コンパイラからも文句を言われないのは知りませんでした。
自分的には、この方法はいちばんスマートな解決方法であると思いました。
@taks@github さん、コメントありがとうございます。
まとめ
自分は、仕事と趣味の両方で、どちらかといえば上記のうちの最初の解決策 (null 許容を指定しつつ、使用箇所で適宜 null チェックを行なう) を使うことが多いです。
しかし、これらの解決策はまったくもってスマートではないので、自分は上記 1~3 の解決策に大きな不満を持っています。
ということで、近い将来、C# のコンパイラや構文が、この厄介な問題をスマートに解決してくれることを期待したいところです。
([2021/05/02 追記] 前述のとおり、コメントにて、null!
で初期化する方法を知りまして、今はこの null!
初期化方式がいちばん妥当なように感じています。)
実は、自分は別の方法で実装することも多いです。それは「これらコーディングスタイルを混ぜない」という方法です。
つまり、コードビハインドスタイルでコーディングはするのですが、ただし DI からの注入だけは @inject
ディレクティブを .razor ファイルに記述する方法で書き、DI コンテナから注入されるプロパティは .cs ファイルに書かない、というやり方です。(下記参照)
<!-- 👇 DI コンテナからのサービス注入は .razor ファイルに書き... -->
@inject IJSRuntime JS
public partial class MyComponent {
...
// コードビハインドの C# ファイル中辛はそれを使うだけ。
await this.JS.InvokeVoidAsync("foo");
もしこの問題のより良い解決策があれば、この記事のコメントで教えてくれるとありがたいです。
Happy Coding! :)