LoginSignup
0
1

More than 1 year has passed since last update.

Swift言語のfor-in文にハマった話

Last updated at Posted at 2022-07-26

自分への備忘録も兼ねてここにまとめておく

Swift 3.0からC言語風のfor文は廃止され、for-inだけのシンタックスとなった。

C言語
for (int i = 0; i < 10; i++) {
  //処理
}

上と同等なことをswiftで書くとこうなる。

swift言語(Swift 3.0以降)
for i in 0 ..< 10 {
  //処理
}

これだけなら、なんら困ることはない。
今回ハマったケースは、終了条件にかかわる変数をループ内で更新している場合である。

C言語なら
int last = 10;
for (int i = 0; i < last; i++) {
  //処理
  last = 11;

}

上の例では、ループ内の処理は11回実行される。しかし、swiftだと、

swift言語だと
var last = 10
for i in 0 ..< last {
  //処理
  last = 11

}

10回しかループしないのだ。これに気付かず、原因究明に時間を要してしまった。
swiftを使いはじめてかなりの年数が経つが初めての経験であった。

for-in文はループ開始前にループ回数が決まる

ループ開始前に式の評価が終わっているため、ループ内で終了条件にかかわる変数を更新しても(再評価されないので)、ループ回数は変わらないのだ。

同様に、配列の要素を順に処理する場合に、ループ内で要素数が変化(追加や削除)する場合でも、ループ前に評価された要素数となるので注意が必要だ。

例1(要素数が増える場合)
var A = Array(0 ..< 10)
for n in 0 ..< A.count {
    if n > 0 && n.isMultiple(of: 3) {
        A.insert(n + 10, at: n)
    }
    print(n, A.count, A, separator: "\t")
}

//結果
n   A.count   A
0	10	[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
1	10	[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
2	10	[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
3	11	[0, 1, 2, 13, 3, 4, 5, 6, 7, 8, 9]
4	11	[0, 1, 2, 13, 3, 4, 5, 6, 7, 8, 9]
5	11	[0, 1, 2, 13, 3, 4, 5, 6, 7, 8, 9]
6	12	[0, 1, 2, 13, 3, 4, 16, 5, 6, 7, 8, 9]
7	12	[0, 1, 2, 13, 3, 4, 16, 5, 6, 7, 8, 9]
8	12	[0, 1, 2, 13, 3, 4, 16, 5, 6, 7, 8, 9]
9	13	[0, 1, 2, 13, 3, 4, 16, 5, 6, 19, 7, 8, 9]
例2(要素数が減る場合)
var A = Array(0 ..< 10)
for n in 0 ..< A.count {
    if n > 0 && n.isMultiple(of: 3) {
        A.removeLast()
    }
    print(n, A.count, A, separator: "\t")
}

//結果
n   A.count   A
0	10	[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
1	10	[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
2	10	[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
3	9	[0, 1, 2, 3, 4, 5, 6, 7, 8]
4	9	[0, 1, 2, 3, 4, 5, 6, 7, 8]
5	9	[0, 1, 2, 3, 4, 5, 6, 7, 8]
6	8	[0, 1, 2, 3, 4, 5, 6, 7]
7	8	[0, 1, 2, 3, 4, 5, 6, 7]
8	8	[0, 1, 2, 3, 4, 5, 6, 7]
9	7	[0, 1, 2, 3, 4, 5, 6]

要素数が増えようが減ろうがお構いなしに、ループ開始時に評価した内容でループする。
例2だと、配列外参照必至のパターンだ。

Collectionをイテレートする場合も(ループ内でその要素を更新していても)、ループ前に評価された値のままだ。

例3
var n = 0
var A = Array(0 ..< 10)
for a in A {
    A[(n + 1) % 10] += 10
    print(n, a, A, separator: "\t")
    n += 1
}

//結果
n   a   A
0	0	[0, 11, 2, 3, 4, 5, 6, 7, 8, 9]
1	1	[0, 11, 12, 3, 4, 5, 6, 7, 8, 9]
2	2	[0, 11, 12, 13, 4, 5, 6, 7, 8, 9]
3	3	[0, 11, 12, 13, 14, 5, 6, 7, 8, 9]
4	4	[0, 11, 12, 13, 14, 15, 6, 7, 8, 9]
5	5	[0, 11, 12, 13, 14, 15, 16, 7, 8, 9]
6	6	[0, 11, 12, 13, 14, 15, 16, 17, 8, 9]
7	7	[0, 11, 12, 13, 14, 15, 16, 17, 18, 9]
8	8	[0, 11, 12, 13, 14, 15, 16, 17, 18, 19]
9	9	[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

以上のことは、strideを使っても、forEachを使っても回避できないことを付け加えておく。

例4 (stride)
let A = Array(0 ..< 10)
var step = 1
for n in stride(from: 0, to: A.count, by: step) {
    step += 1
    print(n)
}

//結果
0
1
2
3
4
5
6
7
8
9

言語として規定があるのか?

以下はSwift Languageからの抜粋である。

for-in.png

beginning of the loopという単語が見えるので、おそらく言語規定上の動作だと思われる。(英語はまったく自信がないので、違っていたらスミマセン)

で、どうする? 

素直に、while文で書く。ただし、ループ変数の更新は、普通ならループの末尾に置くのが普通だが、continue文を使いたい場面が多々あるため、ループの先頭に置いた。

whileで代替
var i = 0, last = 10
while i < last {
  let n = i // for n in 0 ..< last
  i += 1

  //処理
  //if A[n] == x { continue }
  last = 11

}

for-in文はループ開始前に『ループ変数(item)』が決まるというお話でした。
(コンパイルオプションで変えられる、なんてことあるのかな?)
以上

0
1
0

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
0
1