いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Goメモ-296 (Go の for range ループのちょっとしたクセ)(イテレーション中の要素増減時の挙動)

類似記事

GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ

概要

以下、自分用のメモです。忘れないうちにメモメモ。。。

に乗っていた内容ですが、dotnetやjavaをやっている人だとちょっと戸惑う動きだったので、メモメモ。。。

基本的に、プログラミングしていてやってはいけない(というか、エラーになるか無限処理になってしまう)ことに以下があります。

イテレーション中に、イテレーション対象となっているシーケンスに対して要素の増減を行う

dotnetやjavaだと例外が発生します。pythonの場合は無限ループになります。

なので、上の言語を扱っているエンジニアだと基本この操作は行わないのですが、結構やってしまうんですよねこれ。。。

ですが、例外なり無限ループなりになるので、基本その段階で気づけます。

これが、Goの場合だとちょっとだけ異なる挙動をします。

以下、各言語で試してみた結果です。

試してみる

dotnet

$ dotnet --version
7.0.100
class App
{
    static void Main()
    {
        var items = new List<int> { 1, 2, 3, 4, 5 };
        foreach (var item in items)
        {
            items.Add(100);
        }
    }
}

実行すると以下のようになります。

$ dotnet run
Unhandled exception. System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
   at System.Collections.Generic.List`1.Enumerator.MoveNextRare()
   at App.Main() in /home/dev/tmp/dotnetapp/Program.cs:line 6

例外が発生します。

java

$ java --version
openjdk 19.0.2 2023-01-17
OpenJDK Runtime Environment Homebrew (build 19.0.2)
OpenJDK 64-Bit Server VM Homebrew (build 19.0.2, mixed mode, sharing)
import java.util.*;

class App {
    public static void main(String[] args) {
        var items = new ArrayList<Integer>(){ { add(1); add(2); add(3); add(4); add(5); } };
        for (var item : items) {
            items.add(item);
        }
    }
}

実行すると以下のようになります。

$ java Main.java
Exception in thread "main" java.util.ConcurrentModificationException
        at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013)
        at java.base/java.util.ArrayList$Itr.next(ArrayList.java:967)
        at App.main(Main.java:6)

例外が発生します。

python

$ python3 -V
Python 3.10.6
items = [1, 2, 3, 4, 5]
for item in items:
    items.append(item)

print(items)

実行すると以下のようになります。

$ python3 main.py

処理が終わりません。(無限ループになる)

無限に要素を追加し続けて処理が終わりません。

Go

んで、Goの場合は以下のようになります。

$ go version
go version go1.20.1 linux/amd64
package main

import "fmt"

func main() {
        items := []int{1, 2, 3, 4, 5}
        for _, v := range items {
                items = append(items, v)
        }

        fmt.Println(items)
}

実行すると以下のようになります。

$ go run main.go
[1 2 3 4 5 1 2 3 4 5]

なんと、、ちゃんと動きます。最初この挙動を確認したとき逆に驚きましたw

dotnetやjava歴が長いせいもあって、パニックになると思ってました。

んで、なぜGoはこのようになるのかの説明については、上記にリンクした書籍の 4.2 #31: Ignoring how arguments are evaluted in range loops に書かれているのですが

掻い摘むと、Goのfor rangeループはループする直前に対象のコピーを作った上でループを実行してくれている ということになります。

なので、元のシーケンスと同じ数分ループしてくれて、かつ、元のシーケンスに要素を追加してもちゃんと動く。

便利だなって思った反面、基本プログラム書いてるときに元のシーケンスをイテレーション中に変更するのはバグっているときが多いので、パニックで落ちてくれる方が良かったなとも思いました。

参考情報

Goのおすすめ書籍

Go言語による並行処理

Go言語による並行処理

Amazon


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。