【问题标题】:How to check that object of type `Any` is an array of concrete class implementing some protocol如何检查“Any”类型的对象是实现某些协议的具体类的数组
【发布时间】:2017-04-19 16:34:22
【问题描述】:

我遇到了一个问题。 考虑我有一个协议和两个实现它的类:

protocol Initiatable{
    init()
}

class A: Initiatable{
    required init() {}
}

class B: Initiatable{
    required init() {}
}

然后在某个时候我正在创建一个数组并将其传递给函数:

var array = [A]()

func update(object: Any){

}

update(object: array)

从那个函数update 如果它满足另一个函数的条件,我想将object 传递给另一个函数:

func process<T: Initiatable>(array: T){
    /* ... */
}

那么如何检查Any 类型的对象是实现Initiatable 协议的具体类的数组?我想写一些类似的东西

func update(object: Any){
    if let array = object as Array<T: Initiatable>{
        process(array: array)
    }
}

但那是行不通的。代码如下:

func update(object: Any){
    if let array = object as [Initiatable]{
        process(array: array)
    }
}

func process(array: [Initiatable]){ }

编译得很好,但这不是我想要的——process 函数应该接收Initiatable 的具体实现数组,所以在某些时候它可以使用:

func process<T: Initiatable>(array: [T]){
    /* other code */
    T.init()
}

那么有没有办法做到这一点?非常感谢您!

【问题讨论】:

  • 我在处理泛型时也遇到过这类问题。是否有理由没有像func update&lt;T: Initiatable &gt;update(object: [T]) 这样的第二版更新?
  • 一旦你打电话给T.init(),你能用它做什么?没有您可以调用它的方法,那么process 实际上可以做什么?你唯一知道的是它可以使用init() 构造。标准库中不存在Initiatable 是有原因的。这不是偶然的遗漏。 oleb.net/blog/2016/12/protocols-have-semantics 这里有很多令人担忧的模式(从传递Any 开始)。即使你让它工作,这种通用/反射代码也往往非常脆弱。你想用它做什么?
  • 上面示例中的代码只是我正在使用的代码的简化。当然Initiatable在我的代码中还有其他功能(并且名称也不同)

标签: arrays swift generics


【解决方案1】:

这个问题有几个部分:

生成类型数组

您的数组声明需要A objects 的数组,而不是A types。要生成 A 类型的数组,可以传入 Postfix self 表达式:(link)

var array = [ A.self ]

这会将array 定义为A.Type 的数组,称为元类型类型(same link)。

您还可以使用此元类型类型生成一个空数组:

var array:[A.Type] = []

如果您想要一个同时包含 A.selfB.self 的数组,您可以将其指定为 [Any]...

var array:[Any] = [A.self,B.self]

...或者,使用您创建的Initiatable 协议:

var array:[Initiatable.Type] = [A.self,B.self]

在更新方法中将数组向下转换为类型数组

您在将 Any 对象向下转换为类型数组时遇到了问题。 这是我更新的update 方法:

func update(object: Any){
  if let array = object as? [Initiatable.Type] {    //1
    process(array: array)
  }
}
  1. 您现在可以对通过update 方法的数组执行可选的向下转换。将其向下转换为 Initiatable 的元数据类型数组:(这是我从您的方法中修改的唯一行)

在处理方法中接收一个类型作为参数

我假设您只希望您的流程方法接收一个类型数组,并根据其中一种类型实例化一个变量。您没有提到数组中的哪个元素,所以我只使用了第一个。

func process(array: [Initiatable.Type]){    //1
  if let firstType = array.first {          //2
    let firstObject = firstType.init()      //3
  }
}
  1. process 方法可以接收采用 Initiatable 协议的类型数组。
  2. 我正在使用可选绑定来获取数组中第一个元素的值。这应该是一种类型。
  3. 根据数组中的第一个元素实例化一个对象。

【讨论】:

  • 感谢您提供如此详细的答复!不幸的是,这不是我要找的。该数组必须是具体类型的数组(我不能使用您的答案中的类型数组)并且它是空的(在您的答案中它充满了对象类型)。整个想法是,我想检查 Any 类型的对象,如果它是数组,则用它类型的对象填充它。
  • 在我的回答中,我展示了以 4 种方式实例化数组,其中一种会创建一个空数组。也许我误解了您所说的具体类型是什么意思?我假设您的意思是将类/结构/枚举定义为具体类型(与抽象类型相反,例如协议的定义)。但也许你只是指一个对象/实例?
  • 在你创建对象类型数组的所有方式中,数组的类型要么是[A.Type],要么是[Initiatable.Type]。在我的问题中(并且它是必需的)数组应该是具体对象的数组,例如 [A][B]
  • 啊 - 好吧,我误会了 - 你希望数组是对象(例如 A),而不是具体的类型/类(例如 A.Type),明白了。我想我对在 process 方法中调用 init 方法感到困惑,并假设您想在那时从一个类型实例化一个对象。当你在你的问题中说“编译得很好,但这不是我想要的” - 那段代码到底有什么问题? (除了缺少的可选解包)
  • 问题是像process(array: [Initiatable]) 这样的签名我无法在其中创建数组对象类型的实例。当然我不能写Initiatable.init()。因此我希望对array 有更严格的限制,因此只有[A][B] 的数组可以传递给这个函数。
