筋肉SwiftプログラマーになるためのARCトレーニング

  • 122
    いいね
  • 3
    コメント
この記事は最終更新日から1年以上が経過しています。

はじめに

こんにちは。haranicleです。
2015年。エンジニアにも筋肉が求められる時代になりました。
今回は、筋肉Swiftプログラマーになりたい皆様といっしょに、
Swiftの基礎であるARCについてトレーニングしていきましょう。
がんばりましょう。💪💪💪

前提

Swift2.1についての記載しています。

ARCの基礎

ARCとは

プログラマーが意識していなくても、Swift(のコンパイラ)がARC(Automatic Reference Counting)という仕組みを使ってメモリ領域管理をしてくれます。
もやしプログラマーが飯を食っていけるのもARCのおかげです。

ARCは、基本的に以下のルールに基づいてメモリ領域を管理します。

  • 参照型(クラスのインスタンスとクロージャなど)のみを対象にする
  • インスタンスを生成した時にメモリ空間を確保する
  • インスタンスが 必要 な間は、メモリ空間を確保し続ける
  • インスタンスが 不要 になった時に、メモリ空間を開放する

参照カウント

ARCは参照カウントという仕組みを利用して、インスタンスが 必要 or 不要 を判定しています。
参照カウントは以下のように定義されています。

  • インスタンスごとに割り振られる値
  • インスタンスを参照(*1)しているプロパティ、定数、変数の個数

Objective-CでARCをOFFにすると、retainCountというメソッドで参照カウントを取得できます。

*1 後述しますが、"参照"はstrong参照と呼ばれるもののみが対象です。

ARCの挙動

  • 新しいインスタンスを作るとARCがメモリ領域を確保する
    • メモリは以下を保持する
    • インスタンスの型
    • ストアドプロパティ(*2)
  • インスタンスの参照カウントが0になったら、そのインスタンスを記録しているメモリ領域を開放する
  • 開放したメモリ領域は、他の目的に使用することができる

*2 ストアドプロパティ、コンピューテッドプロパティについては、以下を参照してください
The Swift Programming Language (Swift 2.1): Properties

ARCの挙動サンプルコード

The Swift Programming Languageのサンプルコードをベースに、コメントで解説しています。

arc_sample.swift
class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}

var reference1: Person? // == nil (Optionalなのでnilで初期化される)
var reference2: Person? // == nil
var reference3: Person? // == nil

reference1 = Person(name: "John Appleseed") // John Appleseedが作られる

// "John Appleseed is being initialized"とprintされる

// reference1がJohn Appleseedへの参照を持つためJohn Appleseedの参照カウントが1になる
// John Appleseedは、参照カウントが0でないので開放されない

reference2 = reference1 // reference2はJohn Appleseed を参照している(参照カウント2)
reference3 = reference1 // reference3はJohn Appleseed を参照している(参照カウント3)

reference1 = nil // reference1は何も参照しない(参照カウント2)
reference2 = Person(name: "haranicle") // reference2 が他のインスタンスを参照するようになった(参照カウント1)

// John Appleseedは、参照カウントが1以上なのでdeallocされない

reference3 = nil // reference3は何も参照しない(参照カウント0)

// "John Appleseed is being deinitialized"とprintされる

// John Appleseedの参照カウントが0になったので開放された

do {
    let reference4:Person = Person(name: "Kazushi Hara") // reference4はKazushi Haraを参照している(参照カウント1)

    // ここでスコープ抜ける
}
// "Kazushi Hara is being deinitialized"とprintされる
// reference4がなくなるのでKazushi Haraの参照カウント0になり、開放された

ARCの罠 "循環参照"

循環参照とは

  • 複数のインスタンスが互いに参照をもつとき、参照カウントが0にならず、インスタンスが開放されない 循環参照 という現象を起こすことがあります。
  • 循環参照は、プログラマーの筋肉で解決してあげる必要があります。

循環参照のサンプルコード

The Swift Programming Languageのサンプルコードをベースに、コメントで解説しています。

strong_reference_cycle_sample.swift
class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) is being deinitialized") }
}

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    var tenant: Person?
    deinit { print("Apartment \(unit) is being deinitialized") }
}

var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed") // johnの参照カウント1
unit4A = Apartment(unit: "4A")        // unit4Aの参照カウント1

john!.apartment = unit4A // unit4Aの参照カウント2 (*事件現場A)
unit4A!.tenant = john    // johnの参照カウント2 (*事件現場B)

john = nil   // johnの参照カウント1
unit4A = nil // unit4Aの参照カウント1

// 何もprintされない

// _人人人人人人人人人人人人人人人人人人_
// > johnとunit4Aが開放されない!! <
//  ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^ ̄

循環参照の解決方法

strong参照

今まで、何度も"参照"というワードを使ってきましたが、厳密にはstrong参照と呼ばれるものを指しています。
strong参照は、以下のように記述します。

strong var john = Person(name: "John Appleseed")

