Check whether Swift object is an instance of a given metatype
Asked Answered
R

3

10

I need to keep a collection of Swift metatypes and write a function which will check if a given object is an instance of one of them. I can do that easily in Java:

Class c = x.getClass();
c.isInstance(someObj)

However, I have no idea how to do that in Swift:

var isInt = 7 is Int.Type // compiles

let x = Int.self
var isInt = 7 is x // compiler error - Use of undeclared type 'x'

Is this even possible to be done in Swift?

Rockingham answered 2/7, 2017 at 18:48 Comment(7)
var isInt = 7 is Int.Type does actually not work. You mean var isInt = 7 is IntMaleeny
It works, check out the grammar of a "type", it includes "type -> metatype-type" - developer.apple.com/library/content/documentation/Swift/…Rockingham
oh, sorry, by "works" I meant "compiles" :)Rockingham
Check the image added in my answer. It will always fail.Kynan
now it is obvious for everyone that this is actually my first day ever of coding in swift, couldn't hide it :)Rockingham
@Rockingham Do you have control over the creation of the metatypes? If so, you can use a wrapper type in order to preserve the ability to perform an is check, compare my answer below.Alagoas
Yes, I do have control over the creation! :)Rockingham
A
12

Unfortunately, you can currently only use a named type with the is operator, you cannot yet use an arbitrary metatype value with it (although really IMO you ought to be able to).

Assuming you have control over the creation of the metatypes that you want to compare against, one solution that achieves the same result would be to create a wrapper type with an initialiser that stores a closure that performs the is check against a generic placeholder:

struct AnyType {

  let base: Any.Type
  private let _canCast: (Any) -> Bool

  /// Creates a new AnyType wrapper from a given metatype.
  /// The passed metatype's value **must** match its static value,
  /// i.e `T.self == base`.
  init<T>(_ base: T.Type) {
    precondition(T.self == base, """
      The static value \(T.self) and dynamic value \(base) of the passed \
      metatype do not match
      """)

    self.base = T.self
    self._canCast = { $0 is T }
  }

  func canCast<T>(_ x: T) -> Bool {
    return _canCast(x)
  }
}

protocol P {}
class C : P {}
class D : C {}

let types = [
  AnyType(P.self), AnyType(C.self), AnyType(D.self), AnyType(String.self)
]

for type in types {
  print("C instance can be typed as \(type.base): \(type.canCast(C()))")
  print("D instance can be typed as \(type.base): \(type.canCast(D()))")
}

// C instance can be typed as P: true
// D instance can be typed as P: true
// C instance can be typed as C: true
// D instance can be typed as C: true
// C instance can be typed as D: false
// D instance can be typed as D: true
// C instance can be typed as String: false
// D instance can be typed as String: false

The only limitation of this approach is that given we're performing the is check with T.self, we have to enforce that T.self == base. For example, we cannot accept AnyType(D.self as C.Type), as then T.self would be C.self while base would be D.self.

However this shouldn't be a problem in your case, as we're just constructing AnyType from metatypes that are known at compile time.


If however you don't have control over the creation of the metatypes (i.e you get handed them from an API), then you're quite a bit more limited with what you can do with them.

As @adev says, you can use type(of:) in order to get the dynamic metatype of a given instance, and the == operator to determine if two metatypes are equivalent. However, one problem with this approach is that it disregards both class hierarchies and protocols, as a subtype metatypes will not compare equal with a supertype metatypes.

One solution in the case of classes is to use Mirror, as also shown in this Q&A:

/// Returns `true` iff the given value can be typed as the given
/// **concrete** metatype value, `false` otherwise.
func canCast(_ x: Any, toConcreteType destType: Any.Type) -> Bool {
  return sequence(
    first: Mirror(reflecting: x), next: { $0.superclassMirror }
  )
  .contains { $0.subjectType == destType }
}

class C {}
class D : C {}

print(canCast(D(), toConcreteType: C.self)) // true
print(canCast(C(), toConcreteType: C.self)) // true
print(canCast(C(), toConcreteType: D.self)) // false
print(canCast(7, toConcreteType: Int.self)) // true
print(canCast(7, toConcreteType: String.self)) // false

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.

However this method still won't work with protocols. Hopefully a future version of the language will provide much richer reflection APIs that allow you to compare the relationship between two metatype values.


