for 文の中で Razor コンポーネントを使うと IndexOutOfRangeException が発生する件について
C# のコードだけだと、なかなかひっかからない問題なのですが Blazor の razor ファイル + 自分が作ったものではないコンポーネントの組み合わせで起きたときに問題の原因がわかるまで時間がかかったので忘れないようにメモしておきます。
起きた現象
例えば以下のような ChildContent を持つ MyText というコンポーネントがあるとします。
<p>
@ChildContent
</p>
@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }
}
for ループの中でループのインデックスの変数を使って配列にアクセスする処理を、このコンポーネントの ChildContent で行うと IndexOutOfRangeException が発生します。
@page "/"
<PageTitle>Home</PageTitle>
@for (int i = 0; i < _messages.Length; i++)
{
<MyText>@_messages[i]</MyText>
}
@code {
private readonly string[] _messages = ["a", "b", "c"];
}
理由は単純で MyText の ChildContent のレンダリングは for 文の中で即座に行われるのではなく、ラムダ式のように変数がキャプチャされてから後で行われるためです。そのためレンダリング時の i の値は必ず 3 になってしまい、_messages[3] が存在しないため IndexOutOfRangeException が発生します。
ラムダ式だとわかりやすいですが ChildContent を持つ Razor コンポーネントだと気づきにくいので注意が必要です。
解決策 1
解決策の 1 つ目は for ループの中で i をローカル変数としてキャプチャすることです。
@page "/"
<PageTitle>Home</PageTitle>
@for (int i = 0; i < _messages.Length; i++)
{
var index = i;
<MyText>@_messages[index]</MyText>
}
@code {
private readonly string[] _messages = ["a", "b", "c"];
}
これで index は for ループの中での i の値をキャプチャするため、MyText の ChildContent に渡すときに正しい値が渡されるようになります。
解決策 2
foreach ループを使うこともできます。というか純粋なループなら脳死で foreach を使うのがいいです。
@page "/"
<PageTitle>Home</PageTitle>
@foreach (var message in _messages)
{
<MyText>@message</MyText>
}
@code {
private readonly string[] _messages = ["a", "b", "c"];
}
まとめ
ということで、わかってしまえば納得なのですがわかるまで時間がかかったので忘れないようにメモでした。
悩んだ+メモとして書いておくことで記憶の定着を狙う…!
Discussion