Swift 中的关联协议

Associated protocol in Swift

我正在尝试抽象化从视图模型配置的视图。到目前为止,我一直在使用关联类型:

public protocol ViewModelProtocol: Equatable {}

public protocol ModeledView: class {
    /// The type of the view model
    associatedtype ViewModel: ViewModelProtocol

    var viewModel: ViewModel? { get }

    /// Sets the view model. A nil value describes a default state.
    func set(newViewModel: ViewModel?)
}

我可以像这样使用:

struct MyViewModel: ViewModelProtocol {
    let foo: String

    static public func == (lhs: MyViewModel, rhs: MyViewModel) -> Bool {
        return lhs.foo == rhs.foo
    }
}

class MyView: UIView, ModeledView {
    typealias ViewModel = MyViewModel

    private(set) var viewModel: MyViewModel?

    public func set(newViewModel: MyViewModel?) {
        print(newViewModel?.foo)
    }
}

但是我想为我的视图模型指定一个协议,而不是一个具体化的类型。原因是一个 struct / class 可以遵守多个这样的视图模型协议。我既不想将此对象转换为另一种类型只是为了将其传递给视图,也不想让视图具有关联类型,但要求比它需要的更多。所以我想我想做这样的事情:

protocol MyViewModelProtocol: ViewModelProtocol {
    var foo: String { get }
}

class MyView: UIView, ModeledView {
    typealias ViewModel = MyViewModelProtocol

    private(set) var viewModel: MyViewModelProtocol?

    public func set(newViewModel: MyViewModelProtocol?) {
        print(newViewModel?.foo)
    }
}

struct DataModel: MyViewModelProtocol {
    let foo: String

    let bar: String

    static public func == (lhs: MyViewModel, rhs: MyViewModel) -> Bool {
        return lhs.foo == rhs.foo && lhs.bar == rhs.bar
    }
}

let dataModel = DataModel(foo: "foo", bar: "bar")
let view = MyView()
view.set(newViewModel: dataModel)

这行不通。编译器说 MyView 不符合 ModeledView 协议,并提示

Possibly intended match 'MyView.ViewModel' (aka 'MyViewModelProtocol') does not conform to 'ViewModelProtocol'

我真的不明白编译器有什么问题,因为 MyViewModelProtocol 被定义为扩展 ViewModelProtocol

MyViewModelProtocol is defined as extending ViewModelProtocol

正确。 MyViewModelProtocol 扩展 ViewModelProtocol。它不符合 ViewModelProtocol。这是 "protocols do not conform to themselves." 您的关联类型的经典案例,要求 ViewModel 是符合 ViewModelProtocol 的具体类型,而 MyViewModelProtocol 不是具体类型,它不符合任何内容(协议不符合协议)。

解决这个问题的方法是从调用代码开始,然后构建支持您希望调用代码看起来像什么的协议。在您提供的代码中,正确的解决方案是完全摆脱 ModeledView 。它什么也没做;没有任何东西依赖于它,并且它不提供任何扩展。我假设在您的 "real" 代码中,某些 确实 依赖于它。这是要关注的关键代码段。实现这一目标的最佳方法是编写一些您希望存在的符合类型的具体示例。 (如果你写了几个具体的实现有困难,那就重新考虑是否有什么可以抽象的。)


只是为了详细说明为什么这在您的特定情况下不起作用(它在更一般的情况下也不起作用,但您的情况会增加一些重要的附加限制)。您要求符合 ViewModelProtocol 的类型也符合 Equatable。这意味着他们必须实施:

static public func == (lhs: Self, rhs: Self) -> Bool {

这并不意味着某些符合 ViewModelProtocol 的类型可等同于某些其他符合 ViewModelProtocol 的类型。它表示 Self,实现协议的特定具体类型。 ViewModelProtocol 本身 不等式。如果是,您希望 dataModel == myViewModel 调用上面的哪个代码?它会检查 bar 吗?请记住,== 只是一个函数。 Swift 不知道你在问 "are these equal." 它不能注入像 "if they're different concrete types they're not equal." 这样的东西,这是函数本身必须做的事情。因此 Swift 需要知道调用哪个函数。

在 Swift 中实现这一点将是对该语言的主要补充。它已经被讨论了很多(参见我在之前的评论中发布的 link),但它至少还有几个版本。

如果您删除 Equatable 要求,它今天仍然不起作用,但所需的语言更改要小得多(并且可能会更快)。