3

Swift 1.2
I'm trying to pattern match in a switch case in a function that take a type Any as it's parameter, in order to dispatch to a private more specialize init.

Here is a Playground extrapolation :

import Foundation

struct myStruct {
}

func switchOnAny(any: Any) -> String {
    println("Dynamic Type == \(any.dynamicType)")
    switch any {
    case let array as [Any]:
        return "Array"
    case let array as NSArray:
        return "NSArray"
    default:
        return "Default"
    }
}

let emptyStringArray : [String] = []
let stringArray : [String] = ["Bob", "Roger"]
let intArray = [1, 2, 3]
let customStructArray : [myStruct] = []

println("\t\touput : \(switchOnAny([]))")
println("\t\touput : \(switchOnAny(emptyStringArray))")
println("\t\touput : \(switchOnAny(stringArray))")
println("\t\touput : \(switchOnAny(intArray))")
println("\t\touput : \(switchOnAny(customStructArray))")

Wich produce the following output :

Dynamic Type == __NSArrayI
ouput : NSArray
Dynamic Type == Swift.Array
ouput : NSArray
Dynamic Type == Swift.Array
ouput : NSArray
Dynamic Type == Swift.Array
ouput : NSArray
Dynamic Type == Swift.Array<__lldb_expr_37.myStruct>
ouput : Default

I am wondering why the case as [Any] don't gets it since I'm never requesting an NSArray?

And can I assume that any kind of Swift array will get in the NSArray case or will I need to write 2 case statement (one for NSArray and one for [Any]) in order to cover my back (apparently there will be a need)?


After making some more test, I can see that when I'm providing an array of a custom struct none of the pattern will match. I will need to have a match like [myStruct] for it to recognize. Which is exactly what I'm trying to avoid, because it is only one of the option that I can receive.


To give more context I've put my project on Github : https://github.com/VinceBurn/SwiftyPlist/tree/test/init-Any. The project is about TDD and representing a Property list as a Struct like tree that can be accessed by subscript. (like SwiftyJSON)

