【问题标题】:Swift: conformance to the Protocol with Generic method with "where" clauseSwift:使用带有“where”子句的通用方法符合协议
【发布时间】:2019-05-31 06:50:05
【问题描述】:

总结:

我想创建一个Class<T>,其中有一个对应的ClassDelegate 协议,其中包含func<T>

目标:

使用多个对象类重用单个对象和行为。接收已具有专用类的委托回调,而无需将对象强制转换为特定类以使用它。

示例代码:

具有通用方法的协议:

protocol GenericTableControllerDelegate: AnyObject {
    func controller<T>(controller: GenericTableController<T>, didSelect value: T)
}

通用基础UITableViewController 子类:

open class GenericTableController<DataType>: UITableViewController {
    weak var delegate: GenericTableControllerDelegate?
    var data = [DataType]()

    open override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let item = data[indexPath.row]
        delegate?.controller(controller: self, didSelect: item)
    }
}

GenericTableController 的专用版本:

final class SpecializedTableController: GenericTableController<NSObject> {}

SpecializedTableController 的客户端 - 实现结果,但需要类型转换才能访问专用数据类型:

final class ClientOfTableController: UIViewController, GenericTableControllerDelegate {
    // Works OK
    func controller<T>(controller: GenericTableController<T>, didSelect value: T) {
        if let value = value as? NSObject {
            // Requires unwrapping and casting
        }
    }
}

SpecializedTableController 的客户端,具有“where”要求 - 唯一无法编译的问题

final class AnotherClientOfTableController: UIViewController, GenericTableControllerDelegate {
    // Works OK
    func controller<T>(controller: GenericTableController<T>, didSelect value: T) where T: NSObject {
        // `value` is String
    }    
}

类型“AnotherClientOfTableController”不符合协议 'GenericTableControllerDelegate' 你想添加协议存根吗?

有没有办法让协议具有通用方法并能够在该方法实现中具有具体(专用)类型?

是否存在满足类似要求的相近替代方案(具有泛型类但能够在委托回调中处理具体类型)?

