3

Let's say we have an Array, assigned to a variable with the type Any

let something: Any = ["one", "two", "three"]

Let's also assume we don't know if it's an array or something entirely else. And we also don't know what kind of Array.Element we are dealing with exactly.

Now we want to find out if it's an array.

let isArray = something is Array // compiler error
let isArray = (something as? [Any?] != nil) // does not work (array is [String] and not [Any?])

Is there any elegant solution to tickle the following information out of the swift type system:

  1. Is the given object an Array
  2. What's the count of the array
  3. Give me the elements of the array

(bridging to NSArray is not a solution for me, because my array could also be of type [Any?] and contain nil-values)

15
  • The key is, at runtime it could be an array of anything. How can we cast something so we actually have the variable array? Commented Apr 4, 2016 at 19:29
  • If you already know it's an array, why don't you assign it like this let something: [Any?] = [...]? Commented Apr 4, 2016 at 19:32
  • @EmilioPelaez In reality I get the array from somewhere else (as one of the elements of another [Any] array), so the assignment above is a simplification so it's easier to explain the problem. Commented Apr 4, 2016 at 19:34
  • 1
    @stefreak consider protocol P {}; extension Array : P {}; something is P Commented Apr 4, 2016 at 19:47
  • 2
    You can also use type introspection to see if something has a collection displaystyle, e.g. if let disp = Mirror(reflecting: something).displayStyle where disp == .Collection { // is array }, but I believe @milos protocol conformance check above is the ideal one. Commented Apr 4, 2016 at 19:50

5 Answers 5

4

I love @stefreak's question and his solution. Bearing in mind @dfri's excellent answer about Swift's runtime introspection, however, we can simplify and generalise @stefreak's "type tagging" approach to some extent:

protocol AnySequenceType {
    var anyElements: [Any?] { get }
}

extension AnySequenceType where Self : SequenceType {
    var anyElements: [Any?] {
        return map{
            $0 is NilLiteralConvertible ? Mirror(reflecting: $0).children.first?.value : $0
        }
    }
}

extension Array : AnySequenceType {}
extension Set   : AnySequenceType {}
//    ... Dictionary, etc.

Use:

let things:  Any = [1, 2]
let maybies: Any = [1, nil] as [Int?]

(things  as? AnySequenceType)?.anyElements // [{Some 1}, {Some 2}]
(maybies as? AnySequenceType)?.anyElements // [{Some 1}, nil]

See Swift Evolution mailing list discussion on the possibility of allowing protocol extensions along the lines of:

extension<T> Sequence where Element == T?

In current practice, however, the more common and somewhat anticlimactic solution would be to:

things as? AnyObject as? [AnyObject] // [1, 2]

// ... which at present (Swift 2.2) passes through `NSArray`, i.e. as if we:

import Foundation
things as? NSArray  // [1, 2]

// ... which is also why this fails for `mabyies`
maybies as? NSArray // nil

At any rate, what all this drives home for me is that once you loose type information there is no going back. Even if you reflect on the Mirror you still end up with a dynamicType which you must switch through to an expected type so you can cast the value and use it as such... all at runtime, all forever outside the compile time checks and sanity.

Sign up to request clarification or add additional context in comments.

11 Comments

I personally don't think this is an issue, as the AnyArray protocol should not be conformed to by random structure and class types: it should only exist (and we should be able to put so much responsibility at the developer declaring it) as a type test protocol for Array. Hence, conforming Dummy to AnyArray above don't really, imho, cover a practical case that we would possibly run into (even if somewhat moving away from "logical error safety" and placing it in the hands of the developer... :) )
Thank you so much! :) What exactly is the problem / difference? Also your very nice three-line solution only solves the first part of my question (Is the given object an array), not part 2. and 3.
@stefreak I believe what milos points to here is the possibility of making use of the protocol AnyProtocol (conforming to it) for some structure/class/etc type, when we've only ever created this protocol for type checking if something is an array. If someone were to use AnyArray like this, it would break our type control elsewhere. So it does describe a possible problem, but one that faults in a logical error by a developer. As long as you're running a one-man-project, you're most likely safe as you know yourself never to let random types conform to AnyArray.
Yep, nicely put @dfri. But I am also +1 stefreak's solution. Excellent both Q and A.
@milos for your last part: the conditional casting only works if you get the type EXACTLY right. so if you'd write let maybies: Any = [1, nil] as [Int?]and let things: Any = [1, 2] as [Int] it would stop working
|
3

As an alternative to @milos and OP:s protocol conformance check, I'll add a method using runtime introspection of something (foo and bar in examples below).