Vincent Bernier
  • 8,674
  • 4
  • 35
  • 40
  • Do you only want to filter values that are arrays (either `NSArray` or Swift `Array`)? – Dániel Nagy Aug 22 '15 at 16:29
  • the input I want can be : Int, Float, String, myStruct, [Int], [myStruct], [String], [Float], [NSNumber], NSDate, NSData, [NSDate], [NSData], NSArray, NSDictionary, [String : myStruct]. So my goal would be to be able to have a single 'case' for all form of Array – Vincent Bernier Aug 22 '15 at 16:42
  • 1
    possible duplicate of [Swift check if value is of type array (of any type)](http://stackoverflow.com/questions/32033993/swift-check-if-value-is-of-type-array-of-any-type) – Kametrixom Aug 22 '15 at 18:05
  • @VinceBurn I updated my answer with a pattern matching example. – Dániel Nagy Aug 22 '15 at 20:30
  • Your linked example code is incorrectly using `Any`. You don't mean `Any`. You mean "types that can be converted to a property list." The fact that you needed a default assertion demonstrates that `Any` was the wrong tool. You want a `PropertyListConvertible` protocol. Then most of your complexity would go away. Rethink your problem in terms of protocols rather than in terms of type-casting and reflection. – Rob Napier Aug 22 '15 at 22:36
  • @RobNapier I know that Any is a very imperfect solution, but using `case let array as [PlistConvertible]:` with extension on every type that can be converted to declare the protocol, still don't enter in the correct case when I'm passing an empty array of type [Plist] – Vincent Bernier Aug 22 '15 at 23:15
  • Even though I said there is no workaround I've probably found a solution for your problem. See my edited answer below. – Qbyte Aug 24 '15 at 16:36

4 Answers4

3

To decide the most reliably whether a variable is any kind of array is to use reflection, in Swift 1.2:

let array = []
let mirror = reflect(array)
let isArray = mirror.disposition == MirrorDisposition.IndexContainer

and in Swift 2.0:

let anArray = []
let mirror = Mirror(reflecting: anArray)
let isArray = mirror.displayStyle == .Collection

And just for curiosity, it is interesting to check out these enums:

enum MirrorDisposition { //Swift 1.2
    case Struct
    case Class
    case Enum
    case Tuple
    case Aggregate
    case IndexContainer
    case KeyContainer
    case MembershipContainer
    case Container
    case Optional
    case ObjCObject
}

enum DisplayStyle { //Swift 2.0
    case Struct
    case Class
    case Enum
    case Tuple
    case Optional
    case Collection
    case Dictionary
    case Set
}

UPDATED Here is a full pattern match example:

func switchOnAny(any: Any) -> String {
    println("Dynamic Type == \(any.dynamicType)")
    switch any {
    case let array as Any where reflect(any).disposition == MirrorDisposition.IndexContainer:
        return "Any kind of array"
    default:
        return "Default"
    }
}
Dániel Nagy
  • 11,815
  • 9
  • 50
  • 58
2

Unfortunately casting between generic types like Array is not fully supported (yet). There are also odd situations even if you want to upcast:

let emptyStringArray : [String] = []
emptyStringArray as [Any]    // succeeds

let stringArray : [String] = ["Bob", "Roger"]
stringArray as [Any]         // error! due to the elements?!

let intArray = [1, 2, 3]
intArray as [Any]            // error

let customStructArray : [myStruct] = []
customStructArray as [Any]   // '[myStruct]' is not convertible to '[Any]'

There is also no good workaround without using a protocol. If you really want to have this dynamic behavior you could use reflections with the reflect() function. In Swift 2 they are more powerful, but it is still not a good solution.

Edit:

A solution with a protocol which gets adopted by all Arrays through an extension (only for your specific case):

protocol ArrayType {
    var anyValues: [Any] { get }
}

extension Array: ArrayType {
    var anyValues: [Any] {
        return self.map { $0 as Any }
    }
}

// now the switch gets rewritten as
switch any {
case let array as ArrayType:
    let anyArray = array.anyValues
    return "Array"
case let array as NSArray:
    return "NSArray"
default:
    return "Default"
}
Qbyte
  • 12,753
  • 4
  • 41
  • 57
  • 1
    The last three examples of yours kinda do make sense, because e.g an array of `String`s doesn't occupy the same space as an array of `Any`: `sizeof(Any) = 32`, `sizeof(String) = 24`. To convert an array of a certain type to an array of `Any` you have to make an explicit cast (as mentioned they do not occupy the same space and don't represent the same): `stringArray.map{ $0 as Any }`. This may look weird and like it shouldn't be but it actually makes sense if you think about it. – Kametrixom Aug 22 '15 at 18:14
  • @Kametrixom Great explanation. But the `map` could/should be done implicitly if you upcast. Hopefully in Swift 2.1 – Qbyte Aug 22 '15 at 18:26
  • the thing with `as` is that it's not supposed to do nothing other than what the programmer wants. `as` only changes the representation of a variable and shouldn't change the underlying data. That's the reason this won't happen in the next Swift is that `as` should stay a non-cost operation, the programmer should do the more expensive cast when necessary himself. That's also the reason you can't cast directly with `as` from an `Int` to a `Double` and you have to use `Double(myInt)`. – Kametrixom Aug 22 '15 at 18:34
  • @Kametrixom Even if it is changing the underlying data a cast from `String` to `Any` still succeeds but a generic type doesn't work? However optionals are a special case, they can be casted form `Optional` to `Optional`. Just curious, where did you get your information? – Qbyte Aug 22 '15 at 19:05
  • Ohh right it does work from String to Any.. Dunno why. I read it online, don't know where anymore, but there have been a lot of discussions about heterogenous collections recently, where I have seen the `.map{ $0 as Any }` and the explanation – Kametrixom Aug 22 '15 at 19:09
1

I suggest extending the Array type with a custom protocol that you can use to check it like so:

protocol ArrayType {}
extension Array : ArrayType {}

Then you can do something like this:

let array : Any = [1, 2, 3, 4]
array is ArrayType    // true

Also have a look at my other answer here.

But it actually looks like you don't want to have a single public initializer which takes Any as an argument (You very rarely want that at all), but rather two different initializers, one for arrays, one for non-arrays like so:

class MyClass {
    init<T>(array: [T]) {

    }

    init<T>(nonArray: T) {

    }
}
Community
  • 1
  • 1
Kametrixom
  • 14,673
  • 7
  • 45
  • 62
  • Thanks, but the protocol solution don't work for all case, I still have a case where it's passing over. And I actually want a single Init, because I want to implement the RawRepresentable. I will edit the question for more context. – Vincent Bernier Aug 22 '15 at 22:23
  • @VinceBurn When it is passing over? In which case? – Qbyte Aug 23 '15 at 08:58
  • on this case : switchOnAny([]), when passing a non-casted Array. OK I need to add also : extension NSArray : ArrayType {}. But then how can I iterate over that collection in the switch case? – Vincent Bernier Aug 23 '15 at 14:21
0

Conclusion : in Swift 1.2

with the Kametrixom and Daniel Nagy answer, it is possible to enter a single switch case for all sort of Array.
But inside the case, I've not been able to cast the item to a usable array for all case.

So in conclusion I'm left with 2 case statement, one

case let array as NSArray:
    return "NSArray"
case let array as [myStruct]:
    return "myStruct array"
Vincent Bernier
  • 8,674
  • 4
  • 35
  • 40