【问题讨论】:

    标签: ios swift generics protocols swift-protocols


    【解决方案1】:

    你的错误在于协议:

    protocol GenericTableControllerDelegate: AnyObject {
        func controller<T>(controller: GenericTableController<T>, didSelect value: T)
    }
    

    这表示为了成为 GTCD,类型必须接受 any 类型 T 传递给此函数。但你不是那个意思。你的意思是:

    public protocol GenericTableControllerDelegate: AnyObject {
        associatedtype DataType
        func controller(controller: GenericTableController<DataType>, didSelect value: DataType)
    }
    

    然后您希望委托的 DataType 与表视图的 DataType 匹配。这让我们进入了 PAT(具有关联类型的协议)、类型橡皮擦和 generalized existentials(在 Swift 中尚不存在)的世界,实际上它只是变得一团糟。

    虽然这是一个特别适合泛化存在的用例(如果它们曾经被添加到 Swift 中),但在很多情况下,你可能无论如何都不想要这个。委托模式是在添加闭包之前开发的一种 ObjC 模式。过去在 ObjC 中传递函数非常困难,因此即使是非常简单的回调也变成了委托。在大多数情况下,我认为 Richard Topchiy 的方法是完全正确的。只需传递一个函数。

    但是如果你真的想保持代理风格呢?我们(几乎)可以做到这一点。一个小故障是您不能拥有名为delegate 的属性。你可以设置它,但你不能获取它。

    open class GenericTableController<DataType>: UITableViewController
    {
        // This is the function to actually call
        private var didSelect: ((DataType) -> Void)?
    
        // We can set the delegate using any implementer of the protocol
        // But it has to be called `controller.setDelegate(self)`.
        public func setDelegate<Delegate: GenericTableControllerDelegate>(_ d: Delegate?)
            where Delegate.DataType == DataType {
                if let d = d {
                    didSelect = { [weak d, weak self] in
                        if let self = self { d?.controller(controller: self, didSelect: $0) }
                    }
                } else {
                    didSelect = nil
                }
        }
    
        var data = [DataType]()
    
        // and here, just call our internal method
        open override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            let item = data[indexPath.row]
            didSelect?(item)
        }
    }
    

    这是一种有助于理解的有用技术,但在大多数情况下我可能不会使用它。如果这些方法引用了 DataType,那么当您添加更多方法时肯定会令人头疼。你需要很多样板。请注意,由于将self 传递给委托方法,会有些混乱。这是委托方法需要的东西,但闭包不需要(如果闭包需要,您总是可以在闭包中捕获控制器)。

    当您探索这种可重用的代码时,我鼓励您更多地考虑封装策略,而不是对象和委托协议。封装策略的一个示例是拥有一个您将其交给控制器的 SelectionHandler 类型:

    struct SelectionHandler<Element> {
        let didSelect: (Element) -> Void
    }
    

    有了它,您可以构建简单的策略,例如“打印出来”:

    extension SelectionHandler {
        static func printSelection() -> SelectionHandler {
            return SelectionHandler { print($0) }
        }
    }
    

    或者更有趣的是,更新一个标签:

    static func update(label: UILabel) -> SelectionHandler {
        return SelectionHandler { [weak label] in label?.text = "\($0)" }
    }
    

    那么你会得到如下代码:

    controller.selectionHandler = .update(label: self.nameLabel)
    

    或者,更有趣的是,您可以构建高阶类型:

    static func combine(_ handlers: [SelectionHandler]) -> SelectionHandler {
        return SelectionHandler {
            for handler in handlers {
                handler.didSelect($0)
            }
        }
    }
    
    static func trace(_ handler: SelectionHandler) -> SelectionHandler {
        return .combine([.printSelection(), handler])
    }
    
    controller.selectionHandler = .trace(.update(label: self.nameLabel))
    

    这种方法比委托更强大,并开始释放 Swift 的真正优势。

    【讨论】:

    • 您好 Rob,感谢您对文章和代码示例中的泛型和存在性以及命令/策略封装的示例进行了广泛的概述。看起来很有希望,但对于我想要的用例来说有点过分了。我想到的目标之一是,在调用站点有一个干净的语法,并且在内存管理方面没有任何问题。
    • Richard Topchiy 的方法很好,但是在强烈捕获self(即调用实体)方面存在不足,并且可能会产生保留循环。你对setDelegate 的想法绝对很有趣。似乎 Swift 还不支持以简洁易用的方式表达这种关系,所以我可能会使用您的或其他一些方法来封装通用选择器。
    【解决方案2】:

    从某种意义上说,我认为这是不可行的。最接近的是与子类结合。考虑以下几点:

    protocol MagicProtocol {
        func dooMagic<T>(_ trick: T)
    }
    
    class Magician<TrickType> {
        private let listener: MagicProtocol
        private let tricks: [TrickType]
        init(listener: MagicProtocol, tricks: [TrickType]) { self.listener = listener; self.tricks = tricks }
        func abracadabra() { listener.dooMagic(tricks.randomElement()) }
    }
    
    class Audience<DataType>: MagicProtocol {
    
        var magician: Magician<DataType>?
    
        init() {
            magician?.abracadabra()
        }
    
        func doExplicitMagic(_ trick: DataType) {
    
        }
    
        func dooMagic<T>(_ trick: T) {
            doExplicitMagic(trick as! DataType)
        }
    
    }
    

    现在我可以创建一个子类并将其限制为某种类型:

    class IntegerAudience: Audience<Int> {
    
        override func doExplicitMagic(_ trick: Int) {
            print("This works")
        }
    
    }
    

    问题是这两个泛型之间没有相关性。所以在某些时候必须进行演员表。这里我们在协议方法中做:

    doExplicitMagic(trick as! DataType)
    

    看起来这很安全,它永远不会崩溃,但如果你仔细观察,我们可以这样做:

    func makeThingsGoWrong() {
        let myAudience = IntegerAudience()
        let evilMagician = Magician(listener: myAudience, tricks: ["Time to burn"])
        evilMagician.abracadabra() // This should crash the app
    }
    

    这里的myAudience 对应于协议MagicProtocol,它可能不限于泛型。但myAudience 仅限于Int。没有什么能阻止编译器,但如果它停止了,错误会是什么?

    无论如何,只要您正确使用它,它就可以工作。如果你不这样做,它就会崩溃。你可以做一个可选的展开,但我不确定它是否合适。

    【讨论】:

      【解决方案3】:

      解决这种情况的一种可能方法是使用回调而不是委托。通过传递的不是闭包而是实例方法,它看起来几乎与委托模式相同:

      open class GenericTableController2<DataType>: UITableViewController {
          var onSelect: ((DataType) -> Void)?
          var data = [DataType]()
      
          open override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
              let item = data[indexPath.row]
              onSelect?(item)
          }
      }
      
      final class CallbackExample: GenericTableController2<NSObject> {
      }
      
      final class CallBackClient: UIViewController {
          override func viewDidLoad() {
              super.viewDidLoad()
              let vc = CallbackExample()
              vc.onSelect = handleSelection
          }
      
          func handleSelection(_ object: NSObject) {
      
          }
      }
      

      作为一个好处,代码非常简单,并且不涉及 Swift 类型系统的任何高级解决方法,这在处理泛型和协议时经常会出现一些问题。

      【讨论】:

      • 我绝对同意在这里使用回调而不是委托,但是像这样传递方法通常会创建一个保留循环并且需要非常小心。 vc.onSelect = handleSelection 隐式捕获 self 作为强引用。在这些情况下,您通常需要使用[weak self]。在这种特定情况下,它不是因为 vc 是一个局部变量,但我怀疑在任何实际代码中,vc 将是一个属性。
      • @RobNapier 同意,好点。您能否改进这个想法,以便我可以同时使用命名方法(非匿名)并使CallbackExample 控制器弱捕获它?所以,万一我把它的引用存储在 `CallBackClient 的某个地方,它仍然会被释放。
      • 我刚刚检查了一个简单的演示项目,确实,当存储对新创建的视图控制器的强引用时,存储回调也会创建一个保留周期。
      • 我不相信有任何类似的语法,但我添加了一个对此进行扩展的答案。正如你所说,函数绝对是 IMO 的道路。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多