【问题标题】:Parent-child protocol relationship and ObjC availability父子协议关系和 ObjC 可用性
【发布时间】:2017-05-17 12:03:11
【问题描述】:

我无法让以下代码工作:

@objc protocol Child { }

@objc protocol Parent {
    var child: Child { get }
}
    
class ChildImpl: Child {
    // not part of the `Child` protocol
    // just something specific to this class 
    func doSomething() { }
}
    
class ParentImpl: Parent {
    let child = ChildImpl()

    func doSomething() {
        // need to be able to access `doSomething`
        // from the ChildImpl class
        childImpl.doSomething()
    }

    // this would solve the problem, however can't access the ChildImpl members
    // that are not part of the protocol
    // let child: Child = ChildImpl()
    // as well as this, however maintaining two properties is an ugly hack
    // var child: Child { return childImpl }
    // private let childImpl = ChildImpl()
}

我得到的错误:

类型“ParentImpl”不符合协议“Parent”。
是否要添加协议存根?

基本上我有两个父子协议和两个实现这两个协议的类。但是,编译器仍然无法识别 ChildImplChild

如果我在 Parent 上使用关联类型,我可以消除错误

protocol Parent {
    associatedtype ChildType: Child
    var child: ChildType { get }
}

,但是我需要有可供 Objective-C 使用的协议,并且还需要能够引用 child 作为实际的具体类型。

是否有解决方案不涉及重写Objective-C 中的协议,或者不添加重复的属性声明只是为了避免问题?

