4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

foreach でコレクションに格納しているstringが更新できない のは string が 不変性を持っているからだけではない

Last updated at Posted at 2024-01-31

この記事は 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}");
}
出力結果

sharpLab

01:aX
01:bX
01:cX
02:a
02:b
02:c

もしも 変数に参照のコピーではなく、参照をそのまま使いたいのであれば参照変数(もしくは 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}");
}
出力結果

sharpLab

01:aX
01:bX
01:cX
02:aX
02:bX
02:cX

これは 配列の インデクサアクセスが 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}");
}
出力結果

sharpLab

01:aX
01:bX
01:cX
02:aX
02:bX
02:cX

ちなみに 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}");
}
出力結果

sharpLab

01:aX
01:bX
01:cX
02:aX
02:bX
02:cX

結論

参照変数を使えば参照型だろうが値型だろうが配列の要素を上書きができるので foreach でも使っていくことができる。その場合やりやすさ的に、 Span<T> に変換した方がよい。

以上。

4
4
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?