为什么协议中的只读属性需求不能由符合该属性的属性满足?

46
为什么以下代码会产生错误?
protocol ProtocolA {
    var someProperty: ProtocolB { get }
}

protocol ProtocolB {}
class ConformsToB: ProtocolB {}

class SomeClass: ProtocolA { // Type 'SomeClass' does not conform to protocol 'ProtocolA'
    var someProperty: ConformsToB

    init(someProperty: ConformsToB) {
        self.someProperty = someProperty
    }
}

这个类似问题的答案很有道理。 但是在我的例子中,该属性是只读的。为什么这样做不起作用?这是 Swift 的缺陷还是有其他原因呢?


谢谢提供链接。虽然不幸,但是很好知道! - solidcell
2
如果您想要这种行为,在 ProtocolA 中应该有 associatedtype T: ProtocolB,然后声明 var someProperty: T { get } - BallpointBen
在它(希望)被修复之前,那可以作为一个权宜之计,但我真的不太愿意添加关联类型,因为这会将该知识向上冒泡到对象图的其余部分,很快就会失控。 - solidcell
从Swift 5.1开始,您可以使用不透明返回类型与关联类型结合使用,以避免将它们通过对象图传递。 - Ilias Karim
@IliasKarim你能把那个作为一个答案添加吗? - pkamb
@pkamb,我添加了一个答案。 - Ilias Karim
3个回答

60

这种情况本来是可以实现的,只需将一个只读属性要求协变化,从而使得以ProtocolB为类型的属性返回一个ConformsToB实例成为合法操作。

但目前Swift还不支持这种操作。为了实现这一点,编译器需要在协议见证表和符合实现之间生成一个thunk,以执行必要的类型转换。例如,为了将ConformsToB实例作为ProtocolB类型使用(由于调用者可能不知道被调用的实现),需要将其打包到存在容器中

但是,编译器没有理由不能做到这一点。有多个关于此问题的错误报告开放,this one 是关于只读属性要求的特定报告,this general one 中,Swift团队成员Slava Pestov表示:

[...]我们希望在允许函数转换的每种情况下都有协议见证和方法覆盖

因此,看起来Swift团队正在寻求在未来的语言版本中实现这一点。

然而,在此期间,如 @BallpointBen所说,一个解决方法是使用associatedtype

protocol ProtocolA {
    // allow the conforming type to satisfy this with a concrete type
    // that conforms to ProtocolB.
    associatedtype SomeProperty : ProtocolB
    var someProperty: SomeProperty { get }
}

protocol ProtocolB {}
class ConformsToB: ProtocolB {}

class SomeClass: ProtocolA {

    // implicitly satisfy the associatedtype with ConformsToB.
    var someProperty: ConformsToB

    init(someProperty: ConformsToB) {
        self.someProperty = someProperty
    }
}

但这样做并不理想,因为它意味着 ProtocolA 不再可用作类型(因为它具有 associatedtype 要求)。它也改变了协议的含义。最初它表示 someProperty 可以返回符合 ProtocolB任何 内容 - 现在它表示 someProperty 的实现仅处理符合 ProtocolB 的一个 特定 具体类型。
另一个解决方法是只是定义一个虚拟属性以满足协议要求:
protocol ProtocolA {
    var someProperty: ProtocolB { get }
}

protocol ProtocolB {}
class ConformsToB: ProtocolB {}

class SomeClass: ProtocolA {

    // dummy property to satisfy protocol conformance.
    var someProperty: ProtocolB {
        return actualSomeProperty
    }

    // the *actual* implementation of someProperty.
    var actualSomeProperty: ConformsToB

    init(someProperty: ConformsToB) {
        self.actualSomeProperty = someProperty
    }
}

在这里,我们实际上是为编译器编写thunk,但它并不特别好,因为它会向API添加一个不必要的属性。


1
感谢您详细的回答,@Hamish。我已经在按照您建议的方式(使用计算属性包装器)进行操作了,但我同意,不得不添加另一个属性是很不幸的。 - solidcell
只读协议属性的协变存在至少一个潜在问题。请参见此处:https://forums.swift.org/t/should-allow-covariance-of-get-only-protocol-property-requirements/27937/10 - Anton Belousov
此答案中提供的YouTube链接因版权问题已失效! - Sourav Kannantha B

2
除了Harmish的回答之外,如果您想在 SomeClassProtocolA 上保持使用相同的属性名称,您可以这样做:
protocol ProtocolB {}

protocol ProtocolA {
    var _someProperty_protocolA: ProtocolB { get }
}

extension ProtocolA {
    var someProperty: ProtocolB {
        return _someProperty_protocolA
    }
}

class ConformsToB: ProtocolB {}

class SomeClass: ProtocolA {


    // the *actual* implementation of someProperty.
    var _someProperty: ConformsToB

    var someProperty: ConformsToB {
      // You can't expose someProperty directly as
      // (SomeClass() as ProtocolA).someProperty would
      // point to the getter in ProtocolA and loop
      return _someProperty
    }

    // dummy property to satisfy protocol conformance.
    var _someProperty_protocolA: ProtocolB {
        return someProperty
    }

    init(someProperty: ConformsToB) {
        self.someProperty = someProperty
    }
}

let foo = SomeClass(someProperty: ConformsToB())
// foo.someProperty is a ConformsToB
// (foo as ProtocolA).someProperty is a ProtocolB

当你需要遵循另一个协议ProtocolA2并且原本也会对someProperty进行限制时,或者当你想要隐藏你绕开Swift限制的方法时,这将非常有用。

现在我很好奇为什么Swift不能直接为我做到这一点。


1

从Swift 5.1开始,您可以使用不透明的返回类型引用引用另一个协议的协议,只要您也使用关联类型来实现。

它不仅适用于只读的“get”属性,还适用于可读写的属性。例如,


protocol ProtocolA {
  associatedtype T: ProtocolB
  var someProperty: T { get }
  var x: Int { get set }
}

protocol ProtocolB {
  var x: Int { get set }
}

struct ConformsToB: ProtocolB {
  var x: Int
}

class SomeClass: ProtocolA {
  var someProperty: ConformsToB

  init(someProperty: ConformsToB) {
    self.someProperty = someProperty
  }

  var x: Int {
    get {
      someProperty.x
    }
    set {
      someProperty.x = newValue
    }
  }
}

var protocolA: some ProtocolA = SomeClass(someProperty: ConformsToB(x: 1))

print(protocolA.x) // 1
protocolA.x = 2
print(protocolA.x) // 2

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