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

标签 swift protocols conflict requirements dependency-inversion

这是归结的情况:

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

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

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

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

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

protocol CursorInput {
  var cursorLocation: CGPoint { get }
}

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

class MyCursorDescriber {
  var cursorInput: CursorInput?

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

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

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

但为了编译它,我必须追溯地使 BBMouse 符合我的协议(protocol):

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

现在我已经使 BBMouse 符合我的 CursorInput 协议(protocol),我的代码可以编译并且我的架构是我想要的方式。我在这里没有问题的原因是我认为 where_is_the_mouse 是该属性的糟糕名称,我很高兴再也不会使用该名称。然而,对于 AATrackpad 来说,情况就不同了。我碰巧认为 Alice 完美地命名了她的 cursorLocation 属性,正如您所看到的,我希望能够为我的协议(protocol)要求使用相同的名称。我的问题是 AATrackpad 不使用 CGPoint 作为此属性的类型,而是使用称为 AAPoint 的专有点类型。事实上,我的协议(protocol)要求 (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 就好了,但我认为这在这种情况下没有意义。同样,协议(protocol)一致性甚至不会首先编译,因此为了解决无限递归而消除歧义是次要的。

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

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


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

最佳答案

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

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

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

我现在可以追溯地使 AATrackpad 符合此协议(protocol)而不会发生冲突:

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,这仍然会编译但不同的类型。这种行为正是我在原始帖子中想要从我的属性(property)中得到的。因此:

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

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

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

这就是协议(protocol)扩展的妙处。协议(protocol)扩展中声明的任何内容都类似于函数的默认参数——只有在没有其他优先级时才使用。由于这种不同的行为模式,当我使 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?,它的属性都是我在协议(protocol)扩展中定义的类型,而不是类中定义的原始类型。


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

关于Swift - 使第三方类型符合我自己的协议(protocol)但有冲突的要求,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50963447/

相关文章:

protocols - 最有效的双氢睾酮

swift - 实现通用协议(protocol)编译器错误

ios - 如何操作 React Native - Native UI 组件的属性?

swift - 在 Swift 中使用数组/字典进行键值编码 (KVC)

arrays - 将符合多个协议(protocol)的类型转换为单个协议(protocol)

Python导入冲突

javascript - 将 Backbone.js 与 _.noConflict() 一起使用

git - 当在同一位置添加不同的方法(等)时,如何避免 merge 冲突?

ios - 在 iOS 中分析文本到语音的字符串

ios - 如何计算 NSLayoutManager 中的字形数量