Swift: 使用“where”子句的泛型方法符合协议

5

概要:

我想创建一个 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
        }
    }
}

使用“where”要求的SpecializedTableController客户端 - 唯一的问题是它无法编译

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

类型'AnotherClientOfTableController'不符合协议'GenericTableControllerDelegate',您是否想添加协议存根?

是否有一种方法可以使用泛型方法并能够在该方法实现中具体化(专门化)类型?

有没有类似的替代方案来满足类似的要求(拥有一个泛型类但能够在委托回调中处理具体类型)?

screenshot

3个回答

5

你的错误在于协议:

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

这意味着为了成为GTCD,类型必须接受传递给此函数的任何类型T。但是你并不是这个意思。你的意思是:
public protocol GenericTableControllerDelegate: AnyObject {
    associatedtype DataType
    func controller(controller: GenericTableController<DataType>, didSelect value: DataType)
}

然后您希望委托的DataType与表视图的DataType匹配。这将使我们进入PAT(具有关联类型的协议)、类型擦除器和广义存在(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的真正优势。

嗨,罗布,感谢您在文章和代码示例中对泛型和存在性的广泛概述,以及命令/策略封装的示例。看起来很有前途,但对于我想要的用例来说有些过度设计。我心目中的一个目标是,在调用站点具有清晰的语法,并且不会出现任何内存管理问题。 - Richard Topchii
Richard Topchiy的方法很好,但是在捕获“self”(即调用实体)方面存在不足,并且可能会创建保留周期。你使用setDelegate的想法绝对有趣。似乎Swift还没有以简洁易用的方式支持表达这种关系,因此我可能会使用你的或其他不同的方法来封装通用选择器。 - Richard Topchii

1
我认为你想要的方式并不可行。最接近的方法是与子类结合使用。考虑以下内容:
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的限制。编译器没有任何阻止,但如果有的话,错误会是什么?

无论如何,只要使用正确,它就可以工作。如果没有,则会崩溃。您可以进行可选解包,但我不确定是否适当。


1

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

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 的类型系统问题,这种问题通常在处理泛型和协议时会出现。

1
我完全同意在这里使用回调而不是委托,但像这样传递方法通常会创建一个保留循环并需要特别小心。vc.onSelect = handleSelection 隐式地将 self 捕获为强引用。在这些情况下,您经常需要使用 [weak self]。在这种特定情况下,它不需要,因为 vc 是一个局部变量,但我怀疑在任何实际代码中,vc 都将是一个属性。 - Rob Napier
@RobNapier 同意,说得好。你能否改进这个想法,让我既可以使用命名方法(而非匿名方法),又可以使 CallbackExample 控制器弱引用它?这样,在我将其存储在 CallBackClient 的某个位置时,它仍然会被释放。 - Richard Topchii
我刚刚用一个简单的演示项目进行了检查,确实,在存储新创建的视图控制器的强引用时,同时存储回调会创建一个保留循环。 - Richard Topchii
我不相信有任何这样的语法,但我已经添加了一个扩展答案。在我看来,函数绝对是正确的道路,就像你所说的那样。 - Rob Napier

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接