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

Posted

技术标签:

【中文标题】我们可以在 Swift 中创建具有非可选属性的类型擦除弱引用吗?【英文标题】:Can we create type erasing weak references with non-optional properties in Swift? 【发布时间】:2017-12-09 02:53:41 【问题描述】:

一些背景

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

http://www.russbishop.net/type-erasure https://realm.io/news/tryswift-gwendolyn-weston-type-erasure/ https://www.bignerdranch.com/blog/breaking-down-type-erasures-in-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) 
    

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

struct TypeUnderTest 
    var view: AnyView<ExampleViewModel>


var example = Example(viewModel: ExampleViewModel(content: "hello"))
var instanceUnderTest = TypeUnderTest(view: AnyView(example))

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

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

weak var view: AnyView&lt;ExampleViewModel&gt; 不好。这将只留下对盒子类型的弱引用,并且会立即被释放。

var view: WeakAnyView&lt;ExampleViewModel&gt; 让我们更接近。我们可以创建一个弱引用其内容的盒子。如果我们的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

但是,如果我删除的协议 (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

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

【问题讨论】:

为长代码示例道歉,但至少您可以将这些全部放到操场上看看发生了什么。 【参考方案1】:

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

一种方法是确保盒子只弱引用包含的对象,并使用 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()

输出:

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

【讨论】:

以上是关于我们可以在 Swift 中创建具有非可选属性的类型擦除弱引用吗?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Spark 2.1 中创建可以具有可选属性的类型化数据集

是否可以在 Swift 中创建具有 Self 或关联类型要求的通用计算属性,如果可以,如何?

防止 Obj-C 代码将 `nil` 传递给具有非可选参数的 Swift 方法

Swift之深入解析如何处理非可选的可选项类型

斯威夫特 3;用非可选字符串附加可选字符串

在情节提要中使用 segues 使视图控制器具有非可选属性?