はじめに
下記順番でアウトプットしていく。
- 循環参照とは?
- 循環参照の例
- 循環参照したときの修正案(さらっとまとめなので一部アウトプット)
- おまけ(ちょっと複雑な循環参照の可能性を示唆するコードの修正案)
循環参照とは?
循環参照とは、Swiftプログラムでクラス同士がお互いに強い参照(強参照)
を持つときに発生する。
これにより、メモリが正しく解放されず、プログラムが予期しない挙動をする可能性がある。(アプリが落ちるとかね)
循環参照の例
例えば、クラスAがクラスBを保持し、同時にクラスBもクラスAを保持する場合、お互いが解放されない状態になる。下記コードは、最後の行のinstanceA = nil
とinstanceB = nil
を実行しても、クラスAとクラスBのインスタンスは解放されず、解放されたメッセージも表示されない。
Swift
class ClassA {
var classB: ClassB? // AでBを保持するため型を定義
deinit {
print("ClassAが解放されました")
}
}
class ClassB {
var classA: ClassA? // BでAを保持するため型を定義
deinit {
print("ClassBが解放されました")
}
}
var instanceA: ClassA? = ClassA()
var instanceB: ClassB? = ClassB()
instanceA?.classB = instanceB // AでBを保持する
instanceB?.classA = instanceA // BでAを保持する
instanceA = nil // インスタンスは解放されない
instanceB = nil // インスタンスは解放されない
循環参照したときの修正案(さらっとまとめなので一部アウトプット)
循環参照を避けるためには、クラスAかクラスBのどちらかのプロパティにweak修飾子
、つまり、弱い参照(弱参照)
を使用する。または参照関係を見直す。
今回は、弱い参照(弱参照)
の方で対応する。
Swift
class ClassA {
var classB: ClassB?
deinit {
print("ClassAが解放されました")
}
}
class ClassB {
weak var classA: ClassA? // weak付けて弱参照にする
deinit {
print("ClassBが解放されました")
}
}
var instanceA: ClassA? = ClassA()
var instanceB: ClassB? = ClassB()
instanceA?.classB = instanceB
instanceB?.classA = instanceA
// 循環参照が解消され、インスタンスが適切に解放されるようになる。
instanceA = nil
instanceB = nil
おまけ(ちょっと複雑な循環参照の可能性を示唆するコードの修正案)
- 前提
- MVVMを想定した上でSwiftUIを使用し、Combineを用いてAPI通信を行う。
- ModelとViewModelの部分である。
-
循環参照です!
がメインなのでデータ取得箇所などあまりよろしくないコードである。
struct ItemModel: Codable {
let id: Int
let title: String
}
class ItemViewModel: ObservableObject {
private let apiService = APIService()
@Published var items: [ItemModel] = []
@Published var errorMessage: String = ""
private var cancellables = Set<AnyCancellable>()
func fetchItems() {
apiService.fetchItems()
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { [weak self] completion in
switch completion {
case .failure(let error):
self?.errorMessage = "An error occurred: \(error.localizedDescription)"
case .finished:
break
}
},
receiveValue: { [weak self] items in
self?.items = items
self?.errorMessage = ""
})
.store(in: &cancellables)
}
}
class APIService {
func fetchItems() -> AnyPublisher<[ItemModel], Error> {
let url = URL(string: "https://api.example.com/items")! // 仮のAPIエンドポイント
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: [ItemModel].self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
}
おわりに
間違い等ありましたらコメント欄にてご指摘ください!
参考記事
開発環境
- Xcode-14.3
- Swift version 5.8