この記事は foreachでコレクションに格納しているStringは更新できない/不変性
に関する 記事となっております。
元記事では主に java であったが、 C# だと次の様に書ける。
using System;
string[] strArray = [ "a", "b", "c" ];
// foreachで更新。
foreach (var str in strArray) {
str += "X";
Console.WriteLine($"01:{str}");
}
// 配列の内容出力
foreach(var str in strArray) {
Console.WriteLine($"02:{str}");
}
こう実行したとき 出力結果が次の様になる。
error CS1656: Cannot assign to 'str' because it is a 'foreach iteration variable'
そう、ビルドエラー CS1656
である。
これは C# に於いて、 foreach 時の取り出された要素への再代入は意味が無い為、禁止されているからである。
何故意味が無いかを解説する為に一度 for で書いてみるならば次の様に書ける。
using System;
string[] strArray = [ "a", "b", "c" ];
for(var i = 0;i<strArray.Length; i++) {
// ここで参照のコピーが渡される
var str = strArray[i];
// ここで上書きされるのは `str` 変数であって strArray[i] ではない
str += "X";
Console.WriteLine($"01:{str}");
}
// 配列の内容出力
foreach(var str in strArray) {
Console.WriteLine($"02:{str}");
}
もしも 変数に参照のコピーではなく、参照をそのまま使いたいのであれば参照変数
(もしくは ref ローカル
)を用いて次の様に書けばよい。
using System;
string[] strArray = [ "a", "b", "c" ];
for(var i = 0;i<strArray.Length; i++) {
// ここで参照が渡される
ref var str = ref strArray[i];
// ここで上書きされるのは 参照先の strArray[i]
str += "X";
Console.WriteLine($"01:{str}");
}
// 配列の内容出力
foreach(var str in strArray) {
Console.WriteLine($"02:{str}");
}
これは 配列の インデクサアクセスが ref T item[int index]
として実装されている為、可能なことである。
ただ、foreach の参照変数対応のインターフェスは 配列には用意されていない為(厳密には foreach を展開する際 ref 戻り値の特別対応ができない為)、 .NET Sta Span<T>
を 用いる形にすれば可能である。(勿論、IEnumerator<T>.Current
にあたる戻り値が ref T な 実装を持つ wrapper を追加で作れば対応できるのだが、一番手頃なのが Span<T>
な為そちらで進める。( sharplabとかで変換結果見ればわかる が Span<T>
こそ特別対応されているので厳密には違うが、ここでは考えないことにする。
using System;
string[] strArray = [ "a", "b", "c" ];
foreach(ref var str in strArray.AsSpan()) {
str += "X";
Console.WriteLine($"01:{str}");
}
// 配列の内容出力
foreach(var str in strArray) {
Console.WriteLine($"02:{str}");
}
ちなみに List には AsSpan()
が生えていない為、することができないと思いきや、構造は割れているので 公式で CollectionsMarshal.AsSpan<T>(List<T>)
が用意されている。
つまり、上記のコードを元にするなら次の様なコードとなる。
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
List<string> strArray = [ "a", "b", "c" ];
foreach(ref var str in CollectionsMarshal.AsSpan(strArray)) {
str += "X";
Console.WriteLine($"01:{str}");
}
// 配列の内容出力
foreach(var str in strArray) {
Console.WriteLine($"02:{str}");
}
結論
参照変数
を使えば参照型だろうが値型だろうが配列の要素を上書きができるので foreach でも使っていくことができる。その場合やりやすさ的に、 Span<T>
に変換した方がよい。
以上。