3

这是归结的情况:

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

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

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

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

在运行时,可能需要这些类中的任何一个,具体取决于用户决定使用的硬件。因此,根据依赖倒置原则,我不希望自己的类型依赖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一个不同的故事。我碰巧认为 AlicecursorLocation完美地命名了她的属性,并且如您所见,我希望能够为我的协议要求使用相同的名称。我的问题是它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,但我认为这在这种情况下没有意义。尽管如此,协议一致性甚至不会首先编译,因此为了解决无限递归而消除歧义是次要的。

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

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


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

4

1 回答 1

2

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

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

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

我现在可以追溯地遵守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同名但类型不同的函数,它仍然会编译。这种行为正是我在原始帖子中想要从我的财产中得到的。因此:

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

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

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

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


“等一下——现在 AATrackpad 符合了 CursorInput,不是有两个版本的 cursorLocation吗?如果我要使用 trackpad.cursorLocation,是a CGPoint 还是an AAPoint

它的工作方式是这样的——如果在这个范围内对象是已知的,AATrackpad那么使用 Alice 的原始属性:

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

但是,如果类型只知道是 aCursorInput那么我定义的默认属性将被使用:

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碰巧是 anAATrackpad还是 a BBMouse- 只是它是某种CursorInput. 因此,无论我在哪里使用 my cursorInput: CursorInput?,它的属性都将是我在协议扩展中定义的类型,而不是类中定义的原始类型。


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

于 2018-06-22T09:19:41.473 回答