如何在Swift中检查一个对象是否属于动态类类型?

6
我正在实现一个名为ofType的函数,它会过滤掉所有给定类型的元素。
以下是我的代码:
class Animal {}
class Mammal: Animal {}
class Monkey: Mammal {}
class Pig: Mammal {}
class Human: Mammal {}

extension Array {
    func ofType<T>(_ metatype: T.Type) -> [T] {
        return flatMap { type(of: $0) == metatype ? $0 as? T : nil }
//      return flatMap { $0 as? T } // This is not working as the T is always the static type of the parameter, which is Animal in this example.
//      return flatMap { $0 as? metatype } // This is not working either because of the grammar restriction.
    }
}

let animals = [Monkey(), Pig(), Human(), Mammal(), Animal()]
func animalType() -> Animal.Type {
    return Mammal.self
}
animals.ofType(animalType()).count // returns 1, expect to be 4.

在Objc中,我可以使用isKindOf()来检查一个对象是否是类或子类的实例。在Swift中也有类似的操作isas,但是它们后面的类型应该是静态类型,而不是动态类型值(例如,我可以写is Mammal,但不能写is Mammal.self)。
我也不能使用类型参数T,因为在这个例子中,T等于Animal,这不是我想要的。
您有任何关于如何实现此功能的想法吗?

你在编译时不知道要过滤哪种类型吗? - Sweeper
那么编译器怎么可能知道T是什么? - Sweeper
@Sweeper 编译器只知道参数的静态类型,在这个例子中,TAnimal - Bing
没错!所以我的意思是返回一个[Mammal]是不可能的。 - Sweeper
@Sam 我同意可能有更好、更快的方法,但我想先尝试直接的方式。 - Bing
显示剩余3条评论
4个回答

5

这个方法可行。只需要在flatMap中使用as?。如果动物可以转换成功,它将被返回,否则将返回nil,并且flatMap会将其丢弃。

class Animal {}
class Mammal: Animal {}
class Monkey: Mammal {}
class Pig: Mammal {}
class Human: Mammal {}

extension Array {
    func ofType<T>() -> [T] 
    {
        return flatMap { $0 as? T }
    }
}

let animals = [Monkey(), Pig(), Human(), Mammal(), Animal()]
let monkeys: [Monkey] = animals.ofType() // A one element array
let mammals: [Mammal] = animals.ofType() // A four element array

如果您显式地键入输出数组,编译器可以从上下文推断出T的类型,否则您将T的类型作为参数传递给函数但不在函数中使用。


如果您想要能够动态检查类型,即在编译时不知道要过滤的类型,那么您可以使用镜像。下面是一个有点笨拙但确实可行的解决方案:

class Animal
{
    func isInstance(of aType: Any.Type) -> Bool
    {
        var currentMirror: Mirror?  = Mirror(reflecting: self)
        while let theMirror = currentMirror
        {
            if theMirror.subjectType == aType
            {
                return true
            }
            currentMirror = theMirror.superclassMirror
        }
        return false
    }
}
class Mammal: Animal {}
class Monkey: Mammal {}
class Pig: Mammal {}
class Human: Mammal {}


let animals = [Monkey(), Pig(), Human(), Mammal(), Animal()]

for aType in [Animal.self, Mammal.self, Monkey.self]
{
    let result = animals.flatMap { $0.isInstance(of: aType) ? $0 : nil }
    print("\(result)")
}

输出:

[__lldb_expr_12.Monkey, __lldb_expr_12.Pig, __lldb_expr_12.Human, __lldb_expr_12.Mammal, __lldb_expr_12.Animal]
[__lldb_expr_12.Monkey, __lldb_expr_12.Pig, __lldb_expr_12.Human, __lldb_expr_12.Mammal] 
[__lldb_expr_12.Monkey]

编辑 在评论区根据Sam的建议,我想到上述方法最好放在协议扩展中。

protocol TypeCheckable {}

extension TypeCheckable  
{
    func isInstance(of aType: Any.Type) -> Bool
    {
        var currentMirror: Mirror?  = Mirror(reflecting: self)
        while let theMirror = currentMirror
        {
            if theMirror.subjectType == aType
            {
                return true
            }
            currentMirror = theMirror.superclassMirror
        }
        return false
    }
}

