12

I have a Swift function that accepts Any and I want it to be able to accept an array of Strings, an array of Ints, a mixed array, or an array of arrays, etc. It also can accept just a String or an Int, etc, not in an array.

So I have this:

    private func parse(parameter: Any) {
        if parameter is Int {
            // Int
        } else if (parameter is Float) || (parameter is Double) {
            // Double
        } else if parameter is String {
            // String
        } else if parameter is Bool {
            // Bool
        } else if let array = parameter as? [Any] {
            // Should catch all Arrays
        } else {
            assert(false, "Unsupported type") // [String] ends up here
        }
    }

But if I call parse(["Strings"]), the assert is raised. How can I catch all types of Arrays?

edit - there was some confusion as to what I'm trying to accomplish. I basically need to return a String based on the type, so Int -> "" and String -> "", so an array would make recursive calls to return "..."

This post is marked as a duplicate, but that other question is about Javascript, not Swift.

Ryan
  • 3,853
  • 4
  • 28
  • 32

4 Answers4

13

I finally found the way to do that, which is to use NSArray for casting.

private func parse(x: Any) {
    if let o = x as? [Any] {
        println("[Any]")
    }
    if let o = x as? [AnyObject] {
        println("[AnyObject]")
    }
    if let o = x as? NSArray {
        println("NSArray")
    }
}

let a: [Any] = ["bar"]
let b: [AnyObject] = ["bar"]
let c = ["foo", 3.14]

parse(a) // ==> [Any]
parse(b) // ==> [AnyObject], and also NSArray
parse(c) // ==> NSArray

It look so that an array containing values of Any internally represented in NSArray. (But should it be able to cast c to [Any]...? I'm suspecting it's a bug.)

gabriellanata
  • 3,946
  • 2
  • 21
  • 27
findall
  • 2,176
  • 2
  • 17
  • 21
  • Gimme a minute to try this idea in my implementation! – Ryan Oct 07 '14 at 02:54
  • Bam! That works! I'm going to keep trying to simplify to get a good example to radar, but I'll use this in the mean time. – Ryan Oct 07 '14 at 03:04
  • Swift 5 it looks like it doesn't work anymore.. "error: cannot convert value of type 'String' to expected element type 'AnyObject'" for line let b: [AnyObject] = ["bar"] – Jerem Lachkar Jan 03 '21 at 17:03
7

The key to understanding typing and type related issues in Swift is that all roads lead to protocols.

The challenge of this problem is detecting any type of array, not just one concrete type. The OP's example failed because [Any] is not a base class or a generalized pattern of [String], that is to say, that (from what I can tell), in Swift [T] is not covariant on T. Beyond that, you cannot check for SequenceType or CollectionType since they have associated types (Generator.Element).

The idiomatic solution is thus to use a marker protocol to indicate which types you want to match your criteria. As illustrated below, you achieve this by creating an empty protocol, and associating it with the types of interest.

import Foundation


protocol NestedType {}
extension Array: NestedType {}
extension Set: NestedType {}
extension Dictionary: NestedType {}
extension NSSet: NestedType {}

protocol AnyTypeOfArray {}
extension Array: AnyTypeOfArray {}
extension NSArray: AnyTypeOfArray {}

protocol AnyTypeOfDictionary {}
extension Dictionary: AnyTypeOfDictionary {}


func printType(v:Any) {
    if v is NestedType {
        print("Detected a nested type")
    }

    if v is AnyTypeOfArray {
        print("\t which is an array")
    }

    else if v is AnyTypeOfDictionary {
        print("\t which is a dictionary")
    }
}


printType([String:Int]())
printType([Int]())
printType(NSArray())

The output of which is:

Detected a nested type
     which is a dictionary
Detected a nested type
     which is an array
Detected a nested type
     which is an array
Chris Conover
  • 8,889
  • 5
  • 52
  • 68
3

One way you can do this is to separate the function out to two separate implementations (with the same name), one that takes anArray and one for everything else. You'll also need to make them generic functions instead of using the Any type. With that setup, Swift can use type inference to figure out the best function to call.

I'd implement it something like this (I'm just printlning the type to show where things end up):

func parse<T>(parameter: T) {
    if parameter is Int {
        println("Int")
    } else if (parameter is Float) || (parameter is Double) {
        println("Double")
    } else if parameter is String {
        println("String")
    } else if parameter is Bool {
        println("Bool")
    } else {
        assert(false, "Unsupported type")
    }
}

func parse<T>(parameter: Array<T>) {
    println("Array")
    for element in parameter {
        // Recursively parsing...
        parse(element)
    }
}

Then calling it like this:

parse(1)  // Int
parse(0.1) // Double
parse("asdf") // String
parse(true) // Bool
parse(["asdf", "asdf"]) // Array -> String String

Outputs:

Int
Double
String
Bool
Array
String
String
Mike S
  • 41,895
  • 11
  • 89
  • 84
  • Running your code as is works perfectly. Running it in the context of my code, however, breaks it. I'm unpacking the parameters from an array of type [Any] and calling parse() on each element. No matter what the element is, the first function is called, never the second. – Ryan Oct 07 '14 at 01:43
  • This fails: parseAny([["a", "b", "c"]]) func parseAny(values: [Any]) { for value in values { parse(value) } } – Ryan Oct 07 '14 at 01:54
  • And so does the same, except changing parseAny([Any]) to parseAny(Array) – Ryan Oct 07 '14 at 01:55
  • Hmm, yep, you're right... offhand I'm not sure why, but I'll think about it a bit and see if I can come up with something. – Mike S Oct 07 '14 at 02:14
  • The only other thing I can think of is some sort of wrapper struct. – Ryan Oct 07 '14 at 02:27
0

You can use the _stdlib_getTypeName that returns the mangled type name for the given value.

For example:

    var myString = "String"
    var myInteger = 10
    var myArray = [10,22]
    var myDictionary = ["one": 1, "two": 2, "three": 3]
    println("\(_stdlib_getTypeName(myString))")
    println("\(_stdlib_getTypeName(myInteger))")
    println("\(_stdlib_getTypeName(myArray))")
    println("\(_stdlib_getTypeName(myDictionary))")

The result will be:

_TtSS // for String
_TtSi // for integer
_TtSa // for array
_TtVSs10Dictionary // for dictionary
Sebastian
  • 6,154
  • 5
  • 33
  • 51
  • This would technically let me know when I've been given array, but wouldn't let me treat it as an array without trying to cast it. The problem is, a forced downcast using `parameter as [Any]` causes a runtime crash. Also, this is super hacky. – Ryan Oct 07 '14 at 01:38