/* returns an array if argument is an array, otherwise, nil */
func getAsCleanArray(something: Any) -> [Any]? {
    let mirr = Mirror(reflecting: something)
    var somethingAsArray : [Any] = []
    guard let disp = mirr.displayStyle where disp == .Collection else {
        return nil // not array
    }

    /* OK, is array: add element into a mutable that
     the compiler actually treats as an array */
    for (_, val) in Mirror(reflecting: something).children {
        somethingAsArray.append(val)
    }

    return somethingAsArray
}

Example usage:

/* example usage */
let foo: Any = ["one", 2, "three"]
let bar: [Any?] = ["one", 2, "three", nil, "five"]

if let foobar = getAsCleanArray(foo) {
    print("Count: \(foobar.count)\n--------")
    foobar.forEach { print($0) }
} /* Count: 3
     --------
     one
     2
     three      */

if let foobar = getAsCleanArray(bar) {
    print("Count: \(foobar.count)\n-------------")
    foobar.forEach { print($0) }
} /* Count: 5
     -------------
     Optional("one")
     Optional(2)
     Optional("three")
     nil
     Optional("five")  */

8 Comments

Still awesome and complete answer! Thank you very much for the work! :)
Awesome! Just to note that in my 2.2 playground, I need to cast in the first line: as [Any]
@milos Updated to a more general usage, do you still need to cast for this to work? The above works for me for all kinds of examples I can think up (even without casting), using Swift 2.2.
Yes, I'm getting: "error: contextual type 'Any' (aka 'protocol<>') cannot be used with array literal"
@milos: Sounds like you are not using exactly the same example as me above. You are not allowed to include nil (as an assignment by array literal) in an array assigned to a type Any (without some casting). Note that in my example above, however, foo does not contain any optionals or nil.
|
1

The only solution I came up with is the following, but I don't know if it's the most elegant one :)

protocol AnyOptional {
    var anyOptionalValue: Optional<Any> { get }
}
extension Optional: AnyOptional {
    var anyOptionalValue: Optional<Any> {
        return self
    }
}
protocol AnyArray {
    var count: Int { get }
    var allElementsAsOptional: [Any?] { get }
}
extension Array: AnyArray {
    var allElementsAsOptional: [Any?] {
        return self.map {
            if let optional = $0 as? AnyOptional {
                return optional.anyOptionalValue
            }
            return $0 as Any?
        }
    }
}

Now you can just say

if let array = something as? AnyArray {
    print(array.count)
    print(array.allElementsAsOptional)
}

1 Comment

Hi @stefreak. Your "type tagging" approach can be slightly simplified and generalised. I've added the variant to my answer (Edit 2) – see what you think.
0

This works for me on a playground:

// Generate fake data of random stuff
let array: [Any?] = ["one", "two", "three", nil, 1]
// Cast to Any to simulate unknown object received
let something: Any = array as Any

// Use if let to see if we can cast that object into an array
if let newArray = something as? [Any?] {
    // You now know that newArray is your received object cast as an
    // array and can get the count or the elements
} else {
    // Your object is not an array, handle however you need.
}

8 Comments

Here you've explicitly declared array as an array (of optional Any:s). The problem the OP described is the case where we only know something to be captureable by type Any (i.e., can be anything), and that something is not necessarily an array. If we can actually declare something as an array in the first place, we wont have any issue here.
Hmm it will stop working if you replace the first line with let array: [String?] = ["one", "two", "three", nil]
@dfri I did declare array as an array because let something: Any = [...] doesn't compile, as you can see on the answer though, array is only being used to be cast to an Any object.
@EmilioPelaez let something: Any = ["one", "two", "three"] compiles for me with Xcode 7.3/Swift 2.2.
@JAL Try let array: [Any?] = ["one", "two", "three", nil, 1], I was trying to simulate data a little bit more random than "three strings"
|
0

I found that casting to AnyObject works for an array of objects. Still working on a solution for value types.

let something: Any = ["one", "two", "three"]

if let aThing = something as? [Any] {
    print(aThing.dynamicType) // doesn't enter
}

if let aThing = something as? AnyObject {
    if let theThing = aThing as? [AnyObject] {
        print(theThing.dynamicType) // Array<AnyObject>
    }
}

2 Comments

But It seems to not work anymore when something is an array of optionals :( let something: Any = ["one", "two", "three", nil] as [String?]
@stefreak That's because Any is more "optional" than Optional<AnyObject>. Not sure how I would get around that one.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.