然后,您可以通过使其符合协议来为任何 Swift 类型添加能力。

class Animal: TypeCheckable { ... }

extension String: TypeCheckable {}

@Hamish 的答案非常好。你可以在这里看到它:https://gist.github.com/hamishknight/5bf45b025fcde4c2e8f9c67fce9fbe7e - Bing
我喜欢你的方法。为了简化它,我会将 isInstance(of aType: Any.Type) 方法移动到一个独立的类中,并让 Animal 继承它。 - Sam
@Sam 我基本上同意你的观点。事实上,最好的方法是创建一个协议(protocol),然后将方法放在协议扩展(protocol extension)中。这样,你可以通过使用扩展(extension)来将功能添加到任何类型(type)中。 - JeremyP
协议扩展实现很棒! - Bing
@JeremyP 我不知道你可以扩展协议。那看起来真的很好! - Sam

3

就个人而言,我认为 @JeremyP 提供的使用 Mirror 的建议是最好的; 尽管我会对其进行一些微调:

/// Conditionally cast `x` to a given dynamic metatype value, taking into consideration
/// class inheritance hierarchies.
func conditionallyCast<T, U>(_ x: T, to destType: U.Type) -> U? {

  if type(of: x) is AnyClass && destType is AnyClass { // class-to-class

    let isCastable = sequence(
      first: Mirror(reflecting: x), next: { $0.superclassMirror }
    )
    .contains { $0.subjectType == destType }

    return isCastable ? (x as! U) : nil
  }

  // otherwise fall back to as?
  return x as? U
}

我们在这里使用sequence(first:next:)来从x的动态类型中创建一个metatype序列,包括它可能具有的任何超类metatypes(可能是我见过的第一个看起来不可怕的函数 :P)。此外,当我们知道我们不会进行类到类转换时,我们会回退到进行as?类型转换,这使得该函数也可以处理协议metatypes。

然后你就可以简单地说:

extension Sequence {
  func ofType<T>(_ metatype: T.Type) -> [T] {
    return flatMap { conditionallyCast($0, to: metatype) }
  }
}

protocol P {}
class Animal {}
class Mammal: Animal {}
class Monkey: Mammal, P {}
class Pig: Mammal {}
class Human: Mammal, P {}

let animals = [Monkey(), Pig(), Human(), Mammal(), Animal()]

let animalType: Animal.Type = Mammal.self
print(animals.ofType(animalType)) // [Monkey, Pig, Human, Mammal]

print(animals.ofType(P.self)) // [Monkey, Human]

另一种选择,假设您使用的是苹果平台(即可以访问Objective-C运行时),则可以使用Objective-C元类方法isSubclass(of:)来检查给定元类型是否相等或是否是另一个元类型的子类:

import Foundation

/// Conditionally cast `x` to a given dynamic metatype value, taking into consideration
/// class inheritance hierarchies.
func conditionallyCast<T, U>(_ x: T, to destType: U.Type) -> U? {

  let sourceType = type(of: x)

  if let sourceType = sourceType as? AnyClass,
     let destType = destType as? AnyClass { // class-to-class

    return sourceType.isSubclass(of: destType) ? (x as! U) : nil
  }

  // otherwise fall back to as?
  return x as? U
}

这个方法可行是因为在苹果平台上,Swift类基于Obj-C类构建,因此Swift类的元类型是一个Obj-C元类对象。

我仍然更喜欢你的第二个答案,尽管它没有第一个那么启发人。 :P - Bing
1
我发现 isKind(of:) 可以用于 AnyObject。所以这一行 let sourceType = type(of: x) 可以省略,只需检查 (x as? AnyObject)?.isKind(of: destType) - Bing
@Bing 是的,你可以使用 isKind(of:);但请注意,将 AnyObject 强制转换总是成功的,因为不兼容 Obj-C 的东西会被装箱成一个不透明的 Obj-C 兼容盒子。所以要检查 x 是否真的是一个类的实例,你需要说 type(of: x) is AnyClass,而我发现到那时你可能还不如直接使用元类型的 as?isSubclass(of:) :) - Hamish