// Swiftではデフォルトでstrong参照になるため、"strong"は省略できる
var haranicle = Person(name: "Kazushi Hara")

前述しましたが、strong参照するとインスタンスの参照カウントが+1されます。  

循環参照のサンプルコードの事件現場Aか事件現場Bで参照カウントが増えなければ、今回のような悲惨な結果には、ならなかったはずです。

Swiftは、参照カウントが増えない(参照カウントのカウント対象にならない)参照の方法を2つ提供しています。

weak参照

weak参照は以下のような特徴があります。

  • 参照しても、参照カウントが増えない
  • 参照先がnilになることがある
    • Optional型
  • 定数で使用することはできない
    • 使おうとしたらXcodeで以下のようなエラーが出ます
      'weak'must be a mutable variable, because it may change at runtime

使用するパターン

以下のようなパターンで使います。
* AはBの参照をもつことができる
* BはAの参照をもつことができる

weak参照のサンプルコード

The Swift Programming Languageのサンプルコードをベースに、コメントで解説しています。

weak_reference_sample.swift
class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) is being deinitialized") }
}

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    weak var tenant: Person? // weakになっている!
    deinit { print("Apartment \(unit) is being deinitialized") }
}

var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed") // johnの参照カウント1
unit4A = Apartment(unit: "4A") // unit4Aの参照カウント1

john!.apartment = unit4A // unit4Aの参照カウント2
unit4A!.tenant = john // johnの参照カウント1

john = nil // johnの参照カウント0

// John Appleseed is being deinitializedとprintされる

unit4A = nil // unit4Aの参照カウント0

// Apartment 4A is being deinitializedとprintされる

unowned参照

unowned参照は以下のような特徴があります。

  • 参照しても、参照カウントが増えない
  • 参照先がnilになることはない
    • Optional型ではない
    • 参照先が解放済みのとき、unowned参照のプロパティ、変数にアクセスするとランタイムエラーでアプリがクラッシュする
    • 絶対に参照先が開放されていない自身があるときはunownedを使用する
  • 定数で使用することはできない
    • 使おうとしたらXcodeで以下のようなエラーが出ます
      'weak'must be a mutable variable, because it may change at runtime

使用するパターン

以下のようなパターンで使います。
* AはBの参照をもつことができる
* BはAの参照が必要

unowned参照のサンプルコード

The Swift Programming Languageのサンプルコードをベースに、コメントで解説しています。

unowned_reference_sample.swift
class Customer {
    let name: String
    var card: CreditCard?
    init(name: String) {
        self.name = name
    }
    deinit { print("\(name) is being deinitialized");}
}

class CreditCard {
    let number: UInt64
    unowned let customer: Customer // unownedになっている!
    init(number: UInt64, customer: Customer) {
        self.number = number
        self.customer = customer // initでcustomerをセットするので、CreditCardのインスタンスが生きている間にcustomerがnilになることはない
    }
    deinit { print("Card #\(number) is being deinitialized");}
}

var john: Customer? = Customer(name: "John Appleseed") // johnの参照カウントは1

john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!) // johnの参照カウントは1, cardの参照カウントは1

john = nil // johnの参照カウントは0, cardの参照カウントは0

// John Appleseed is being deinitializedとprintされる
// Card #1234567890123456 is being deinitializedとprintされる

unowned参照とImplicitly Unwrapped Optionalプロパティ

Implicitly Unwrapped Optionalは、アクセスするときにnilだったらランタイムエラーでアプリがクラッシュするプロパティ、変数のことです。
Optional型ではないため、Unwrapせずに値を使用することができます。
詳しくは、以下を参照してください。
The Swift Programming Language (Swift 2.1): The Basics

使用するパターン

以下の様なパターンが発生することがあります。ほとんど無いですが。

  • AはBの参照が必要
  • BはAの参照が必要

unowned参照とImplicitly Unwrapped Optionalプロパティサンプルコード

The Swift Programming Languageのサンプルコードをベースに、コメントで解説しています。

class Country {
    let name: String
    var capitalCity: City! // Implicitly Unwrapped Optionalなので、解放済みのときにアクセスするとランタイムエラーでアプリがクラッシュ
    init(name: String, capitalName: String) {
        self.name = name
        self.capitalCity = City(name: capitalName, country: self)
    }
}

class City {
    let name: String
    unowned let country: Country // unownedなので、解放済みのときにアクセスするとランタイムエラーでアプリがクラッシュ
    init(name: String, country: Country) {
        self.name = name
        self.country = country
    }
}

Closureと循環参照

Closureも参照型なので、循環参照が発生することがあります。
Closureで循環参照が発生するのは以下のパターンです。

  1. HogeクラスのインスタンスにClosureへのstrong参照のプロパティがある
  2. 1.のClosure内で、Hogeクラスのプロパティ、メソッドにアクセスしている

Closureで循環参照サンプルコード

The Swift Programming Languageのサンプルコードをベースに、コメントで解説しています。

closure_sample.swift
class HTMLElement {

    let name: String
    let text: String?

