Bing
Bing

Reputation: 361

How to check whether an object is kind of a dynamic class type in swift?

I am implementing a function called ofType which filters out all the elements of the given type.

Here are my codes:

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.

In Objc, I can use isKindOf() to check whether an object is an instance of the class or the subclass. There are similar operations in swift is and as, but the type after them should be a static type, not a dynamic type value (e.g. I can write is Mammal, but not is Mammal.self).

I cannot use the type parameter T either because, in this example, the T equals to Animal, which is not what I want.

Do you have any idea about how to implement this function?

Upvotes: 6

Views: 2197

Answers (4)

JeremyP
JeremyP

Reputation: 86691

This works. Just use as? inside flatMap. If the animal can be cast, it will be returned otherwise nil is returned and flatMap throws it away

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

If you explicitly type the output array, the compiler can infer T from the context, otherwise you pass T's type as a parameter but don't use it in the function.


If you want to be able to dynamically check the type i.e. you don't know the type to filter at compile time, you can use mirrors as it turns out. Here's a solution which is a bit clunky but it does work:

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)")
}

Prints:

[__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]

Edit Following Sam's suggestion in the comments, it occurred to me that the above method is best put in a protocol extension.

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
    }
}

Then you can add the capability to any Swift type by making it conform to the protocol.

class Animal: TypeCheckable { ... }

extension String: TypeCheckable {}

Upvotes: 5

mugx
mugx

Reputation: 10105

You might use the reflection to find all the items that are compatible with the metatype, doing so:

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

Alternatively, with the following code I am using the operator is and I am passing directly Mammal.self to the function 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

Upvotes: 2

Hamish
Hamish

Reputation: 80951

Personally, I think @JeremyP's suggestion to use Mirror is the best; though I would make a couple of tweaks to it:

/// 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
}

Here we're using sequence(first:next:) to create a sequence of metatypes from the dynamic type of x through any superclass metatypes it might have (probably the first use of the function I've seen that doesn't look awful :P). In addition, we're falling back to doing an as? cast when we know we're not doing a class-to-class cast, which allows the function to also work with protocol metatypes.

Then you can simply say:

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]

Another option, assuming you're on an Apple platform (i.e have access to the Objective-C runtime), is to use the the Objective-C metaclass method isSubclass(of:) in order to check if a given metatype is equal, or is a subclass of another:

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
}

This works because on Apple platforms, Swift classes are built on top of Obj-C classes – and therefore the metatype of a Swift class is an Obj-C metaclass object.

Upvotes: 3

Sam
Sam

Reputation: 639

The isKindOf() method is also available in Swift, as it is a method of the NSObjectProtocol. So what you really need to do is subclass NSObject for your declaration of Animal.

NOTE: The is kind of method is renamed to isKind(of: Type) in swift.

should be as simple as

class Animal: NSObject {}

Now, all that is left, is to get around the problem that not all arrays will have elements that are a subclass of NSObject or conform to NSObjectProtocol.

To fix that we add a where clause in the declaration of the swift extension.

It should now look like

extension Array where Element: NSObjectProtocol 

Putting it all together, the final code should be similar to

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)

Upvotes: 1

Related Questions