変なところで壁にぶち当たって8時間ぐらいふいにしたので覚え書き的にサーッと書きました。
今はまだ文章量が少ないですけど、また壁にぶち当たったら増やします。ぶち当たらなかったら……増えません。
この記事を書いた時点の環境
- VisualStudio Community 2019 Version 16.8.3
- .NET Standard 2.1
前提知識
nullになる可能性がある変数を表示する
nullになるかもしれない変数を表示する時に、nullの時にメンバにアクセスするとNullReferenceException例外が投げられます。
考えてみれば当たり前のことではあるのですが、以下のように単純にnullの変数を表示しようとしても何も起きないのでつい間違えてしまうんですよね。
<p>@Hoge</p> @* 何も起きない *@
<p>@Hoge.Length</p> @* NullReferenceException *@
@code {
private string Hoge = null;
}
それでもnullであれば何も起きないようにしたい場合は、null条件演算子?.
を使えば大丈夫です。
<p>@Fuga?.Piyo</p> @* インスタンスがnullだったらnullを返す -> nullは何も表示しない *@
null条件演算子は後のメンバを評価する前にnullかどうかを判定し、nullだった場合はそのままnullを返して例外を発生させません。
Blazorにおける例外の捕捉
で、ここからが本題です。
Blazorでは基本的に実行時例外をVisualStudio上で捕捉できません。例外設定をどういじくりまわしてもVisualStudio上で例外発生時に止めることはできません。ステップ実行はできるのに……いやそっちはできないと滅茶苦茶困るけど。
ちなみに実行時例外が発生したときにどうしてもその場で例外メッセージを読みたい場合はこういう記事が参考になると思います。
とりあえず例外が発生したらChromeならF12キーでスタックトレースを読むことになると思いますが、現在のBlazorだとRazorページ内で例外が発生した時、少しだけ奇妙なことになります。
法則性がいまいち分かっていないのですが、どうしても本来のコード(.razor)から少し遡った位置で例外が発生する傾向があります。
下記のようなコードをIndex.razorに置くと、index.htmlの「Loading…」表記から先に進むことなく例外の発生によって以降の処理が中断されます。
@page "/"
<p>@Hoge.Length</p>
@code {
private string Hoge = null;
}
スタックトレース上では初期化中に例外が発生したような表記になります。一応一番上にNullReferenceExceptionが発生したと書いてあるので例外の内容については問題ないようです。内容だけに。
Razorページでは先に中身を見て動的にhtmlを構成する必要があるので、初期化中に例外が出たというだけならあまり違和感は無いでしょうか。
では以下のような書き方で例外を発生させます。
@page "/"
@if (IsLoading)
{
<h2>読み込み中</h2>
}
else
{
<p@Hoge.Length</p> @* 9行目: NullReferenceException *@
}
@code {
private string Hoge = null;
private bool IsLoading = true;
protected override async Task OnInitializedAsync() {
await Task.Delay(1000); // 1秒待つ
IsLoading = false;
}
}
実行すると1秒経った後に例外が発生し、8行目(elseの次の「{」)で例外が投げられたようなスタックトレースになると思います。この時点で少しずれているのが違和感がありますが、C#コード中でhtmlを生成していると考えれば納得がいくでしょうか。
ではさらに次のような書き方で例外を発生させるとどうなるのでしょう。
@page "/"
@if (IsLoading)
{
<h2>読み込み中</h2>
}
else
{
@foreach (var item in Piyos)
{
<p>@item</p>
}
<p@Hoge.Length</p> @* 13行目: NullReferenceException *@
}
@code {
private string Hoge = null;
private string[] Piyos = null;
private bool IsLoading = true;
protected override async Task OnInitializedAsync() {
Piyos = await GetPiyos(); // 1秒待つ
IsLoading = false;
}
private Task<string[]> GetPiyos() {
await Task.Delay(1000);
return new string[] {"あ", "い", "う", "え"};
}
}
実行すると、1秒経った後に例外が発生します。今までの傾向からして、また8行目かあるいは10行目の例外として記録されるでしょうか。
しかし、スタックトレースには9行目(@foreach)で例外が発生したような内容になると思います。違うそこじゃない絶対にそこじゃない。
もう滅茶苦茶悩みました。GetPiyosメソッドでreturnしている配列をPiyosの初期化式に入れてもやはり同じところで例外が出るので、それはもう悩みました。「初期化してあるのにnullなわけねえだろ!!!」と。
理由は定かではありませんが、BlazorのRazorページ内での例外は実際に発生した行よりも少しずれるわけですね。……かなり致命的なまでに。
ファイル名をHoge.razor.csかつpartial classにするとレイアウトとコードの分離ができるのですが、例外の発生位置が分離したコード内であれば、信用できるスタックトレースを吐くはずです。
コードを分離して上記のようなレイアウト要素内でnull例外を発生させるとどうなるのかについては試していないので分からないです。……気が向いたら調べます。いやこの記事の内容に気付くまでにかなり体力を消費したのでもう顔も見たくない……。
まとめ
この記事の内容をまとめると、
- どうしてもnullの可能性がある変数のメンバにアクセスしたい時はnull条件演算子を使う。
- Blazorの実行時例外はどうやっても捕捉できない。
- こんな記事が参考になるかも。リンク
- Razorページで発生した例外は行位置が少しずれて記録される。
こんな感じになります。
特に3番目は致命的ですかね……。この問題、僕のプロジェクトだけだったりしないですかね。もしこれが仕様なら人によってはBlazorを辞める選択肢が出ますね。それぐらい致命的です。
蛇足
Web系のプロジェクトって慣れていないと面倒なものが多い(偏見)ので、C#みたいなカッコいい書き方のままSPAかつクライアントサイドもサーバーサイドも書けるっていうのが僕にとってかなり革新的で魅力だと思うんです。
ブログ主が今までやってきたのってAndroid/JavaとUnity/C#、あとASP.NETのC#とVBを少々程度の知識しかなくて、静的型付け言語に慣れるとJavaScriptが書きにくいったらありゃしない。型が分からないのって状態変数を気にしながら書くコードと同じくらい書きにくいです。あとASP.NETの少々の知識からWeb系プロジェクトが「滅茶苦茶面倒」ということくらいしか分からないというのもあったりします。そこかしこでJavaScriptの知識が必要だったりしますし。
なんでまあ、C#でhtmlの<script>タグの中身が書けるhtmlも一緒にC#の知識をベースにゴリゴリ書ける、みたいな言語の偉大さがBlazorにはあって、それが僕的にかなり嬉しかったりします。
ただその書きやすさの代償なのか例外周りがかなり面倒なことになってますね。例外発生元誤認問題はガチ面倒。
とはいえBlazorでタイピングゲーム作ってみたまでいったんですけど、あの問題あったならUnityの方が良かったかもとか思ったり。ちなみにUnityはゲームエンジンなだけあってかなり直感的で分かりやすい代わりにUI周りが鬼畜だったりします。まあ使いこなせれば強いんですけど……それまでがね……。ああBlazorで作ったタイピングゲームの前身がUnity製だったりします。そちらは作るだけ作って公開まではいかなかったんですけど。
例外発生元誤認とかBlazorに何か更新があったりまた躓いたりしたら記事が更新されると思います。
それではまた。