「ループで動的に要素を生成しようとしたら ArgumentOutOfRangeException エラーが発生してしまいました。どうしたらよいでしょうか?」といったお問い合わせを時々受けますので、その事象と原因、対処方法について簡単にまとめました。

事象について

例えば TODO アプリを考えてみます。

 

このようなアプリの場合、要素数が増減するので通常はループを使用して動的に生成します。その際、何気なく for で作ってしまうと ArgumentOutOfRangeException 例外が発生することがあります。

@* ArgumentOutOfRangeException 例外が発生してしまうコードの例 *@

@for (int i = 0; i < _simpleTodoItems.Count; i++)
{
    <IgbCheckbox>@_simpleTodoItems[i].Text</IgbCheckbox>
}

@code {
private List<SimpleTodoItem> _simpleTodoItems = new()
    {
        new() { Text = "歯医者を予約する" },
        new() { Text = "ごみ捨て袋を買う" },
        new() { Text = "本を返却する" },
        new() { Text = "サブスクを更新する" },
        new() { Text = "夏休みの飛行機を予約する" }
    };
}

このコード例の場合 @_simpleTodoItems[i].Text で例外が発生します。

例外が出力されている例

Blazor Server の例

Blazor WebAssembly の例

 

原因を解き明かす

まずこのコード↓

@for (int i = 0; i < _simpleTodoItems.Count; i++)
{
    <IgbCheckbox>@_simpleTodoItems[i].Text</IgbCheckbox>
}

は、実際のところは次のような C# コードにコンパイルされています。説明のためにかなり簡略化していますのでその点はご了承ください。

for (int i = 0; i < _simpleTodoItems.Count; i++)
{
    builder.OpenComponent<IgbCheckbox>();    // IgbCheckbox型の子コンポーネントを追加し、
    builder.AddAttribute("ChildContent", (RenderFragment)((__builder2) => { __builder2.AddContent(_simpleTodoItems[i].Text); }));    // IgbCheckboxのChildContent属性にコンテンツを追加し、
    builder.CloseComponent();    // 追加した子コンポーネントを閉じる。
}

特にこの部分

(__builder2) => { __builder2.AddContent(_simpleTodoItems[i].Text); }

にラムダ式 (匿名関数) が使用されているのにお気づきでしょうか? そしてこの匿名関数の中で匿名関数の外にある for ループの i を使用しているのにお気づきでしょうか? さらにこの匿名関数は、この段階では定義されているだけであってまだ実行のタイミングではないことにも気づいてください。

このように匿名関数の中から匿名関数の外側にある変数が参照されいる場合、その変数は値ではなく参照としてキャプチャされます。そして実行のタイミングで初めて参照先から値が取り出されます。実行のタイミングは for ループを抜けた後ですので、i の値は _simpleTodoItems.Count と同じ値になっています。それでインデックスが範囲を超えたという例外が出てしまうのです。

ラムダ式、匿名関数、キャプチャ、定義と実行のタイミング、等々については、正確な内容はマイクロソフトが出している公式ドキュメントを確認してください。今回の例で取り上げた内容をすべて説明したものではありませんが、たとえば以下のドキュメントが参考になると思います。

  • キャプチャされた外部変数
    • 特に「for ループで反復変数が宣言されている場合、その変数自体はループの外部で宣言されていると見なされます。」以降に同じ例が記述されています。

第三者がわかりやすく説明している記事も多数ありますので、検索するなり AI に聞くなりしてみるのもよいと思います。

この一連の動きは C# の仕様によります。ですので弊社の製品を使っていなくても起こりえますし、Blazor でなくても例えばコンソール アプリケーションでも起こりえます。また C# に限らずその他の言語でも同様の仕様となっていますので、たとえば JavaScript などでも起こりえる話になります。

対処方法

問題を引き起こしている変数の値をループ内で新たに生成した変数に代入し、ループ内では新たに生成した変数の方を使うことで対処できます。

対処方法 1 (お勧め)

foreach を使うと簡単に回避できます。foreach は繰り返すたびに新たに変数を作成しそこにループ中の要素を代入します。特に意識することなく回避できますし、コードもすっきり書けますので、こちらがお勧めです。

@* 対処方法 1 (お勧め) *@

@foreach (var todoItem in _simpleTodoItems)
{
    <IgbCheckbox>@todoItem.Text</IgbCheckbox>
}

対処方法 2

絶対 foreach で書かなければならないかというとそうではなく、for のままでも対処できます。以下はその方法です。例外を起こしているのが i ですので、i の値をループに入った直後に別の変数に入れ、以後はそちらを使うことで解決します。下のコード例では i の値をループ内の変数 todoItemIndex に入れて、あとは todoItemIndex を使用しています。とはいえ、対処方法 1 の方が断然すっきりシンプルなので、特に for で書かなければならないという理由がない限り、第一の選択肢は対処方法 1 になるかと思います。

@* 対処方法 2 *@

@for (int i = 0; i < _simpleTodoItems.Count; i++)
{
    int todoItemIndex = i;
    <IgbCheckbox>@_simpleTodoItems[todoItemIndex].Text</IgbCheckbox>
}

まとめ

ループで動的に要素を生成する際 ArgumentOutOfRangeException 例外が発生することがあります。その場合は、(1) for ループを使用しているか、(2) もし使用している場合は for ループで使用している i 等の反復変数をそのままコンポーネントで使用していないか、この 2 点を確認してください。もし使用してしまっている場合は、for ループで使用している i がラムダ式の中でキャプチャされてしまっているのが原因です。そのまま使用するのではなく、ループ内で新たに生成した変数を利用することで回避できます。一番簡単でお勧めな方法は for ではなくforeach を使用することです。

サンプル

冒頭の TODO アプリを対処方法 1 で書いたサンプルです。参考のために例外が発生するコードもコメント アウトして残しています。

 

Tagged:

製品について

Ignite UI for Blazor