くま's Tech系Blog

基本的には技術で学んだことを書き留めようと思います。雑談もやるかもね!

Closuresのドキュメントを見る前に

Swiftのクロージャーについてですが、クロージャを細かく説明するわけではありません。 Swiftのクロージャーの公式ドキュメントを見ると単純に和訳するだけでは理解するのが難しい用語があります(個人的にかもしれませんが・・・)

docs.swift.org

今回はドキュメントを読む前に必要な前提知識を解説しようと思います

Capture

まずは、キャプチャーについてです

キャプチャとはスコープ外の変数などをクロージャ内から操作、参照できる仕様のことです

通常、関数内で定義された変数はその関数外からは参照することができません。 これはその変数のスコープが関数内に制限されており、関数の中で使用済みで不要になった変数は自動で破棄されるからです

func sample() -> Int{
  var localCount = 0
  return localCount
}

//  「Cannot find 'localCount' in scope」というコンパイルエラー
print(localCount) 

しかしクロージャでは関数内に定義した変数を外部から参照・操作できます。 例えば次のような例です

func sample() -> () -> Int {
    var localCount = 0
    let closure = { () -> Int in
        localCount = localCount + 1
        return localCount
    }
  return closure
}

var sample = sample()

// 実行のたびにlocalCountが+1されていく
sample()
sample() 
sample() 

sample関数の返り値はクロージャにしてあります。 これにより中に定義したクロージャをそのまま外部へ持ち出すことができます。 つまり変数sampleに格納されているのは{ () -> Int in localCount = localCount + 1 return localCount }ということになります

クロージャの中ではlocalCountは未定義のはずですが、問題なく実行することができ、さらにクロージャの実行のたびにインクリメントされていきます。 つまりlocalCountは自動で破棄されることなく、メモリに残ったままです

Capture List

キャプチャと関連してキャプチャリストも和訳だけだと理解できないかもしれません

クロージャでキャプチャした変数や定義はメモリと強い参照(強参照)で紐付けられます。 そのため循環参照を引き起こし、メモリが解放されなくなる恐れがあります

循環参照を防ぐために、キャプチャリストを使用して明示的に変数の参照を制御することができます

記述方法は[変数名]形式で引数の前に記述します。 クロージャの省略記法を使用している場合でもキャプチャリストを記述する場合はinが必要になります

参照の強さはweakunownedキーワードを使用して指定します

// 暗黙的な強参照
{ () -> Void in print("TEST") }   

// 暗黙的な強参照
{ print("TEST") } 

// 明示的な強参照
{ [self] in print("TEST") }

// 明示的な弱参照
{ [weak self] in print("TEST") } 

// 明示的な非所有参照
{ [unowned self] in print("TEST") } 

Tuple

次にタプルについてです

タプルとは順序付けされた値の集合体のことで、異なる型の値をひとつの変数、または定数で扱えます

配列や辞書はデータ型に非常に厳格で基本的に同じデータ型しか入らない一方、タプルを使用すればそのままのデータ型で保存できる、つまり各値を使用するときとかにいちいち型変換をしなくてもそのままの形で演算できます

ただし、値の追加、削除、また繰り返し処理などはできません

基本的には次のように定義して、値を取得できます

// タプルの宣言
let tuple = (id: 123, name: "ABC", isFlag: true)

// 値の取得(ラベル指定)
print(tuple.id) // 結果: 123
print(tuple.name) // 結果: ABC
print(tuple.isFlag) // 結果: true

// 値をインデックス番号から取得
print(tuple.0) // 結果: 123
print(tuple.1) // 結果: ABC 
print(tuple.2) // 結果: true

また、下記のように定義することもできます

// タプルの宣言
let (id, name, isFlag) = (123, "ABC", true)

// 値の取得
print(id) // 結果: 123
print(name) // 結果: ABC
print(isFlag) // 結果: true

値の更新は次のように行います。 また、タプルは参照渡しではなく、値渡しです

// タプルの宣言
var tuple = (id: 123, name: "ABC", isFlag: true)

// 値の更新
tuple.id = 456

// 値の取得
print(tuple.id) // 結果: 456

in-out parameters

in-out parametersについてです。 in-outパラメータとは参照渡しを実装できるパラメータです。 つまり、関数の中でその引数に変更を加えた場合、呼び出し側の元の変数にも変更が適用されます

次のようにinoutというキーワードを設定することで参照渡しを実装できるパラメータとなります

func main(type: inout Int) -> Int {
    return result
}

注意点としては、参照を渡す場合は、&を付与します。 そして、引数に直接データを渡すと、その引数は imutableになるのでコンパイルエラーになります

func main(type: inout Int) -> Int {
    return result
}

var type = 1
let result = main(type: &type)

let result = main(type: 1) // NG

inoutパラメータを使うメリットとしては、参照を使用することでメモリコピーによるオーバーヘッドを避けることができます

しかし、inoutパラメータを作るのは、基本的には避けた方がよいでしょう。 意図しないところで値の書き換えが起こるというのが、バグの元になります。 関数から新しいデータを返してもらったほうが、プログラムのロジックを追うのが簡単になるので、良い気がします

最後に

個人的にここで挙げた内容を把握した上で英語ドキュメントを読むと理解しやすく、また理解が深まると思います!!

参照

タプル(tuple)について | Swift入門編 - ウェブプログラミングポータル

www.geeksforgeeks.org