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.
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.)
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
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
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
Related
If the defined array is empty, why the data type inside the array does not work
I create a empty Character Array, and I confused that why the result of a is Array<Bool> is true.
In fact, i tried a is Array<T> (T: Bool, String, Int...), all of them is true.
var a: Array<Character> = []
a is Array<Bool>
The result is:
true
After some digging, I found that this is the intended behavior.
Swift checks whether an array can be downcast by attempting to downcast all of the source elements to the destination element type. Since an empty array contains no elements, that means down casting it to another array with a different element type is always successful.
Below are swift's code for handling the as? operator. I doubt that swift handles the is operator differently.
public func _arrayConditionalCast<SourceElement, TargetElement>(
_ source: [SourceElement]
) -> [TargetElement]? {
var successfulCasts = ContiguousArray<TargetElement>()
successfulCasts.reserveCapacity(source.count)
for element in source {
if let casted = element as? TargetElement {
successfulCasts.append(casted)
} else {
return nil
}
}
return Array(successfulCasts)
}
https://github.com/apple/swift/blob/main/stdlib/public/core/ArrayCast.swift (line 74)
Sources
A similar bug report an swift.org: SR-7738
Some convincing explanation: SR-6192
I'm trying to parse a json which has an array with Strings, Ints and Arrays
So I'm to iterate on the json {["item1", 2, ["subItem1", "subitem2"] ]} members in the array:
func parse(json : [Any])
for item in json{
if let arr = item as? Array { //
//do stuff for array
}
}
but I'm getting this compile error:
Generic parameter 'Element' could not be inferred in cast to
'Array<_>'
Optional bind item to the different types
for item in json {
if let stringItem = item as? String {
//do stuff for String
} else if let intItem = item as? Int {
//do stuff for Int
} else if let arrayItem = item as? [String] {
//do stuff for Array
} else {
// it's something else
}
}
You receive the error is because in Swift, Array is a generic container that holds values of a specific type. So you can have an Array<Int> that holds integers, or an Array that holds strings. But you can’t have just an Array. The type of the thing the array contains is the generic parameter, and Swift is complaining because it can’t figure out what that type should be. Sometimes it can infer that type from the context of the code around it, but not always, as in this case.
func parse(json : [Any])
for item in json{
if let arr = item as? Array<Any> { //
//do stuff for array
}
}
Instead of writing Array<Any>, you can write the shorter form, [Any].
You can also solve the problem by using NSArray, as you’ve found. Unlike Array, NSArray doesn’t use generics, since it originates in Objective-C which has a different approach to Swift. Instead, NSArray holds only one kind of thing, an AnyObject. This is is a reference that can point to instances of any class.
I have faced an issue.
Consider I have one protocol and two classes implementing it:
protocol Initiatable{
init()
}
class A: Initiatable{
required init() {}
}
class B: Initiatable{
required init() {}
}
then at some point I'm creating an array and passing it to function:
var array = [A]()
func update(object: Any){
}
update(object: array)
from that function update I would like to pass object to another function if it mets another function's conditions:
func process<T: Initiatable>(array: T){
/* ... */
}
So how can I check that object of type Any is an array of concrete class implementing Initiatable protocol? I would like to write something like
func update(object: Any){
if let array = object as Array<T: Initiatable>{
process(array: array)
}
}
But that's doesn't work. Code like:
func update(object: Any){
if let array = object as [Initiatable]{
process(array: array)
}
}
func process(array: [Initiatable]){ }
Compiles fine, but that's not what I want – process function should receive an array of concrete implementation of Initiatable so at some point it could use:
func process<T: Initiatable>(array: [T]){
/* other code */
T.init()
}
So is there any way to do this? Thank you very much in advance!
There's a few parts to this question:
Generating the array of types
Your array declaration is expecting an array of A objects rather than A types. To generate an array with A types, you could pass in a Postfix self expression: (link)
var array = [ A.self ]
This would define array as an array of A.Type, called the Metatype Type (same link).
You could also generate an empty array with this metatype type:
var array:[A.Type] = []
If you wanted an array with both A.self and B.self you could either specify it as [Any]...
var array:[Any] = [A.self,B.self]
...or, make use of the Initiatable protocol you created:
var array:[Initiatable.Type] = [A.self,B.self]
Downcasting an array to an array of types in your update method
You were having trouble downcasting an Any object to an array of types.
Here's my updated update method:
func update(object: Any){
if let array = object as? [Initiatable.Type] { //1
process(array: array)
}
}
You can now perform an optional downcast on the array that is being past into your update method. Downcast it to an array of metadata types of Initiatable: (This is the only line I modified from your method)
Receiving a type as a parameter in the process method
I'm assuming that you just want your process method to receive an array of types, and instantiate a variable based on one of those types. You didn't mention which element in the array, so I've just gone with the first.
func process(array: [Initiatable.Type]){ //1
if let firstType = array.first { //2
let firstObject = firstType.init() //3
}
}
The process method can receive an array of types that adopt the Initiatable protocol.
I'm using optional binding to get at the value of the first element in the array. This should be a type.
Instantiate an object based on the first element in the array.
If your array is nonempty, then you can solve this by grabbing the runtime type of one element from the array:
func update(object: Any){
if let array = object as? [Initiatable]{
process(array: array)
} else {
// Not initiatable
}
}
func process(array: [Initiatable]) { // no need for generics here
guard let first = array.first else {
// Unable to determine array type
// throw or return or whatever
}
// type(of: first) gives A or B
type(of: first).init()
}
If the array is empty, then I don’t know of a way to do this in Swift (as of Swift 3, anyway).
The static type of object is Any, so you need to use the runtime type of object in order to get anywhere.
However, Swift (and most languages with generics) only resolve type parameters like T based on the static types of the values you pass. Therefore your whole approach with process<T: Initiatable>(array: [T]) is a dead end: T can only take on a type the compiler already knew when you called process().
Swift does give you access to runtime types: type(of: object) will return (for example) Array<A>. So close! But:
AFAIK, there is currently no way to extract type parameters from a metatype, i.e. to get A from Array<A>, unless the compiler already knows the type parameter statically.
You can get the string "Array<A>" and extract the string A from it, but AFAIK there is no way to map strings back to types in Swift unless the type is an Objective-C class.
In short, I think you’ve just hit a limit of Swift’s metatype / reflection capabilities.
Unfortunately I can't think of a solution in a given constraints. It seems like you are trying to achieve compile-time polymorphism at runtime.
Using generics implies that compiler understands what class will be called. But using Any implies that at compile time it could be anything and it would be known only at runtime.
The closest solutions that I can propose are:
// Either use generic `update` function
func updateGeneric<T: Initiatable>(array: Array<T>){
process(array: array)
}
// or explicitly convert `Any` to the class,
// so compiler understands what implementation of `process` to call
func updateExplicit(object: Any) {
if let array = object as? [A] {
process(array: array)
}
if let array = object as? [B] {
process(array: array)
}
}
Is this help you,
protocol Initiatable{
init()
}
class A: Initiatable{
required init() {}
}
class B: Initiatable{
required init() {}
}
class Test<T : Initiatable>
{
func update(object: Any){
if let array = object as? [T]{
process(array: array)
}
}
func process(array: [T]){
}
}
I found a workaround to solve this problem:
func classTypeFrom(_ className: String) -> AnyClass!{
if let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String {
let classStringName = "_TtC\(appName.characters.count)\(appName)\(className.characters.count)\(className)"
return NSClassFromString(classStringName)
}
return nil;
}
func update(object: Any){
if let array = object as? [Initiatable]{
let arrayTypeName = "\(type(of: ar))"
let objectTypeName = arrayTypeName.substringFrom(index: 6, length: arrayTypeName.characters.count - 7)
if let arrayType = classTypeFrom(objectTypeName) as? Initiatable.Type{
process(array: array, type: arrayType)
}
}
}
func process(array: [Initiatable], type: Initiatable.Type){
var ar = array
let newObj = type.init()
ar.append(newObj)
}
I have an array that contains a mixture of strings, arrays, and dictionaries. I would like to check if it contains a dictionary. I am trying the following, but receiving errors:
if array.contains(Dictionary<String, AnyObject>) {
}
How would I accomplish this?
You don't have to bother with overloading the contains method if you don't want to – there's already a version of it that can take a custom predicate to check against each element to determine whether it should be considered to be 'in the array'.
It has the advantage of returning early as soon as it finds a match, unlike filter. You could still turn this into an extension method if you really want – but in my opinion it's so concise you shouldn't have to.
if array.contains({$0 is [String:AnyObject]}) {
print("contains dictionary")
} else {
print("doesn't contain dictionary")
}
#Ryan, the type does not have to conform to the Equatable protocol.
This would get the job done:
extension Array {
// This is a concise, yet inefficient implementation
func contains<T>(type type : T.Type) -> Bool {
return !filter({ $0 is T }).isEmpty
}
// Here is a more efficient implementation
func contains<T>(type type : T.Type) -> Bool {
var contains = false
for element in self {
if element is T {
contains = true
break
}
}
return contains
}
}
It can be used as such:
if array.contains(type: Dictionary<String, AnyObject>.self) {
// Run code if the array holds a dictionary of the given type
}
This requires that the array elements conform to the Equatable protocol (which Dictionary doesn't).
You would have to extend contains with something like this:
extension Array {
func contains<T where T : Equatable>(obj: T) -> Bool {
return self.filter({$0 as? T == obj}).count > 0
}
}
Assuming a function that operates on any Array:
func g<T>(array: [T]) -> Void {
// ...
}
...and a function that receives a value of type Any:
func f(x: Any) -> Void {
if x is Array {
// g(?)
}
}
How can I get a properly typed version of x that I can pass to g?
Note that g doesn't rely (explicitly) on the type T. All it needs is the ability to iterate over the elements, which can be treated as values of type Any. Any solution that rewrites g to remove the type parameter is therefore also acceptable.
Edit: to make things harder, I'd like this to work on Linux, where Swift arrays aren't NSArrays until you've called .bridge() on them.
Warning Please note that the below results are based on my experiments on Mirror and work in Swift 2.1.1 (Xcode 7.1.1). It might be that future Swift versions will change the Mirror behaviour.
If finding out dynamically if the value is an array of any type, you could use Swift's reflection support, via the Mirror struct:
func processArray(array: Any) -> Bool {
let mirror = Mirror(reflecting: array)
guard mirror.displayStyle == .Collection else {
return false
}
print("array has \(mirror.children.count) elements")
return true
}
func f(x: Any) {
if processArray(x) {
// i just found an array
}
}
f([1, 2, 3, "4"]) // prints "array has 4 elements"
if you want to keep the generic, you could do this instead:
func g<T>(array: [T]) -> Void {
}
func f(x: Any) -> Void {
if let array = x as? [Any] {
g(array)
}
}