tuledev
tuledev

Reputation: 10317

How to know if an object is an instance of subclass or superclass

I have 2 classes:

class Parent
{
  func a() {
     self.b()
  }
  func b() {
     // I want to check here
     if self is Parent  // warning "'is' test is always true"
     {
        log("instance of parent")
     }
  }
}
class Child:Parent
{
}

I want to check like this

//
var child = Child()
child.a()  // don't see log
var parent = Parent()
parent.a()  // see log

I know that I can create a method like description in superclass, and override it in subclass. I wonder if Swift can check it without implement description

Thanks for your help

Upvotes: 9

Views: 9385

Answers (4)

Lucas Chwe
Lucas Chwe

Reputation: 2768

The answer should be more simple, so it handles unexpected errors. ie. maybe you added a new subclass, but you forgot to update the switch statement, hence incorrectly printing Parent.

The quick example I gave above, should either not print anything, or print Unexpected Class or something like that so everything remains true, consistent, and future-proof. Seems like a no brainer to check the boolean child is Child but you gotta do that carefully in a timely manner because the is keyword always returns true for subclasses (hence, the warning mentioned in the question 'is' test is always true occurs when doing self is Parent)

Here is my solution:

class Parent {

    // MARK: Main method

    func printClass() {
        print(className)
    }

    // MARK: Helper methods

    var isParent: Bool {
        return type(of: self) === Parent.self
    }

    static var className: String {
        return String(describing: self)
    }

    var className: String {
        return type(of: self).className
    }

    // MARK: Implementation to recognize Parent, Child and any other Subclass

    // Approach #1: Identify if "self" == "Parent"
    func printClassOrSubclass() {
        guard isParent else {
            // Case #1: Class of "self" != "Parent", so "self" must be a subclass of "Parent"
            print("\(className), subclass of Parent")
            return
        }

        // Case #2: Class of "self" == "Parent", its not a subclass
        printClass()
    }

    // Approach #2: Identify what class is "self" exactly. This approach is useful for calling a method or do something with "self" as a subclass instance (ie. "child == self as! Child")
    func printClassOrSubclassWithDowncasting() {
        guard !isParent else {
            // Case #1: Class of "self" == "Parent", so "self" is not a subclass of "Parent"
            printClass()
            return
        }

        // Switch #1: Attempt to identify the exact class of "self", which is definitely a subclass of "Parent" by now.
        switch self {
        case let child as Child where className == Child.className:
            // Case #2: Class of "self" == "Child"
            child.printChildClass()

            // In each case of Switch #1, returning is important so we dont continue to the Switch #2 by accident
            return
        default:
            // Switch #1 failed to identify the exact class of self. Code will continue with Switch #2
            break
        }

        // Switch #2: Attempt to identify if "self" is a subclass of a subclass of Parent (aka, subclass of "Child")
        switch self {
        case let subChild as Child:
            // Case #3: Class of "self" != "Child" but is a subclass of "Child". (Maybe a different dev introduced "GrandChild" Class and forgot to tell you, classic)

            print("\(className), unexpected subclass of Child")
            subChild.printChildClass()
        default:
            // Case #4: Class of "self" could not be identified at all, but its still a subclass of "Parent". (Maybe marketing is testing an "Uncle" Class?)

            print("\(className), unexpected subclass of Parent")
            break
        }
    }
}

class Child: Parent {
    func printChildClass() {
        print("\(className), subclass of \(String(describing: class_getSuperclass(type(of: self))!))")
    }
}

// Unexpected Subclasses
class GrandChild: Child {}
class Uncle: Parent {}

Test:

let parent = Parent()
parent.printClassOrSubclass()
parent.printClassOrSubclassWithDowncasting()
print("\n")

let child = Child()
child.printClassOrSubclass()
child.printClassOrSubclassWithDowncasting()
print("\n")

let grandChild = GrandChild()
grandChild.printClassOrSubclass()
grandChild.printClassOrSubclassWithDowncasting()
print("\n")

let uncle = Uncle()
uncle.printClassOrSubclass()
uncle.printClassOrSubclassWithDowncasting()

Result:

Parent
Parent

Child, subclass of Parent
Child, subclass of Parent

GrandChild, subclass of Parent
GrandChild, unexpected subclass of Child
GrandChild, subclass of Child

Uncle, subclass of Parent
Uncle, unexpected subclass of Parent

Notes:

I did say simple, but I wanted to make a thorough response addressing most cases. That said, you can just adjust/simplify the code for your specific use case. Also the 2 switch statements could be combined into 1, but you must have the switch cases within in perfect order (must always check for exact class, before checking subclass), otherwise you ll get wrong results. For my answer, I was having in mind that multiple devs could be handling and adding new classes and subclasses. So, imo the 2 switches should encourage having a better structure and less confusion for a longer term.

Upvotes: 1

Code Different
Code Different

Reputation: 93141

You get the warning because the the function is attached to the class it's defined within. As such, the compiler already knows ahead of time what type self is. But your design is not a good one at all. For example, let's define a printMe function on the parent:

class Parent
{
  func printMe() {
    if self is Parent {
        print("Parent")
    }
  }
}

class Child: Parent {}

let child = Child()
child.printMe() // Output: Parent

Okay, the Child class inherits printMe from Parent, so let's override it:

class Child: Parent {
    func printMe() {
        if self is Child {
            print("Child")
        }
    }
}

And you have to redefine printMe for every subclass. In each case, the compiler already know what class the function belongs to so why bother doing the is test?


The proper design pattern is to use a protocol:

class Parent: MyProtocol {}
class Child: Parent {}

protocol MyProtocol {
    func printMe()
}

extension MyProtocol {
    func printMe() {
        if let me = self as? Child {
            print("Child")
        } else if let me = self as? Parent {
            print("Parent")
        }
        // Do something with `me` or you gonna get another warning
    }
}

let parent = Parent()
let child = Child()
parent.printMe()
child.printMe()

Upvotes: 3

Uros19
Uros19

Reputation: 391

Its really simple, use is keyword.

if child is Child

Upvotes: 13

Jordan H
Jordan H

Reputation: 55665

This can be accomplished using the as type cast operator:

var child = Child()

if let child = child as? Child {
    //you know child is a Child
} else if let parent = child as? Parent {
    //you know child is a Parent
}

There is also the is keyword:

if child is Child {
    //is a child
}

Note that in your code you're seeing a warning using is - it will always be true, because comparing self to Parent from within class Parent will always be true. If you were comparing it against some other instance of a class instead of self, or if you were comparing against some other type besides Parent, this warning would disappear.

I would recommend reading more about this in the Swift Programming Language book, found on the iBooks Store - see the Type Casting chapter.

Upvotes: 7

Related Questions