Swift歴1年くらいの大学3年生です。Qiita初記事です。
最近、Heart of Swiftを読んでいるのですが、これまでよくわからなかったinout引数の理解が深まったので、アウトプットも含めて記事を書いてみます。もし間違えてるところがあったらご指摘いただけると嬉しいです。
[前提]Swiftは値型中心
参照渡しを説明する際に、前提で必要となることを説明をします。
Swiftは基本的に値型中心の言語で、それがSwiftを初心者でも扱いやすい安全性の高い言語にしています。
Swiftの中で参照型は、classとクロージャーくらいで、
- Int, Float, Double, Bool
- String, Character
- Array, Dictionary, Set
- Optional, Result
などSwiftの標準ライブラリの多くは値型です。
しかし正直、値型は安全性を高めると聞いてもよくわからないです。
なぜ値型はプログラムをプログラマーにとって安全にするのでしょうか?
実際に、例を挙げて考えていきます。
Dogクラスを作って、犬の名前を変えてあげることを考えてみます。ただその前に、作ったインスタンスを複製してそちらの変更を行なってみます。するとどうでしょう。どちらの犬の名前もぽち丸になってしまってる。。。
これが参照型の持つ危うさです。参照型には予期せぬ間にこのような変更が行われない可能性が潜んでいます。
class Dog {
var name = "ぽち"
}
var animal = Animal()
var duplicatedAnimal = animal
duplicatedAnimal.name = "ぽち丸"
print(duplicatedAnimal.name) /// ぽち丸
print(animal.name) /// ぽち丸
ではなぜ、変更していないインスタンスに前で変更が及んでしまうのでしょうか?
答えは、この時の変数animalやduplicatedAnimalが保持しているのはインスタンス自体ではなく、インスタンスの参照のアドレスだからです。
一方値型はどうでしょうか?
struct Dog {
var name = "ぽち"
}
var animal = Animal()
var duplicatedAnimal = animal
duplicatedAnimal.name = "ぽち丸"
print(duplicatedAnimal.name) /// ぽち丸
print(animal.name) /// ぽち
こちらの場合は、値の複製は行われず、animalのnameはぽちのままになっています。このように値型はより直感的で予期せぬ値の変更を防ぐことができます。
なぜこのようなことが起きるか、メモリーの持たせ方の部分はまた別機会に記事を書きます!
参照渡しをしたくなる時
参照渡しとはズバリ参照を渡すことです。そのまま過ぎるのでもう少し深掘ります。
以下のコードはSwiftのグロバールスコープ中のanimalArrayを関数内で別の変数に複製して、そちらに変更を加えたものです。それぞれの配列の状態を最後に確認してみると、
changesArrayElementの結果は変更された配列["monkey", "cat", "elephant"]であることがわかります。しかし、複製された配列animalsArrayは、["dog", "cat", "elephant", "lion"]のままです。なぜこうなるのかというと、ArrayはSwiftにおいて値型であるため、関数に渡した際に複製されます。なので、関数の引数で渡ってきたものに変更を加えたとしても、元の渡した関数に変更は与えられません。
var animalsArray = ["dog", "cat", "elephant", "lion"]
func changesArrayElement(animalsArray: [String]) -> [String] {
animalsArray[0] = "monkey"
animalsArray.removeLast()
// 返り値としてduplicatedAnimalsArrayを返す
return animalsArray
}
// デバッグしてみると、
print(changesAnimalsArray(animalsArray: animalsArray)) /// この配列の結果は["monkey", "cat", "elephant"]になる。
print(animalsArray) /// この結果は["dog", "cat", "elephant", "lion"]のままになる。
上記のような場合に、元の配列に変更を与えるためにはどうするべきでしょうか。
そうです。参照を渡してあげれば良いのです。参照はアドレスをコピーして、ヒープ上にある同じ配列を操作するので、これはうまくいきそうです。
inout引数とは
具体的に参照を渡す際に、現れてくるのがinout引数です。以下のコードを見てみてください。
var animalsArray = ["dog", "cat", "elephant", "lion"]
func changesArrayElement(animalsArray: inout [String]) -> [String] {
animalsArray[0] = "monkey"
animalsArray.removeLast()
// 返り値としてduplicatedAnimalsArrayを返す
return animalsArray
}
// デバッグしてみると、
print(changesAnimalsArray(animalsArray: &animalsArray) /// この配列の結果は["monkey", "cat", "elephant"]になる。
print(animalsArray) /// この結果は["dog", "cat", "elephant", "lion"]のままになる。
これで無事に、元の関数に変更を加えることができました。
少し不自然な書き方に思われるかもしれませんが、これは値型であるSwiftのデータ整合性を壊さざるおえない仕様なので、そのように安全な設計がされています。