7

I'm fiddling around with generics in Swift and hit something I can't figure out: If I cast a value into the type of a generic parameter, the cast is not performed. If I try the same with static types, it works.

class SomeClass<T> {
    init?() {
        if let _ = 4 as? T {
            println("should work")
        } else {
            return nil
        }
    }
}

if let _ = SomeClass<Int?>() {
    println("not called")
}

if let _ = 4 as? Int? {
    println("works")
}

Can anybody explain this behavior? Shouldn't be both cases equivalent?

Update

The above example is simplified to the max. The following example illustrates the need for a cast a little better

class SomeClass<T> {
    init?(v: [String: AnyObject]) {
        if let _ = v["k"] as? T? {
            print("should work")
        } else {
            print("does not")
            return nil
        }
    }
}

if let _ = SomeClass<Int?>(v: ["k": 4]) {
    print("not called")
}

if let _ = SomeClass<Int>(v: ["k": 4]) {
    print("called")
}

2nd Update

After @matt made me learn about AnyObject and Any and @Darko pointed out in his comments how dictionaries make my example too complicated, here's my next refinement

class SomeClass<T> {
    private var value: T!

    init?<U>(param: U) {
        if let casted = param as? T {
            value = casted
        } else {
            return nil
        }
    }
}


if let _ = SomeClass<Int?>(param: Int(4)) {
    println("not called")
}

if let _ = SomeClass<Int>(param: Int(4)) {
    println("called")
}

if let _ = Int(4) as? Int? {
    println("works")
}

if let _ = (Int(4) as Any) as? Int? {
    println("Cannot downcast from Any to a more optional type 'Int?'")
}

I tried using init?(param: Any) before, but that yields the same problem illustrated in the last if which is discussed elsewhere.

So all it comes down to: Has anyone really been far as to ever cast anything to a generic optional type? In any case? I'm happy to accept any working example.

Community
  • 1
  • 1
Sebastian
  • 2,109
  • 1
  • 20
  • 15
  • I'm also stuck on this; feels like a hole in the language. Did you ever get a resolution? – Chris Hatton Feb 02 '17 at 00:32
  • I just tried the third example in an Xcode 8.2.1 project again (Playgrounds are currently broken for me) and it runs as expected, printing `not called` and `called`. I could also change `T!` to `T`. – Sebastian Feb 27 '17 at 06:17

3 Answers3

3

This is really not about generics at all; it's about AnyObject (and how casting works). Consider:

    let d = ["k":1]
    let ok = d["k"] is Int?
    print (ok) // true

    // but:

    let d2 = d as [String:AnyObject]
    let ok2 = d2["k"] is Int?
    print (ok2) // false, though "is Int" succeeds

Since your initializer casts the dictionary up to [String:AnyObject] you are in this same boat.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • So why does 'let ok2 = d2["k"] is Int' succeed? dict adds always the optional to the return value and apart from that Int is not a class type like AnyObject. – Darko Aug 19 '15 at 23:31
  • Because when you say "is", an Optional gets special treatment; we pretend you posed to the question "is" to the thing-inside-the-optional. So now we're asking "Can this AnyObject be cast down to an Int" - and it can, because it is an NSNumber. But an AnyObject cannot be cast down to an `Optional`. – matt Aug 20 '15 at 00:24
  • In other words, once you realize that the generic in the original question is a complete red herring, everything happening here is covered by my book's discussion of casting (http://www.apeth.com/swiftBook/ch04.html#_casting) and of AnyObject (http://www.apeth.com/swiftBook/ch04.html#_anyobject). – matt Aug 20 '15 at 00:32
  • Ok, so the intermediate step over NSNumber was the missing link to understanding. I just tried to `let d : [String: Int64] = ["k":1]` and you are right, the cast to `[String: AnyObject]` does not work anymore, because NSNumber is not compatible with Int64. But why AnyObject can not be cast down to Optional? It could assume Optional. You wrote: "Because when you say "is", an Optional gets special treatment; we pretend you posed to the question "is" to the thing-inside-the-optional." It's somehow inconsistent. (can not find the explanation in your book) – Darko Aug 20 '15 at 00:58
  • I don't see why it's inconsistent. `Optional` is not a class type. It is not bridged to Objective-C. You cannot cast an `Int?` or an `NSNumber?` _up_ to AnyObject, so there's no chance in the world that you'd ever be able to cast an AnyObject _down_ to either of those things. What surprises me is that the compiler lets you pose the question at all. – matt Aug 20 '15 at 01:23
  • Ok, I see. Thanks for the explanation, it's logical. The Obj-C bridge breaks many consistencies in the language in my opinion. E.g. there is no implcit casting in Swift - but it's their for the Obj-C Bridge. Integers are casted to NSNumber - except Int64. var arr = ["text", 2] is inferred as NSDictionary, but it should be NSMutableDictionary when "var" is used. etc... (+1) – Darko Aug 20 '15 at 04:04
  • Thank you very much for your great explanation! You made me aware of the difference between `Any` and `AnyObject` (unfortunately `Any` seems to have different problems: http://stackoverflow.com/questions/27989094/how-to-unwrap-an-optional-value-from-any-type). However I still don't understand why my first example does not work, which is not about `AnyObject`..or is it? – Sebastian Aug 20 '15 at 07:23
1

This works as expected now - in Playgrounds as well as in projects. Retested in Swift 5.4:

class SomeClass<T> {
    private var value: T

    init?<U>(param: U) {
        if let casted = param as? T {
            value = casted
        } else {
            return nil
        }
    }
}


if let _ = SomeClass<Int?>(param: Int(4)) {
    print("called")
}

if let _ = SomeClass<Int>(param: Int(4)) {
    print("called")
}

which prints called two times as expected.

Sebastian
  • 2,109
  • 1
  • 20
  • 15
  • Side note: My previous reply from 2017 that it's working as expected got deleted. Not sure why. The question just received another upvote, so I thought it might be worth sharing the current state of things. – Sebastian Oct 03 '21 at 11:29
0

As far I can see the goal of your updated code is to find out if the passed parameter (the value of the dict) is of type T. So you are mis-using the as? cast to check the type. What you actually want is the "is" operator.

class SomeClass<T> {
    init?(v: [String: AnyObject]) {
        if v["k"] is T {
            print("called if AnyObject is of type T")
        } else {
            print("called if AnyObject is not of type T")
            return nil
        }
    }
}

if let _ = SomeClass<Int>(v: ["k": 4]) {
    print("called")
}

if let _ = SomeClass<Int?>(v: ["k": 4]) {
    print("not called")
}
Darko
  • 9,655
  • 9
  • 36
  • 48