Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

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?

like image 566
Bing Avatar asked Jan 31 '18 06:01

Bing


1 Answers

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 {}
like image 186
JeremyP Avatar answered Nov 11 '22 03:11

JeremyP