6

The following code compiles without warning:

Version 1:

var anything: Any
anything = "woof"

Makes sense... Any is any type, value type or reference type.

However, if we create an optional variable like a Double?, this code throws a warning:

Version 2:

var anything: Any
let aDouble: Double? = 3
anything =  aDouble

But this code does not throw a warning:

Version 3:

enum AnEnum<T>: {
    case first
    case second (T)
}

var anEnum: AnEnum<String> = .first
anything = anEnum



You can rationalize that version 2 throws a warning because Any is not an Optional type, and Double? is an Optional type. Trying to assign an Optional to a non-optional type is sort of a type mismatch.

However, under the covers, an Optional is an enum with a .none case, and with a .some case, where the .some case has an associated value. My version 3 uses an enum, AnEnum, that also has 2 cases, the second of which has an associated value. The AnEnum type is nearly identical to the Swift native Optional type.

Why is assigning an AnEnum value to anything ok, but assigning an Optional value to anything is not ok?

(I started to answer this question: Swift dictionary with mix types (optional and non-optional))

And then realized that I didn't really know the answer.

Community
  • 1
  • 1
Duncan C
  • 128,072
  • 22
  • 173
  • 272

5 Answers5

7

Optional is more than just an enum. It's also an implicit promotion of every type. Any and Optional combine in weird ways that can be surprising. For example, T? is Any. T?? is also Any. Any is Any, Any? is Any. Any?? is also Any.

This creates a number of headaches, and subtle up-casts and down-casts make it very unclear what the optionality of an Any really is. For example.

var anything: Any = ""
let aDouble: Double? = 3
anything = aDouble as Any
let x = anything as? Double

What's x here? Well, aDouble is Optional<Double>, not Double, so as? Double should return nil. But it doesn't because magic. It returns Optional(3). Which is frankly kind of weird (try to get the same thing with a version of MyOptional as an enum), but it would be really bad the other way. Consider:

let data: [String: Any] = ["aDouble": aDouble]
let y = data["aDouble"] as? Double

y is Double? here, not Double?? because of optional coalescing. And thank goodness. But it's bizarre. And if you had generics involved with conformances based on Optional, this could get really confusing (and does; so many questions come up about that....) Now replace Double above with Any and it goes Inception on you really fast.

So the compiler is warning you that mixing Any with Optional is a bad idea and can lead to weird type results, and you definitely should be calling it out anytime you do it. (And maybe you should stop using Any because it is almost never the type you really mean anyway.)

To make it the most concrete example I can think of, consider:

var anything: Any = ""
let one: Int = 1
let maybeTwo: Int? = 2

anything = one
anything as? Int // fine

anything = maybeTwo
anything as? Int? // illegal (see below)
anything as? Int  // fine (but why? that's not what we put in.)

You'd think this would be parallel, but it isn't. You can't just pull out the type you put in when you throw Optionals into the mix.

BTW, interesting addendum, you can't downcast from Any to an Optional type.

let x = anything as? Double? // Illegal

That's really interesting, because anything is a Double?:

(lldb) p anything
(Any) $R4 = {
  payload_data_0 = 0x4008000000000000
  payload_data_1 = 0x0000000000000000
  payload_data_2 = 0x0000000000000000
  instance_type = Double?
}

So how can I tell the difference between "anything is Double? but nil" vs "anything is String"? You'd think you could test that, but I don't think you can, because none of this works consistently. Instead it tries to be convenient. But you can't push it too hard or all the seams will show.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
7

Quite simply, it's because Any is like a Roach Motel for Optionals.

The Roach Motel is a cockroach trap whose motto is, "Roaches check in, but they don't check out." The same is true for Any and Optionals. You can put an Optional into an Any, but you can never get it out again.

To see what I mean, let's first put something else into an Any and get it out again:

let s : String = "howdy"
let any : Any = s
let s2 = any as! String
s2 // "howdy"

Now let's try that with an Optional:

let s : String? = "howdy"
let any : Any = s
let s2 = any as! String? // error

Ooops! You can't cast a nonOptional "down" to an Optional, so the original Optional is lost.

The thing wrapped by the Optional is not lost. You can still unwrap it:

let s : String? = "howdy"
let any : Any = s
let s2 = any as! String
s2 // "howdy"