【问题讨论】:

  • this Q&A——在你的情况下一个可行(但不是特别好的)解决方案是定义一个Child!ParentImpl类型的虚拟属性来满足协议要求(然后有您的实际财产类型为ChildImpl!)。
  • @Hamish,我也评估了这种方法,但是(正如你所说)它不是很好,它需要维护两个具有相同角色的属性:(
  • @Cristik 是的:/不幸的是,我认为这可能是在 Swift 支持之前你能管理的最好的方法——尽管我希望有人能用更好的解决方法证明我错了。跨度>
  • 我发现这是 Swift 编译器中的一个错误 tbh

标签: objective-c swift protocols


【解决方案1】:

简短的回答是否定的,当前的设计不适用于当前的 Swift 语言。

原因是var child: Child { get } 协议要求在幕后创建了一个存在容器,类似于ExistentialContainer<Child>。因此,任何符合协议的类型也必须声明一个相同类型的存在容器,而不是ExistentialContainer<Subtype>

似乎存在容器不是协变的,因此暂时需要使用问题和其他答案中描述的解决方法。

【讨论】:

    【解决方案2】:

    现在我们有了 Swift 5.1,我发现一个几乎很好的解决方案是使用属性包装器:

    @propertyWrapper
    struct Hider<T, U> {
        let wrappedValue: T
    
        init(wrappedValue: T) {
            self.wrappedValue = wrappedValue
        }
    
        var projectedValue: U { return wrappedValue as! U }
    }
    
    @objcMembers class ParentImpl: NSObject, Parent {
        @Hider<Child, ChildImpl> var child = ChildImpl()
    }
    

    这样child 暴露为Child$child 暴露为ChildImpl,这允许在ParentImpl 中使用非协议成员。

    解决方案并不理想,因为我还没有找到一种方法来描述 T 应该是 U 的超类型。

    【讨论】:

      【解决方案3】:

      您正在尝试做的事情称为 covariance 并且 swift 不支持协议或符合这些协议的类/结构中的协变。 您要么必须使用Type-Erassure,要么必须使用associatedTypes

      protocol Child { }
      
      protocol Parent {
          associatedtype ChildType: Child
          var child: ChildType { get }
      }
      
      class ChildImpl: Child {
          func doSomething() {
              print("doSomething")
          }
      }
      
      class ParentImpl: Parent {
          typealias ChildType = ChildImpl
          let child = ChildImpl()
      
          func test() {
              child.doSomething()
          }
      }
      ParentImpl().test() // will print "doSomething"
      

      这里是Parent 协议的一般用法的类型擦除父级:

      struct AnyParent<C: Child>: Parent {
          private let _child: () -> C
          init <P: Parent>(_ _selfie: P) where P.ChildType == C {
              let selfie = _selfie
              _child = { selfie.child }
          }
      
          var child: C {
              return _child()
          }
      }
      
      let parent: AnyParent<ChildImpl> = AnyParent(ParentImpl())
      parent.child.doSomething() // and here in Parent protocol level, knows what is child's type and YES, this line will also print "doSomething"
      

      【讨论】:

      • 不幸的是,我不能使用关联值,也不能使用泛型键入橡皮擦,因为我需要与 Objective-C 交互...
      • @Cristik 这个话题实际上已经很老了,并且已经在许多博客和其他 SO 问题上进行了讨论。如果 objc 接口对您很重要,那么您别无选择,请忍受丑陋的解决方案 :(。我不得不对项目使用相同的机制,老实说,这并不是“那么”糟糕,但是,您总会注意到事实上,这个代码让我很恼火!像这样的事情,是我不会在一家仍然使用 Obj-c 编码的公司招聘或工作的原因之一。如果你搜索Swift protocol covariance,所有主题都会弹出。
      • 此外,如果可能,您可以将ChildParent 定义为抽象类。 IK swift 没有这样的术语,但您可以使用一些变通方法,例如 `fatalError("implement in subclass") 或类似的东西。
      • 感谢您的建议,我知道各种解决方法,但是我的目标是尽可能干净地实现它。可惜 Swift 编译器无法正确推断出该位置明显存在的类型...
      【解决方案4】:

      我在 cmets 中提到了 link,显示了您尝试过的内容,使用关联类型或单独的属性只是为了满足协议一致性。我认为 Swift 很快就会支持从 let child: Child &amp; ChildImpl = ChildImpl() 或简单的 child: ChildImpl 这样的组合类型推断类型,因为 ChildImplChild。但在那之前,我认为我建议另一种选择,即将您需要的 api 与 ChildImpl 分开,并将它们放在 Child 继承的单独协议中。这样当 Swift 编译器支持这个特性时,你不需要重构,只需将其删除即可。

      // TODO: Remove when Swift keeps up.
      @objc protocol ChildTemporaryBase { }
      private extension ChildTemporaryBase {
          func doSomething() {
              (self as? ChildImpl).doSomething()
          }
      }
      
      @objc protocol Child: ChildTemporaryBase { }
      
      class ParentImpl: Parent {
          let child: Child = ChildImpl()
          func testApi() {
              child.doSomething?()
          }
      }
      

      【讨论】:

      • 这使doSomething() 公开,我想将其保留在两个类的集群内部。这就是为什么在我的代码中doSomething 是实现类的一部分。
      • @Cristik 也许你可以在ChildTemporaryBase 私有扩展中添加api。我用类似的东西编辑了我的答案,这绝对是比使用计算属性更多的代码,但我想它保持ParentImpl干净。
      • 有趣的方法,但是似乎我必须添加很多样板代码,并且当方法返回值时事情会变得复杂,因为我需要guard 或强制解包结果
      【解决方案5】:

      您的 ParentImpl 类没有 Child 类型协议。我解决了这个解决方案。

      class ParentImpl: Parent {
         var child: Child = ChildImpl()
      }
      

      【讨论】:

      • @Cristik 我认为你不能在没有孩子的情况下调用ChildImpl 的方法
      • @Cristik 喜欢(child as! ChildImpl).doSomething()
      【解决方案6】:

      如果您不介意向 ParentImpl 类添加扩展属性:

      @objc protocol Child {}
      
      @objc protocol Parent {
          var child: Child! { get }
      }
      
      class ChildImpl: Child { }
      
      class ParentImpl: Parent {
          var child: Child!
      }
      
      extension ParentImpl {
      
          convenience init(child: Child?) {
              self.init()
              self.child = child
          }
      
          var childImpl: ChildImpl! {
              get { return child as? ChildImpl }
              set { child = newValue }
          }
      
      }
      
      let parent = ParentImpl(child: ChildImpl())
      let child = parent.child
      

      【讨论】:

      • @Cristik 只是想想如何实现你的想法,而不是真正解决你的问题。
      • 感谢您的意见,很遗憾,objective-c 中也没有泛型...
      • 立即移除泛型。
      • 抱歉,回复太晚了,这个解决方案仍然不是最优的,因为它使用了不推荐使用的 IUO,并且需要一个额外的几乎重复的属性,我想避免。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-01-17
      • 2015-11-18
      • 1970-01-01
      • 2017-01-22
      • 1970-01-01
      相关资源
      最近更新 更多