LoginSignup
150
143

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-12-07

はじめに

こんにちは。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プログラマーです。
明日から、ダンベル片手に出社しましょう。

150
143
3

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
150
143