But now s2 is a String, not an Optional. The Optional is gone for good. You can't get it out. You can't find out that what was put into the Any was an Optional. It's gone.

So that's why putting an Optional into an Any always elicits a warning. The compiler is saying: "You can do this, but do you really understand what you're doing?" And if you do, there are ways to silence the warning (and the error message tells you what they are).

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • 2
    You can get the optional back out, though the compiler certainly doesn't make it easy. You have to convince it that you're casting to a non-optional value by e.g using a generic placeholder: https://gist.github.com/hamishknight/1a10df124d391cbd39a46a695dd7f8ba. The runtime will then happily do The Right Thing™. – Hamish Sep 26 '18 at 16:38
  • 1
    @Hamish Okay that's hilarious. – matt Sep 26 '18 at 16:51
6

It has its own history.

Starting with this feature, in the era of Swift 3.

SE-0116 Import Objective-C id as Swift Any type(So, called id-as-Any in the history of Swift, which is the worst thing in the history, in my opinion.)

(Until then, Any was not such a popular type in Swift programmers.)

It includes a fantastic new feature, universal bridging conversion, which guarantees literally Any values in Swift can be convertible to non-null id when bridging to Objective-C.

But this feature caused a huge number of disastrous tragedies, called _SwiftValue.

In the original conversion first introduced in Swift 3, Optional<Double> was converted to _SwiftValue regardless of whether it was nil or non-nil. It is completely useless in Objective-C world, almost all operations on _SwiftValue caused a crash, which could not have found till runtime, throwing away the goodness of strongly typed language.

So, Swift team needed to introduce a new feature immediately:

SE-0140 Warn when Optional converts to Any, and bridge Optional As Its Payload Or NSNull

Since this feature introduced in Swift 3.0.1, nils are bridged to NSNull and non-nil values are bridged based on the unwrapped values.

The feature also includes Warning when Optional is converted to Any to avoid unintentional misuse of Optional to Any.

The warning you have described started at this time in the Swift history.


The behavior of Optional is backed by many compiler magics (so-called by Swift team members), for example, you cannot make use of simple if-let for other enum types, you cannot use Optional Chaining with your custom enums. Swift compiler treats Optional as a special thing than normal enums.

OOPer
  • 47,149
  • 6
  • 107
  • 142
0

Because you can have an Optional<Any>, by doing Any?. The compiler has no idea if you are expecting your anything type to be nil or Optional.none so it emits a warning to make sure you are aware of the situation.

Daniel T.
  • 32,821
  • 6
  • 50
  • 72
  • But how is `Optional` fundamentally different from `AnEnum`? – Duncan C Sep 26 '18 at 00:28
  • Because `Optional` is identical to `Any?`, but `AnEnum` is not. The real question you should be asking yourself is, how is `Any?` any different than `Any`? :-) – Daniel T. Sep 26 '18 at 01:32
-1

Here is how AnEnum different from Optional.

You can mimic Optional by implementing everything Optional does. However, what you create is another independent "Optional" that is not interoperable with Swift built-in Optional. For example, you can implement ExpressibleByNilLiteral so that you can assign nil to AnEnum which resolves to .first. Just like how Optional resolves nil to Optional.none

enum AnEnum<T> : ExpressibleByNilLiteral {

    case first
    case second (T)

    public init(nilLiteral: ())
    {
        self = .first
    }

}

var anEnum: AnEnum<String> = nil
var anything = anEnum

However, this nil will be DIFFERENT from the nil of Optional.

As suggested by the implementation, Optional does not use the value of nil directly, it just resolves nil literal to Optional.none which it actually stores.

Same as other Enum, Optional.none is just an unknown value (at least you couldn't guarantee it to equal any other values) since no rawValue is specified.

Your nil will be AnEnum.first while Optional's nil will be Optional.none. Optional.none and AnEnum.first are two different unrelated Enum values. Optional won't consider AnEnum.first as Optional.none since they are different and the only way you get Optional.none is by assigning nil literal to Optional.

This explains why you can assign AnEnum, which you think is nearly identical to Swift native Optional, to Any with no problem but assigning an actual Swift native Optional to Any does causes problem. Because your custom "Optional" does not mean anything special other than an Enum to the compiler. The compiler only treats Swift native Optional specially even it is technically just an Enum.

Ricky Mo
  • 6,285
  • 1
  • 14
  • 30