Swift - 使第三方类型符合我自己的协议但有冲突的要求

Swift - Conform third-party type to my own protocol with conflicting requirement

这是归结的情况:

假设 Alice Allman 编写的第三方框架提供了一个非常有用的 class:

public class AATrackpad {
  public var cursorLocation: AAPoint = .zero
}

Bob Bell 编写的另一个框架提供了不同的 class:

public class BBMouse {
  public var where_is_the_mouse: BBPoint = .zero
}

在运行时,可能需要这些 class 中的任何一个,具体取决于用户决定使用的硬件。因此,为了与 Dependency Inversion Principle 保持一致,我不希望自己的类型直接依赖于 AATrackpadBBMouse。相反,我想定义一个描述我需要的行为的协议:

protocol CursorInput {
  var cursorLocation: CGPoint { get }
}

然后让我自己的类型改用该协议:

class MyCursorDescriber {
  var cursorInput: CursorInput?

  func descriptionOfCursor () -> String {
    return "Cursor Location: \(cursorInput?.cursorLocation.description ?? "nil")"
  }
}

我希望能够使用 BBMouse 的实例作为光标输入,如下所示:

let myCursorDescriber = MyCursorDescriber()
myCursorDescriber.cursorInput = BBMouse()

但是为了编译我必须追溯BBMouse我的协议:

extension BBMouse: CursorInput {
  var cursorLocation: CGPoint {
    return CGPoint(x: self.where_is_the_mouse.x, y: self.where_is_the_mouse.y)
  }
}

现在我已经 BBMouse 符合我的 CursorInput 协议,我的代码可以编译并且我的架构是我想要的方式。我在这里没有问题的原因是我认为 where_is_the_mouse 对于 属性 来说是一个糟糕的名字,我很高兴再也不用那个名字了。但是,AATrackpad 是另一回事。我碰巧认为 Alice 完美地命名了她的 cursorLocation 属性,正如您所看到的,我希望能够为我的协议要求使用相同的名称。我的问题是 AATrackpad 没有使用 CGPoint 作为这个 属性 的类型,而是使用一个名为 AAPoint 的专有点类型。我的协议要求 (cursorLocation) 与 AATrackpad 的现有 属性 同名,但类型不同,这意味着我无法追溯遵守 CursorInput

extension AATrackpad: CursorInput {
  var cursorLocation: CGPoint { // -- Invalid redeclaration
    return CGPoint(x: self.cursorLocation.x, y: self.cursorLocation.y) // -- Infinite recursion
  }
}

正如该片段中的评论所说,这段代码无法编译,即使编译了,我也会在运行时面临无限递归,因为我无法具体引用 AATrackpad 版本cursorLocation。如果像这样的东西可以工作,那就太好了 (self as? AATrackpad)?.cursorLocation,但我认为这在这种情况下没有意义。同样,协议一致性甚至不会首先编译,因此为了解决无限递归而消除歧义是次要的。

考虑到所有这些背景,我的问题是:

如果我使用协议(这是广泛推荐的,有充分理由)构建我的应用程序,那么我使用某种第三方具体类型的能力是否真的取决于该第三方开发人员不希望这样做?不同意我对命名约定的喜好?


注意:答案 "Just pick a name that doesn't conflict with the types you want to use" 不会令人满意。也许一开始我只有 BBMouse 并且没有冲突,然后一年后我决定我也想添加对 AATrackpad 的支持。我最初选择了一个好名字,现在它在我的应用程序中普遍使用——我是否必须为了一种新的具体类型而在所有地方都更改它?稍后当我想添加对 CCStylusTablet 的支持时会发生什么,它现在与我选择的任何新名称冲突?我是否必须再次 更改协议要求的名称?我希望你明白我为什么要寻找比这更合理的答案。

受 Jonas Maier 评论的启发,我发现了我认为在架构上足以解决此问题的解决方案。正如乔纳斯所说,函数重载展示了我正在寻找的行为。我开始认为也许协议要求应该只是函数,而不是属性。按照这种思路,我的协议现在是:

protocol CursorInput {
  func getCursorLocation () -> CGPoint
  func setCursorLocation (_ newValue: CGPoint)
}

(请注意,在这个答案中,我也将其设置为可设置,这与原始 post 不同。)

我现在可以追溯 AATrackpad 遵守此协议而不会发生冲突:

extension AATrackpad: CursorInput {
  func getCursorLocation () -> CGPoint {
    return CGPoint(x: self.cursorLocation.x, y: self.cursorLocation.y)
  }
  func setCursorLocation (_ newValue: CGPoint) {
    self.cursorLocation = AAPoint(newValue)
  }
}

重要 - 即使 AATrackpad 已经有一个函数 func getCursorLocation () -> AAPoint,它仍然可以编译,它具有相同的名称但类型不同。这种行为正是我在 post 中的 属性 想要的。因此:

在协议中包含 属性 的主要问题是,由于命名空间冲突,它可能使某些具体类型实际上无法遵守该协议。

以这种方式解决这个问题后,我有一个新问题要解决:我希望 cursorLocation 成为 属性 而不是函数是有原因的。我绝对不想被迫在我的整个应用程序中使用 getPropertyName() 语法。值得庆幸的是,这可以解决,如下所示:

extension CursorInput {
  var cursorLocation: CGPoint {
    get { return self.getCursorLocation() }
    set { self.setCursorLocation(newValue) }
  }
}

这就是协议扩展的妙处。协议扩展中声明的任何内容都类似于函数的默认参数——只有在没有其他优先级时才使用。由于这种不同的行为模式,当我使 AATrackpad 符合 CursorInput 时,此 属性 不会引起冲突。我现在可以使用我最初想要的 属性 语义,而且我不必担心命名空间冲突。我很满意。


"等一下-现在AATrackpad符合CursorInput,不是有两个版本的cursorLocation吗?如果我用trackpad.cursorLocation,会不会是 CGPoint AAPoint?

其工作方式是这样的 - 如果在此范围内已知对象是 AATrackpad,则使用 Alice 的原始 属性:

let trackpad = AATrackpad()
type(of: trackpad.cursorLocation) // This is AAPoint

但是,如果已知类型仅为 CursorInput,则使用我定义的默认 属性:

let cursorInput: CursorInput = AATrackpad()
type(of: cursorInput.cursorLocation) // This is CGPoint

这意味着,如果我碰巧知道类型是 AATrackpad,那么我可以像这样访问 属性 的任一版本:

let trackpad = AATrackpad()
type(of: trackpad.cursorLocation) // This is AAPoint
type(of: (trackpad as CursorInput).cursorLocation) // This is CGPoint

这也意味着我的用例完全解决了,因为我特别希望 而不是 知道我的 cursorInput 是否恰好是 AATrackpadBBMouse - 只是它是某种 CursorInput。因此,无论我在哪里使用 cursorInput: CursorInput?,它的属性都是我在协议扩展中定义的类型,而不是 class.

中定义的原始类型

有一种可能性是,仅按要求提供功能的协议可能会导致名称空间冲突 - Jonas 在他的评论中指出了这一点。如果其中一个协议要求是一个没有参数的函数,并且符合类型已经有一个 属性 具有该名称,那么该类型将无法符合协议。这就是为什么我一定要为我的函数命名,包括动词,而不仅仅是名词 (func getCursorLocation () -> CGPoint) - 如果任何第三方类型在 属性 名称中使用动词,那么我可能不想无论如何都要使用它:)