Odd behavior related to [T?] as? [T]

Note the warning in this program:

let x: Int? = 1
let y: Int? = 2

let a: [Int]? = [x, y] as? [Int] // ⚠️ Conditional downcast from 'Int?' to 'Int' does nothing

print(a ?? "nil") // [1, 2]

The warning says I'm downcasting from Int? to Int although what happens is that a [Int?] is casted into a [Int]?.

It also says that this does nothing and provides the fixit "Remove as ? [Int]", but applying the fixit will result in a compiler error (so clearly it did something rather than nothing):

let x: Int? = 1
let y: Int? = 2

let a: [Int]? = [x, y] // 🛑 Cannot convert value of type 'Int?' to expected element type 'Array<Int>.ArrayLiteralElement' (aka 'Int')

print(a ?? "nil")

Also, the following program is equivalent to the first AFAICT, yet there's no warning:

let x: Int? = 1
let y: Int? = 2

let xy = [x, y]

let a: [Int]? = xy as? [Int]

print(a ?? "nil") // [1, 2]

So what happens when any of the elements is nil?

let x: Int? = 1
let y: Int? = nil

let xy = [x, y]

let a: [Int]? = xy as? [Int]

print(a ?? "nil") // nil

Ie, the array of optionals is compact mapped, but only if all the elements are not nil, otherwise the resulting optional array will be nil.

What is intended behavior and what is compiler bugs in this?

3 Likes

I think the warning is the same one you get here:

let x: Int? = 1
let y: Int? = 2
let z = y as? Int // ⚠️ Conditional downcast from 'Int?' to 'Int' does nothing

In the version of your example that produces no warning:

let a: [Int]? = xy as? [Int]

there are two "layers" of casting: one layer of per-element casts (similar to a compactMap as you've noted) to try to convert the values from Int? to Int, and a second cast to try to convert the result to [Int], depending on the result of the first layer.

In the original version:

let a: [Int]? = [x, y] as? [Int]

I think there are three layers of casting: an initial attempt to cast the expression result type of [x, y] to [Int] by type inference on the array elements — which doesn't succeed in getting Ints so the result is [Int?], but produces the warning — then the other two layers. The warning occurs in that extra "layer", so it only appears in this version.

The result is [Int]?, not [Int?].

I meant the result of the [x, y] expression in isolation. That's [Int?], just like the type of your xy declaration.

Ok. But the warning still seems invalid (applying its suggested fix won’t fix it, but produce an error), right?

And the quite complex functionality that the as? has here, ie compact mapping iff all elements are not nil, otherwise returning nil, is that intentional and if so documented somewhere?

2 Likes

Is [Int?] a subtype of [Int] btw?

Yes, I think the warning is irrelevant, so it's a kind of bug.

This is intentional AFAIK. It's always been the way to cast an array bridged from NSArray where the array elements are of a known type — just not known to the compiler.

You can also use it, for example, to check whether an array in a JSON deserialization consists only of String elements.

For other types than Array, the behavior is different for this use of as?:

struct Brray<T> : ExpressibleByArrayLiteral {
    let a, b: T
    init(arrayLiteral elements: T...) {
        precondition(elements.count == 2, "Only two elements are supported so far :)")
        self.a = elements[0]
        self.b = elements[1]
    }
}

func af<T>(_ x: Array<T?>) -> Array<T>? { x as? Array<T> }
func bf<T>(_ x: Brray<T?>) -> Brray<T>? { x as? Brray<T> }

let array: Array<Int?> = [1, 2]
let brray: Brray<Int?> = [1, 2]

let ar: Array<Int>? = af(array)
let br: Brray<Int>? = bf(brray)

print("ar:", ar ?? "nil") // [1, 2]
print("br:", br ?? "nil") // nil

Is this because of a compiler bug or by design?
If it's by design, how to explain it?

Maybe it's because: The type checker hardcodes conversions from Array<T> to Array<U> if there is a conversion from T to U.

3 Likes

Ah, that must be (at least part of) the explanation. It's unfortunate that it results in this kind of confusing behavior. I guess the issue with the nonsensical warning might somehow be caused by this as well.

So the conclusion might be that everything except the warning is working as intended?

Here's a way to get rid of the warning for now:

func f(_ x: Int?, _ y: Int?) -> [Int]? {
    return [x, y] as? [Int] // ⚠️ Conditional downcast from 'Int?' to 'Int' does nothing
}

func g(_ x: Int?, _ y: Int?) -> [Int]? {
    return ([x, y] as [Int?]) as? [Int] // ✅ No warning when adding this seemingly redundant cast.
}
3 Likes

No warning in a Xcode Version 13.3 beta (13E5086k) Playground. What's your Xcode version? If not the same as mine, maybe Swift 5.6 is why?

[Int?] and [Int] are quite different types.. (I believe internally there'll be 9 (or 10?) bytes per element for the former and only 8 for the latter (on 64 bit platforms).

The cast above is not redundant, it converts from [Int?] to [Int], converting every element of array from Int? to Int (if it can), doing this in O(n) time, and if it can't (i.e. when one of the elements is nil) it returns nil. That's what as? is doing. I believe you'll get the same result using:

([x, y] as [Int?]) as! [Int]?

in other way as? [Int] and as! [Int]? should be equivalent.

(the difference is that if for array of, say, strings, as? [Int] would produce nil and as! [Int]? would crash.)

1 Like

I meant the first cast, as [Int?], is redundant, it’s the only difference between f and g.

(Both f and g have the as? [Int].)

I’m using the default toolchain in Xcode 13.2.1, haven’t tried it in a playground.

I’m using 13.3 beta. Try that and see?

I’m away from computer. But if anyone else knows that this warning has been removed after Swift 5.5.2, that would be interesting to know.

Could you try it out in a fresh command line project on 13.3 beta as well? (I don’t trust playground🙂)

% swiftc --version
swift-driver version: 1.44.2 Apple Swift version 5.6 (swiftlang-5.6.0.320.8 clang-1316.0.18.8)
Target: x86_64-apple-macosx12.0
% swiftc jen.swift
% cat jen.swift
import Foundation

func f(_ x: Int?, _ y: Int?) -> [Int]? {
    [x, y] as? [Int] // ⚠️ Conditional downcast from 'Int?' to 'Int' does nothing
}

so with Swift 5.5, warning. with Swift 5.6, no warning

1 Like