【iOS】「SwiftにおけるARC 基本とその先」の循環参照と弱参照について解説 ②

循環参照の解消方法

「弱参照は最後の手段に!!」 〜チーム開発で学んだメモリ管理の教訓〜

弱参照(weak)は確かに便利な宣言です。しかし、安易な導入は将来のコストを大きく膨らませる可能性があります。

弱参照を初期段階で避けるべき理由

  1. 予期せぬnil問題の温床になる
    • 弱参照した変数がいつ解放されるかは、コードの全体的な設計に依存します
    • 他の開発者が後から機能追加する際、「なぜこの変数がnilになるのか」の調査に時間を取られる
  2. 改修時の認知負荷が激増する
    • weak宣言された変数を使う箇所を修正する際、毎回「この変数のライフサイクルは?」を考え直す必要がある
    • 通常の強参照なら不要な検証作業が発生し続ける
  3. チーム開発での混乱を招く
    • 人数が増えるほど、各メンバーのオブジェクト管理の理解度にバラつきが出る
    • 一人が安易にweakを使うと、それが「お手本」として広まるリスクも

弱参照の罠 〜エンジニアあるあるストーリー〜 【1コマ目】プロジェクト終盤、納期2週間前 やばい! メモリリークだ! とりあえず weakで! 【2コマ目】3ヶ月後、新機能追加中... var delegate: weak var? // ??? なんでここweak? いつnilになるの? 調査に3時間... 【3コマ目】さらに2ヶ月後、本番環境で... CRASH! nil参照エラー unexpected nil... 原因調査と 修正対応で 2日間... 【4コマ目】教訓 結論: 初期設計に時間をかける 長期的なコスト削減! weakは緊急時のみ! チーム開発では影響が人数に比例して増大...

弱参照weakにより解決するコード例

次に記載しているサンプルコードでは

func testARC()travelerの参照はtraveler.account = accountが最後であるためARCの参照アカウントはこの後0となり、コンパイラにより解放されることになるが、次の行のaccount.printSummary()内ではtraveler!を強制アンラップして参照している。

この時travelerの解放が行われていた場合アプリはクラッシュになる。

サンプルコード


class Traveler {
  var name: String
  var account: Account?
  init(name: String, account: Account? = nil) {
    self.name = name
    self.account = account
  }
  deinit {
    print("Traveler deinit")
  }
}

class Account {
  weak var traveler: Traveler?
  var points: Int = 0
  
  init(traveler: Traveler? = nil, points: Int) {
    self.traveler = traveler
    self.points = points
  }
  
  deinit {
    print("Account deinit")
  }
  
  func printSummary() {
    print("\(traveler!.name) has \(points) points" )
    // Clasnhする可能性あるよ!!
    //  弱参照
  }
}

//   Accountを単独で生成しない
//
func testARC() {
  let traveler = Traveler(name: "Lily")
  let account = Account(traveler: traveler, points: 1000)
  traveler.account = account
  account.printSummary()
}

deinitでの解決

解決方法の一つとしてdeinitの使用を紹介されている。

ここではdeinitを使用した場合の懸念点のまとめとサンプルコードで解説していきます。

懸念点のまとめ

  • リアルタイムに処理が必要なケースでは更新が遅れるなどの不具合になる可能性がある。
  • メンテナンスによるコストが大きくなる。
    • 初めの設計・実装時は考慮していても後々に違うエンジニアに機能追加してもらう場合など見落としがちになる可能性がある。見落とした場合、タイミング系の不具合となりやすいため調査に時間を要する。

サンプルコード

次のサンプルコードではweak使用による問題の解決方法としてTraveler_deinitのdeinitで最終処理を行うケースを紹介しているが、deinitの処理タイミングはコンパイラの解放処理タイミングに依存するため処理タイミングが保証されない。

class Traveler_deinit {
  var name: String
  var id: UInt
  var destination: String?
  var travelMetrics: TravelMetrics = TravelMetrics()

  init(name: String, id: UInt, destination: String? = nil) {
    self.name = name
    self.id = id
    self.destination = destination
  }
  deinit {
    //  処理タイミングはシステムに委ねられるため実行タイミング
    //  意図していない場合あり
    travelMetrics.publish()
    travelMetrics.computeTravelInterest()
  }

  func updateDestination(_ destination: String) {
    self.destination = destination
    travelMetrics.destinations.append(destination)
  }
}


class TravelMetrics {
  let id: UInt = 0
  var destinations = [String]()
  var category: String?
  var published: Bool = false
  func computeTravelInterest() {
    print("id: \(id), destinations: \(destinations)")
  }
  func publish() {
    published = true
  }
  
}

//var metrics: TravelMetrics
func test()  {
  let traveler = Traveler_deinit(name: "Lily", id: 1000)
  
  traveler.updateDestination("Big Sur")
  traveler.updateDestination("Catalina") // ←ここで最終参照となる。
}

deinitでの懸念点も解消するパターン

解消する内容

  • 必要なデータ更新処理はメソッド化(publishAllMetrics())する。
  • Traveler_Bestクラスが保持するtravelMetricsのアクセス修飾子を外部に公開しない(private)とする。

サンプルコード


class Traveler_Best {
  var name: String
  var id: UInt
  var destination: String?
  private var travelMetrics: TravelMetrics = TravelMetrics()

  init(name: String, id: UInt, destination: String? = nil) {
    self.name = name
    self.id = id
    self.destination = destination
  }
  func publishAllMetrics() {
    travelMetrics.computeTravelInterest()
    travelMetrics.publish()
  }
  deinit {
    assert(travelMetrics.published)
  }

  func updateDestination(_ destination: String) {
    self.destination = destination
    travelMetrics.destinations.append(destination)
  }
}


//var metrics: TravelMetrics
func test_Best() {
  let traveler = Traveler_Best(name: "Lily", id: 1000)
  
  traveler.updateDestination("Big Sur")
  traveler.updateDestination("Catalina") // ←ここで最終参照となる。
  traveler.publishAllMetrics()
}

まとめ

SwiftによるARCを最大限に利用するために注意する3点

  •  Swiftにおいては循環参照はメモリーリークになるため注意すること。
  •  循環参照を回避するために弱参照を使用する前にオブジェクト(クラス)の関係を見直しで回避できないかを熟慮する。
  •  弱参照は基本的に使用しない。

初級者の方は、少し難しい内容であるため理由の理解は後回しにして上記3点を徹底的に注意すればよりメンテナンス性の良い設計・コードになるはずです。

経験・学習を積み上げていくうちに理由が理解できるようになるかと思います。

以上です。最後までお読みいただきありがとうございます。

なんでも良いのでコメント頂けますと嬉しいです。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA