我们可以在 Swift 中创建具有非可选属性的类型擦除弱引用吗?

Jon*_*nah 5 swift

一些背景

鉴于 Swift 目前无法支持传递泛型类型参数,类型擦除容器是 Swift 中有用的结构。社区对此有一些很好的解释:

这是一个例子:

protocol View: class {
    associatedtype ViewModel: Equatable

    var viewModel: ViewModel! { get set }

    func render(_ viewModel: ViewModel)
}

class _AnyViewBoxBase<T: Equatable>: View {

    var viewModel: T!

    func render(_ viewModel: T) {
        fatalError()
    }
}

final class _ViewBox<Base: View>: _AnyViewBoxBase<Base.ViewModel> {

    var base: Base!

    override var viewModel: Base.ViewModel! {
        get {
            return base.viewModel
        }
        set {
            base.viewModel = newValue
        }
    }

    init(_ base: Base) {
        self.base = base
    }

    override func render(_ viewModel: Base.ViewModel) {
        base.render(viewModel)
    }
}

final class AnyView<T: Equatable>: View {

    var _box: _AnyViewBoxBase<T>

    var viewModel: T! {
        get {
            return _box.viewModel
        }
        set {
            _box.viewModel = newValue
        }
    }

    func render(_ viewModel: T) {
        _box.render(viewModel)
    }

    init<Base: View>(_ base: Base) where Base.ViewModel == T {
        _box = _ViewBox(base)
    }
}

struct ExampleViewModel {
    let content: String
}

extension ExampleViewModel: Equatable {
    static func ==(lhs: ExampleViewModel, rhs: ExampleViewModel) -> Bool {
        return lhs.content == rhs.content
    }
}

final class Example: View {
    var viewModel: ExampleViewModel!

    init(viewModel: ExampleViewModel) {
        self.viewModel = viewModel
    }

    func render(_ viewModel: ExampleViewModel) {
    }
}
Run Code Online (Sandbox Code Playgroud)

这些类型擦除框允许我们构造通用容器或创建必须符合特定类型的通用协议但不限于具体实现的属性。例如,使用AnyView下面的内容,我可以轻松地交换视图测试替身。

struct TypeUnderTest {
    var view: AnyView<ExampleViewModel>
}

var example = Example(viewModel: ExampleViewModel(content: "hello"))
var instanceUnderTest = TypeUnderTest(view: AnyView(example))
Run Code Online (Sandbox Code Playgroud)

到目前为止,一切都很好。我可以类似地定义View具有可选或非可选(而不是隐式展开可选)viewModel属性并相应地按框进行更新。

但是,如果我希望我的类型擦除属性成为引用怎么办?

weak var view: AnyView<ExampleViewModel>不行。这将使我只留下对框类型的弱引用,并且它将立即被释放。

var view: WeakAnyView<ExampleViewModel>让我们更亲近。我们可以创建一个弱引用其内容的框。如果我们的View协议仅定义可选属性,那么我们就可以开始:

protocol View: class {
    associatedtype ViewModel: Equatable

    var viewModel: ViewModel? { get set }

    func render(_ viewModel: ViewModel)
}

class _AnyViewBoxBase<T: Equatable>: View {

    var viewModel: T?

    func render(_ viewModel: T) {
        fatalError()
    }
}

final class _ViewBox<Base: View>: _AnyViewBoxBase<Base.ViewModel> {

    weak var base: Base?

    override var viewModel: Base.ViewModel? {
        get {
            return base?.viewModel
        }
        set {
            base?.viewModel = newValue
        }
    }

    init(_ base: Base) {
        self.base = base
    }

    override func render(_ viewModel: Base.ViewModel) {
        base?.render(viewModel)
    }
}

final class AnyView<T: Equatable>: View {

    var _box: _AnyViewBoxBase<T>

    var viewModel: T? {
        get {
            return _box.viewModel
        }
        set {
            _box.viewModel = newValue
        }
    }

    func render(_ viewModel: T) {
        _box.render(viewModel)
    }

    init<Base: View>(_ base: Base) where Base.ViewModel == T {
        _box = _ViewBox(base)
    }
}

struct ExampleViewModel {
    let content: String
}

extension ExampleViewModel: Equatable {
    static func ==(lhs: ExampleViewModel, rhs: ExampleViewModel) -> Bool {
        return lhs.content == rhs.content
    }
}

final class Example: View {
    var viewModel: ExampleViewModel?

    init(viewModel: ExampleViewModel?) {
        self.viewModel = viewModel
    }

    func render(_ viewModel: ExampleViewModel) {
    }
}

struct TypeUnderTest {
    var view: AnyView<ExampleViewModel>
}

let viewModel = ExampleViewModel(content: "hello")
var example: Example? = Example(viewModel: viewModel)
let instanceUnderTest = TypeUnderTest(view: AnyView(example!))
instanceUnderTest.view.viewModel
example = nil
instanceUnderTest.view.viewModel
Run Code Online (Sandbox Code Playgroud)

但是,如果我删除的协议 ( View) 定义了非可选属性,那么我们就会遇到问题。_ViewBox必须定义一个非可选的viewModel来符合View,但这迫使我们忽略我们的弱引用装箱类型将被释放的非常现实的可能性,并且我们没有一种安全的方法来将此信息传达给调用者。

一种选择是添加另一个框,但这使用起来变得很痛苦:

protocol View: class {
    associatedtype ViewModel: Equatable

    var viewModel: ViewModel { get set }

    func render(_ viewModel: ViewModel)
}

class _AnyViewBoxBase<T: Equatable>: View {

    var viewModel: T

    func render(_ viewModel: T) {
        fatalError()
    }

    init(viewModel: T) {
        self.viewModel = viewModel
    }

    var empty: Bool {
        get {
            return false
        }
    }
}

final class _ViewBox<Base: View>: _AnyViewBoxBase<Base.ViewModel> {

    weak var base: Base?

    override var viewModel: Base.ViewModel {
        get {
            return base!.viewModel
        }
        set {
            base?.viewModel = newValue
        }
    }

    init(_ base: Base) {
        super.init(viewModel: base.viewModel)
        self.base = base
    }

    override func render(_ viewModel: Base.ViewModel) {
        base?.render(viewModel)
    }

    override var empty: Bool {
        get {
            return base == nil
        }
    }
}

final class AnyView<T: Equatable>: View {

    var _box: _AnyViewBoxBase<T>

    var viewModel: T {
        get {
            return _box.viewModel
        }
        set {
            _box.viewModel = newValue
        }
    }

    func render(_ viewModel: T) {
        _box.render(viewModel)
    }

    init<Base: View>(_ base: Base) where Base.ViewModel == T {
        _box = _ViewBox(base)
    }

    var empty: Bool {
        return _box.empty
    }
}

struct AnyViewOptionalBox<T: Equatable> {

    private var _view: AnyView<T>?
    var view: AnyView<T>? {
        get {
            if let view = self._view, view.empty == false {
                return view
            } else {
                return nil
            }
        }
        set {
            self._view = newValue
        }
    }

    init(view: AnyView<T>) {
        self.view = view
    }
}

struct ExampleViewModel {
    let content: String
}

extension ExampleViewModel: Equatable {
    static func ==(lhs: ExampleViewModel, rhs: ExampleViewModel) -> Bool {
        return lhs.content == rhs.content
    }
}

final class Example: View {
    var viewModel: ExampleViewModel

    init(viewModel: ExampleViewModel) {
        self.viewModel = viewModel
    }

    func render(_ viewModel: ExampleViewModel) {
    }
}

struct TypeUnderTest {
    var viewBox: AnyViewOptionalBox<ExampleViewModel>
}

let viewModel = ExampleViewModel(content: "hello")
var example: Example? = Example(viewModel: viewModel)
let anyView: AnyView<ExampleViewModel> = AnyView(example!)
let anyViewOptional: AnyViewOptionalBox<ExampleViewModel> = AnyViewOptionalBox(view: anyView)
let instanceUnderTest = TypeUnderTest(viewBox: anyViewOptional)
instanceUnderTest.viewBox.view?.viewModel.content
example = nil
instanceUnderTest.viewBox.view?.viewModel.content
Run Code Online (Sandbox Code Playgroud)

有没有更好的方法来维护对类型擦除属性的弱引用?

Wer*_*her 3

基本上,您想要的是将类型擦除框的生命周期链接到它包含的对象的生命周期,这样一旦包含的对象被释放,该框就会被释放。

一种方法是确保框仅弱引用所包含的对象,并使用 objc_setAssociatedObject(...) 使框成为所包含对象的关联对象。这样,您基本上就颠倒了两个对象之间的所有权关系。

请参阅下面的游乐场示例:

import ObjectiveC

protocol View: class {
    associatedtype ViewModel: Equatable

    var viewModel: ViewModel { get set }

    func render()
}

private var AssociatedObjectHandle: UInt8 = 0

final class AnyView<T: Equatable>: View {

    let _viewModelGetter: () -> T
    let _viewModelSetter: (T) -> Void
    let _render: () -> Void

    init<Base: View>(_ base: Base) where Base.ViewModel == T {
        //Ensure this object doesn't reference base, so there is no retain cycle
        _viewModelGetter = { [weak base] in
            //You can force unwrap, because it is guaranteed that base is not deallocated because of the association
            return base!.viewModel
        }
        _viewModelSetter = { [weak base] in
            //You can force unwrap, because it is guaranteed that base is not deallocated because of the association
            base!.viewModel = $0
        }
        _render = { [weak base] in
            //You can force unwrap, because it is guaranteed that base is not deallocated because of the association
            base!.render()
        }

        //Associate this object with the base, so it gets deallocated when base gets deallocated, also base is guaranteed to exist during our lifetime
        objc_setAssociatedObject(base, &AssociatedObjectHandle, self, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
    }

    deinit {
        print("dealloc: \(self)")
    }

    var viewModel: T {
        get {
            return _viewModelGetter()
        }
        set {
            _viewModelSetter(newValue)
        }
    }

    func render() {
        _render()
    }
}

class ConcreteView: View {
    typealias ViewModel = String

    var viewModel: String

    init(viewModel: String) {
        self.viewModel = viewModel
    }

    deinit {
        print("dealloc: \(self)")
    }

    func render() {
        print("viewModel: \(viewModel)")
    }
}


weak var anyView: AnyView<String>?
autoreleasepool {
    var concreteView = ConcreteView(viewModel: "Test")
    autoreleasepool {
        anyView = AnyView(concreteView)

        //Any view should render correctly because concrete view exists
        anyView!.render()
    }
    //Success: anyView is not nil yet, because concreteView still exists
    anyView!.render()
}
//Crash: anyView is now nil
anyView!.render()
Run Code Online (Sandbox Code Playgroud)

输出:

viewModel: Test
viewModel: Test
dealloc: __lldb_expr_34.ConcreteView
dealloc: __lldb_expr_34.AnyView<Swift.String>
Fatal error: Unexpectedly found nil while unwrapping an Optional value
Run Code Online (Sandbox Code Playgroud)