    // プロパティでasHTMLへのstrong参照を持っている
    lazy var asHTML: Void -> String = { // nameとtextを使いたいからlazy
        // Closure内でselfのプロパティをキャプチャしている
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

    deinit {
        print("\(name) is being deinitialized")
    }
}

筋肉クイズ1

突然ですが、筋肉クイズの時間です。
closure_sample.swiftのクラス定義がある時...

closure_sample_quiz1.swift
var heading:HTMLElement? = HTMLElement(name: "h1")
let defaultText = "some default text"
heading!.asHTML = {
    return "<\(heading!.name)>\(heading!.text ?? defaultText)</\(heading!.name)>"
}
print(heading!.asHTML())

// <h1>some default text</h1>とprintされる

heading = nil

// ★問題★ ここではどうなるでしょう?
// (A) h1 is being deinitializedとprintされる
// (B) なにもprintされない

筋肉クイズ2

closure_sample.swiftのクラス定義がある時...

closure_sample_quiz2.swift
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())

// <p>hello, world</p>とprintされる

paragraph = nil

// ★問題★ ここではどうなるでしょう?
// (A) p is being deinitializedとprintされる
// (B) なにもprintされない

解答はキャプチャリストの後で

キャプチャリスト

Swiftではキャプチャリストという文法で、Closure内で使用するプロパティ、定数、変数のキャプチャの仕方(weak, unowned)を指定することができます。
詳しくは、以下を参照してください。
The Swift Programming Language (Swift 2.1): Expressions

引数、戻り値がある場合

capturelist_sample1.swift
lazy var someClosure: (Int, String) -> String = {
    [unowned self, weak delegate = self.delegate!, userData] (index: Int, stringToProcess: String) -> String in
    // Closure内の処理
}

引数、戻り値がない場合

capturelist_sample2.swift
lazy var someClosure: Void -> String = {
    [unowned self, weak delegate = self.delegate!, userData] in
    // Closure内の処理
}

筋肉クイズ1の解答

答えは...(A) p is being deinitializedとprintされる。

原因はよくわかってないです...(だれか教えて下さい)

キャプチャリストを使って以下のように書き換えると、何もprintされなくなります。

closure_sample_quiz1_answer.swift
heading!.asHTML = {
    [heading] in
    return "<\(heading!.name)>\(heading!.text ?? defaultText)</\(heading!.name)>"
}

@_tid_ さんがBox化されているという解釈で説明してくれました!
Swift - クロージャのメモリ管理について - Qiita

筋肉クイズ2の解答

答えは...(B) なにもprintされない
asHTMLのイニシャライザでセットされているクロージャ内でselfがキャプチャされるためです。

HTMLElementのプロパティasHTMLを以下のように書き換えると、p is being deinitializedとprintされるようになります。

closure_sample_quiz2_answer.swift
lazy var asHTML: Void -> String = {
    [weak self] in
    if let text = self.text {
        return "<\(self.name)>\(text)</\(self.name)>"
    } else {
        return "<\(self.name) />"
    }
}

言いたかったこと

(2015/12/18 追記)

  • 以下の2つの条件を満たすと循環参照する可能性がある
    • プロパティにクロージャを持つ
    • クロージャ内でプロパティの持ち主を参照する
  • コーディング(コードレビュー)している時にどのパターンで循環参照するか考えるのは大変すぎる
  • プロパティのクロージャからプロパティの持ち主を参照するときは必ず、キャプチャリストでweakかunownedをつけていると安心できる
  • コーディング規約で縛ろう💪

筋肉コーディング規約

あなたの筋肉をつかって、チームのコーディング規約に以下をコピペしましょう
('を`に置換してください)

## キャプチャリスト

循環参照の発生を防ぐために、プロパティにClosureを宣言するときは、 
キャプチャリストで'''weak'''または'''unowned'''を指定すること💪💪💪

### Closure内でselfを参照する場合

'''
class HTMLElement {
    // (中略)
    lazy var asHTML: Void -> String = {
        [weak self] in // ←💪
        if let text = self!.text {
            return "<\(self!.name)>\(text)</\(self!.name)>"
        } else {
            return "<\(self!.name) />"
        }
    }
}
'''

プロパティのClosureのasHTMLがselfを参照するので'''[weak self]'''

### Closureの所有者とClosureの参照先が同じとき

'''
var paragraph: HTMLElement = HTMLElement()
paragraph.asHTML = { [weak paragraph] in // ←💪
    guard let paragraph = paragraph else  {
        return ""
    }
    return "<\(paragraph.name)>\(paragraph.text)</\(paragraph.name)>"
}
'''

paragraphがasHTMLを参照し、asHTMLがparagraphを参照するので'''[weak paragraph]'''

参照カウントについてもっと詳しく知りたい人へ

参照カウントの実装は、このあたりにありそうです。

swift/HeapObject.cpp at master · apple/swift · GitHub

おわりに

これであなたも立派な筋肉Swiftプログラマーです。
明日から、ダンベル片手に出社しましょう。