初心者がSwiftで苦戦した内容を記録します。
[Any]型の多次元配列を扱う問題を解く。
問 入れ子になった配列から整数以外を取り除き、整数を1足す関数add1numを定義せよ。
例 ["a", 1, [3, "a", ["e", 6], 5], "b", 6] は[2, [4, [7], 6], 7]になる。
つまり、
スタート = ["a", 1, [3, "a", ["e", 6], 5], "b", 6]
ゴール = [2, [4, [7], 6], 7]
になります。
回答
先に自分の回答を出しておきます。
import UIKit
var example:[Any] = ["a",1,[3, "a", ["e", 6], 5],"b",6]
func add1num(a:[Any])-> [Any]{
var judge : [Any] = []
for num1 in a {
if num1 is Int{
judge.append(num1 as! Int+1)
}else if num1 is [Any] {
judge.append(add1num(a: num1 as! [Any]))
}
}
return judge
}
add1num(a:example)
目次
・[Any]型
・再帰処理
・処理の流れ
・まとめ
[Any型]
違う値を同じ配列に入れていくと[Any]型の配列(Array)になります。
この[Any]型は要素にどんな型も扱うことができます。
var example:[Any] = [
"a", //String型①
1, //Int型②
[3, "a", ["e", 6], 5], //[Any]型③
"b", //String型④
6 //Int型⑤
]
しかし[Any]型は型情報が無くなり操作の幅が狭くなってしまうので、これがとても厄介です。
例えば今回用意した[Any]型のexampleの中にあるInt型の整数に1を足そうとしても
example.map({$0 as! Int + 1 })
//error: Execution was interrupted, reason: signal SIGABRT.
配列の中に一緒に入っているString型や[Any]型を巻き込んでしまいエラーが出てしまいます。
なのでこのような操作がしたい時は、要素毎に型を判定してInt型のみを対象に見なければいけません。
#要素毎にInt型であるかを判定してみます。
for in文を使用し配列の要素を順番に取り出していき、is 演算子を使って要素の型をチェックします。
func add1num(a:[Any])-> [Any]{
var judge : [Any] = []
for num1 in a {
if num1 is Int{
// 配列の要素がInt型であればここの処理を実行
judge.append(num1 as! Int+1)
}
}
return judge
}
// [2,7]
add1num(a:example)
また、[Any]型のままでは配列の操作ができないので、この処理を終えた要素は新たな配列 judge に格納するようにします。
[Any]型ならばその配列の要素をチェックするという考え方をし、今回の問題を解いていきます。
・String型ならば操作はしない(judge配列には格納しない)。
・Int型ならば整数に一を足した後にjudge配列へ。
・[Any]型ならばその配列の要素に、更に同様の型判定をする。→再帰処理へ
再帰処理
再起処理の理解で大きな躓きがあったのですが、長くなるので躓きに関しては割愛。余裕がある時に記事にします。
再帰処理は何か。と、見つけた答えは関数の中で関数を使用するということ。
つまり、関数の中で更にその関数を使うことにより、それで得た関数の”戻り値を使って”元の関数の処理を行います。
var example:[Any] = ["a",1,[3, "a", ["e", 6], 5],"b",6]
func add1num(a:[Any])-> [Any]{
var judge : [Any] = []
for num1 in a {
if num1 is Int{
judge.append(num1 as! Int+1)
}else if num1 is [Any] {
// 配列の要素が[Any]型ならばここの処理を実行
judge.append(add1num(a: num1 as! [Any]))
}
}
return judge
}
add1num(a:example)
回答のような書き方になってきます。
関数add1numに渡したexampleの要素が、判定の結果[Any]型ならば、この判定がされた要素を値として関数に渡して再判定を行って欲しいということです。関数内のadd1numの周りにjudge.append()がついているのは再判定後のadd1numの"戻り値"をjudge.append()して欲しいからです。
実際に処理の流れを見て理解を深めていきます。
処理の流れを見る
判定した型をjudge配列に要素として入れていくadd1num関数の処理を見ていきます。
1週目
var example:[Any] = [
"a", //String型①
1, //Int型②
[3, "a", ["e", 6], 5], //[Any]型③
"b", //String型④
6 //Int型⑤
]
func add1num(a:[Any])-> [Any]{
var judge : [Any] = []
for num1 in a {
if num1 is Int{
judge.append(num1 as! Int+1)
}else if num1 is [Any] {
// 配列の要素が[Any]型ならばここの処理を実行
judge.append(add1num(a: num1 as! [Any]))
}
}
return judge
}
add1num(a: num1 as! [Any])
①は判定を行っていないのでjudge配列に変化はありません。
②はInt型なのでjudge配列に処理後の2が追加されます。
③は[Any]型と判定された時に再帰処理を行います。[3,"a",["e,6"],5]を値として再度add1num関数で判定ををします。下記の2周目へ飛んでください。
③の続き
2周目を終えて戻り値[4,[7],6]がきたのでjudge配列へ[4,[7],6]を追加します。
④は判定を行っていないのでjudge配列に変化はありません。
⑤はInt型なのでjudge配列に処理後の7が追加されます。
1週目の関数が最後まで到達したのでjudge配列を戻り値とし、呼び出し元に返します。
ここでの戻り値は[2,[4,[7],6],7]です。
2週目
add1num2周目
/* num1 : [Any] = [
3, Int型①'
"a", String型②'
["e,6"], [Any]型③'
5 Int型④'
]*/
①'はInt型なので処理をしてjudge配列へ4を追加します。
②'は判定を行っていないのでjudge配列に変化はありません。
③'は[Any]型と判定された時に再帰処理を行います。["e,6"]を値として再度add1num関数で判定をします。下記の3周目へ飛んでください。
③'の続き
3周目を終えて戻り値[7]がきたのでjudge配列へ[7]を追加します。
④'はInt型なので処理をしてjudge配列へ6を追加します。
2週目の関数が最後まで到達したのでjudge配列を戻り値とし、呼び出し元の1週目へ返します。
ここでの戻り値は[4,[7],6]です。
1周目③の続きへ戻ってください。
3周目
add1num3周目
/* num1 : [Any] = [
e, String型①''
6 Int型②''
]*/
①''は判定を行っていないのでjudge配列に変化はありません。
②''はInt型なので処理をして配列へ7を追加します。
3週目の関数が最後まで到達したのでjudge配列を戻り値とし、呼び出し元の2週目へ返します。
ここでの戻り値は[7]です。
2周目③'の続きへ戻ってください。
まとめ
[Any]型も多次元配列も扱うために、ネストを深くしたり複雑なコードを書くとバグが増える可能性が出てくるので気を付ける必要がある。
再帰処理を行うことで多次元配列も少ない行数で処理できる。
また再帰処理はとても複雑なループの構造になり、そのループの間、関数の情報を保持し続けなければならない側面がある為にスタックオーバーフローを起こしてアプリがクラッシュする危険性もある。