However, given the above knowledge of being able to use Mirror, we can use it to lift the aforementioned restriction of T.self == base from our AnyType wrapper on by handling class metatypes separately:

struct AnyType {

  let base: Any.Type
  private let _canCast: (Any) -> Bool

  /// Creates a new AnyType wrapper from a given metatype.
  init<T>(_ base: T.Type) {

    self.base = base

    // handle class metatypes separately in order to allow T.self != base.
    if base is AnyClass {
      self._canCast = { x in
        sequence(
          first: Mirror(reflecting: x), next: { $0.superclassMirror }
        )
        .contains { $0.subjectType == base }
      }
    } else {
      // sanity check – this should never be triggered,
      // as we handle the case where base is a class metatype.
      precondition(T.self == base, """
        The static value \(T.self) and dynamic value \(base) of the passed \
        metatype do not match
        """)

      self._canCast = { $0 is T }
    }
  }

  func canCast<T>(_ x: T) -> Bool {
    return _canCast(x)
  }
}

print(AnyType(D.self as C.Type).canCast(D())) // true

The case where T.self is a class metatype should be the only case where T.self != base, as with protocols, when T is some protocol P, T.Type is P.Protocol, which is the type of the protocol itself. And currently, this type can only hold the value P.self.

Alagoas answered 2/7, 2017 at 21:56 Comment(1)
Amazing idea to have a generic initializer and "capturing" T in a closure! Thanks a lot.Annia
K
2

Ideally the following should work in your case with some changes as type(of: 7) instead of 7 and == operator instead of is. But swift has a bug which throws following error.

let x = Int.self
var isInt = type(of: 7) == x //binary operator '==' cannot be applied to two 'Int.Type' operands

Instead you can use the following code and it will work fine.

let x = Int.self
var typeOfX = type(of: 7)
var isInt = typeOfX == x //true

Apple engineers have confirmed this bug here: Joe Groff - Twitter

First line in your question should be var isInt = 7 is Int. Notice Int instead of Int.Type. Otherwise Xcode will throw below warning.

enter image description here

Usually in most cases you could just do:

if z is String {
   //do something
} 
Kynan answered 2/7, 2017 at 19:50 Comment(9)
just a small remark - Int.Type also works, I already answered with a link to a comment above :)Rockingham
@Rockingham Check the image added now. It wont work as shown in the warning displayed. That will always fail.Kynan
yep, you are right, I actually meant "compiles", corrected myself :) my problem was to make it compile first, that's why I was not thinking about results for now :)Rockingham
as I understood, subclasses cannot be checked like thisRockingham
you can use it as "if z is MyClass" where is z is of subclass type. Is it not working for you?Kynan
I meant the first part of you answer related to my actual question, using type1 == type2 instead of "is" statement will not work for subclasses. I think I can live with it, just need to know the exact types when storing them.Rockingham
You can do that too. If you want to check if it is a subclass, you can use let isSubclass = typeOfX.isSubclass(of: x) where typeOfX is type of any class and x is the Class.self. If it is of type String, Int etc.. the above code will work. isKindOfClass and isSubclassOf are objective c functions and since swift can work with objective c, you can use that for this purpose.Kynan
downvote from me as this answer does not work, hence misleadingAekerly
@Kynan i must apologize, answer actually do work, my mistake. Now i cannot remove my downvote (SO informs that my vote is locked unless post will be edited).Aekerly
M
0

Int.self does not always means holding Int.type.

The object returned from calling MyClass.self is the swift metaType of MyClass. This object exposes init function and all the method defined in this class as curried method (read Instance Methods are Curried Functions in Swift).

how about using isKindOfClass?

isKindOfClass: returns YES if the receiver is an instance of the specified class or an instance of any class that inherits from the specified class.

reference: https://medium.com/ios-os-x-development/types-and-meta-types-in-swift-9cd59ba92295

Mcmann answered 2/7, 2017 at 19:4 Comment(2)
What do you mean "Int.self does not always means holding Int.type"? Int.self is a metatype value, of type Int.Type. And because Int is a value type, the only value the Int.Type metatype can have is Int.self.Alagoas
how to use "isKindOfClass" in swift?Rockingham

© 2022 - 2024 — McMap. All rights reserved.