2
您可以使用反射来查找所有与元类型兼容的项目,方法如下:
class Animal { }
class Mammal: Animal {}
class Monkey: Mammal {}
class Pig: Mammal {}
class Human: Mammal {}

extension Array {
    func ofType<T>(_ metatype: T.Type) -> [T] {
        return flatMap { item in
            var mirror:Mirror? = Mirror(reflecting: item)
            while let currentMirror = mirror {
                mirror = currentMirror.superclassMirror
                if currentMirror.subjectType == metatype {
                    return item as? T
                }
            }
            return nil
        }
    }
}

let animals = [Monkey(), Pig(), Human(), Mammal(), Animal()]
func animalType() -> Animal.Type {
    return Mammal.self
}
let result = animals.ofType(animalType())
print(result) // returns 4 items: Monkey, Pig, Human, Mammal

或者,使用以下代码我使用了运算符is并直接将Mammal.self传递给函数ofType:

class Animal {}
class Mammal: Animal {}
class Monkey: Mammal {}
class Pig: Mammal {}
class Human: Mammal {}

extension Array {
    func ofType<T>(_ metatype: T.Type) -> [T] {
        return flatMap { $0 is T ? $0 as? T : nil }
    }
}

let animals = [Monkey(), Pig(), Human(), Mammal(), Animal()]
let result = animals.ofType(Mammal.self)
print(result) // returns 4 items: Monkey, Pig, Human, Mammal

我知道这个代码能够工作。我只是无法传递文字的类类型,因此编译器无法推断出确切的类型。 - Bing
@AndreaMugnaini 为什么 String(describing: type(of: item)) != String(describing: T.self)?我不明白你的意思... - Bing
@Bing,我找到了一个合适的解决方案。 - mugx
1
@AndreaMugnaini,其他人也找到了类似的解决方案。感谢您回答问题! - Bing

1

isKindOf()方法在Swift中也是可用的,因为它是NSObjectProtocol的一个方法。所以你真正需要做的是为你的Animal声明子类NSObject

注意:在Swift中,is kind of方法被重命名为isKind(of: Type)

应该很简单。

class Animal: NSObject {}

现在,唯一要解决的问题是,不是所有的数组元素都是 NSObject 的子类或符合 NSObjectProtocol

为了解决这个问题,我们在 Swift 扩展的声明中添加一个 where 限定语句。

现在它应该看起来像这样:

extension Array where Element: NSObjectProtocol 

将所有东西组合在一起,最终代码应该类似于:
class Animal: NSObject {}
class Mammal: Animal {}
class Monkey: Mammal {}
class Pig: Mammal {}
class Human: Mammal {}

extension Array where Element: NSObjectProtocol {
    func ofType<T: NSObjectProtocol>(_ metatype: T.Type) -> [T] {
        return flatMap { $0.isKind(of: metatype) ? $0 as? T : nil }
    }
}

let animals = [Monkey(), Pig(), Human(), Mammal(), Animal()]
func animalType() -> Animal.Type {

    return Mammal.self
}

print(animals.ofType(animalType()).count)

谢谢你的回答。我真的不想添加这个限制,因为它将限制该函数仅用于实现NSObjectProtocol的类。 - Bing
我相信在Swift中没有简单的方法来做到这一点,而不需要回到Obj-C时代的代码。如果不通过子类化NSObject来完成此操作,则付出的努力将超过其价值。通常我们在Swift中不会子类化NSObject,这并不意味着违反了“Swift方式”,而是在大多数情况下我们不需要这样做。这是那些少有的情况之一,我们需要子类化NSObject,这并不是坏事。 - Sam
1
您可以编写不需要从“NSObject”继承的代码,示例如下:https://gist.github.com/hamishknight/5bf45b025fcde4c2e8f9c67fce9fbe7e。但由于它仍依赖于Obj-C runtime,在只支持Obj-C interop(即Apple平台)的平台上才能正常工作。 - Hamish
@Hamish,你的解决方案很棒。那正是我想要的。非常感谢! - Bing
@Hamish,也许你可以将你的答案发布为“回答”,这样我就可以接受它作为正确的答案了? - Bing
显示剩余4条评论

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