【解决方案2】:

如果你的数组是非空的,那么你可以通过从数组中获取一个元素的运行时类型来解决这个问题:

func update(object: Any){
    if let array = object as? [Initiatable]{
        process(array: array)
    } else {
        // Not initiatable
    }
}

func process(array: [Initiatable]) {  // no need for generics here
    guard let first = array.first else {
        // Unable to determine array type
        // throw or return or whatever
    }

    // type(of: first) gives A or B
    type(of: first).init()
}

如果数组为空,那么我不知道如何在 Swift 中执行此操作(无论如何,从 Swift 3 开始)。

  • object 的静态类型为Any,因此您需要使用objectruntime 类型才能到达任何地方。
  • 但是,Swift(以及大多数具有泛型的语言)仅根据您传递的值的 静态 类型解析类型参数,如 T。因此,您使用process&lt;T: Initiatable&gt;(array: [T]) 的整个方法是死路一条:T 只能采用编译器已经知道的类型,当您调用process() 时。
  • Swift 确实允许您访问运行时类型:type(of: object) 将返回(例如)Array&lt;A&gt;。很近!但:
    • AFAIK,目前无法从元类型中提取类型参数,即从 Array&lt;A&gt; 获取 A,除非编译器已经静态知道类型参数。
    • 可以获取字符串"Array&lt;A&gt;" 并从中提取字符串A,但是AFAIK 没有办法将字符串映射回Swift 中的类型,除非该类型是Objective-C类。

简而言之,我认为你刚刚达到了 Swift 元类型/反射能力的极限。

【讨论】:

  • 尽管您的回答提供了一个解决方案,但它是唯一能引导我朝着正确方向思考的问题,因此我会奖励您的帮助!
  • 好的,谢谢!如果他们想出一个让它起作用的技巧,如果你把接受的答案换给其他人,我也不会生气。
  • P.S. 可以想象,Swift 4 中的类型系统改进将使这成为可能。 Swift 4 将添加存在性,这将所有 process(…) 接受类似 Array where Element: Initiatable 的东西(但编译器仍然不知道 Element 的运行时类型)。然后你也许可以做type(of: array).Element.self.init()。不过,我认为这在没有存在类型的 Swift 3 中是不可能的。
  • 我已经发布了解决方案,你可以看看:)
  • 酷。您是否必须标记您的课程@objc 和/或扩展NSObject 才能完成这项工作?出于这个原因,我排除了涉及NSClassFromString 的任何事情(根据最后一个要点)。
【解决方案3】:

不幸的是,在给定的约束条件下,我想不出解决方案。看起来您正在尝试在运行时实现编译时多态性。

使用泛型意味着编译器了解将调用哪个类。但是使用Any 意味着在编译时它可以是任何东西,并且只有在运行时才知道。

我能提出的最接近的解决方案是:

// Either use generic `update` function
func updateGeneric<T: Initiatable>(array: Array<T>){
    process(array: array)
}

// or explicitly convert `Any` to the class, 
// so compiler understands what implementation of `process` to call
func updateExplicit(object: Any) {

    if let array = object as? [A] {
        process(array: array)
    }

    if let array = object as? [B] {
        process(array: array)
    }
}

【讨论】:

    【解决方案4】:

    这对你有帮助吗,

    protocol Initiatable{
        init()
    }
    
    class A: Initiatable{
        required init() {}
    }
    
    class B: Initiatable{
        required init() {}
    }
    
    class Test<T : Initiatable>
    {
    
        func update(object: Any){
            if let array = object as? [T]{
                process(array: array)
            }
        }
    
        func process(array: [T]){
    
        }
    }
    

    【讨论】:

      【解决方案5】:

      我找到了解决这个问题的方法:

      func classTypeFrom(_ className: String) -> AnyClass!{
          if  let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String {
              let classStringName = "_TtC\(appName.characters.count)\(appName)\(className.characters.count)\(className)"
              return NSClassFromString(classStringName)
          }
          return nil;
      }
      
      func update(object: Any){
          if let array = object as? [Initiatable]{
              let arrayTypeName = "\(type(of: ar))"
              let objectTypeName = arrayTypeName.substringFrom(index: 6, length: arrayTypeName.characters.count - 7)
              if let arrayType = classTypeFrom(objectTypeName) as? Initiatable.Type{
                  process(array: array, type: arrayType)
              }
          }
      }
      
      func process(array: [Initiatable], type: Initiatable.Type){
          var ar = array
          let newObj = type.init()
          ar.append(newObj)
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-10-19
        • 1970-01-01
        • 2012-06-21
        • 2011-07-13
        • 2017-02-23
        • 1970-01-01
        • 2015-04-14
        • 1970-01-01
        相关资源
        最近更新 更多