Assign value to optional dictionary in Swift
Asked Answered
F

4

10

I'm finding some surprising behavior with optional dictionaries in Swift.

var foo:Dictionary<String, String>?

if (foo == nil) {
    foo = ["bar": "baz"]
}
else {
    // Following line errors with "'Dictionary<String, String>?' does
    // not have a member named 'subscript'"
    foo["qux"] = "quux"
}

I've played with this a lot, trying to figure out what I might be missing, but nothing seems to make this code work as expected short of making the dictionary not optional. What am I missing?

The closest I can get is the following, but of course it's ridiculous.

var foo:Dictionary<String, String>?

if (foo == nil) {
    foo = ["bar": "baz"]
}
else if var foofoo = foo {
    foofoo["qux"] = "quux"
    foo = foofoo
}
Farra answered 7/6, 2014 at 10:35 Comment(0)
W
31

The lightbulb moment is when you realize that an Optional dictionary is not a Dictionary. An Optional anything is not that thing! It is an Optional!! And that's all it is. Optional is itself a type. An Optional is just an enum, wrapping the possible cases nil and some value. The wrapped value is a completely different object, stored inside.

So an Optional anything does not act like the type of that thing. It is not that thing! It is just an Optional. The only way to get at the thing is to unwrap it.

The same is true of an implicitly unwrapped Optional; the difference is just that the implicitly unwrapped Optional is willing to produce (expose) the wrapped value "automatically". But it is still, in fact, wrapped. And, as Bryan Chen has observed, it is wrapped immutably; the Optional is just holding it for you - it is not giving you a place to play with it.

Witkin answered 7/6, 2014 at 11:47 Comment(8)
Experimenting with what you're saying, I'm finding that var a:Int?; if a != nil { a++ } is also an error. In light of this, optionals seem a lot less useful than I originally thought… that's frustrating.Farra
@GarrettAlbright Useful is as useful does. You're not following the idiom so you're tripping yourself up. I'm not sure what you're trying to do, but perhaps this is what you mean: var a:Int?; if var b = a { a = ++b }Witkin
It strikes me as silly that I have to assign another variable and pass values back and forth for that, though. (My code examples are not practical; just examples as I'm trying to discover what's really happening here.) What I was expecting is that optional variables are variables that can be nil, or can behave as standard variables when they are not nil, but that doesn't seem to be the case.Farra
@GarrettAlbright That's right, they are not. But what was silly was keeping the value in an Optional wrapper if you don't have to. As soon as you know it isn't nil and won't be, you should take it out of its wrapper and forget the Optional. My point is merely, your example was artificial, but given its artificiality, there's a correct idiom for doing this.Witkin
So you're saying creating the new variable and swapping it around is the correct idiom, then?Farra
@GarrettAlbright I'm saying have you written any apps using Swift yet? The matter doesn't arise. You'd never keep an incrementable Int inside an Optional. But if that's the kind of nonsensical thing you want to do, yes, that's how to increment such a beast.Witkin
@GarrettAlbright That is, in fact, the same point I'm making with my question / comment on your original question. The situation seems utterly artificial and improbable. Sure, a Dictionary might arrive from across the Objective-C bridge wrapped in an Optional, but you'd never keep it in an Optional var.Witkin
Okay, here's my use case in a nutshell. I have a bunch of data which I want to load into an array from a plist and keep in memory. That array will be nil when the app starts or after the low memory warning, but when I need to pluck some data out of it, I will notice the array is nil and fill it with data from the plist before I try to use it. For now I'm just loading it into a second array and swapping it in, and it should work fine, but it initially stuck me as strange that that was necessary.Farra
S
7

you can use this code

if var foofoo = foo {
    foofoo["qux"] = "quux"
    foo = foofoo
} else {
    foo = ["bar": "baz"]    
}

with this code

var foo:Dictionary<String, String>? = Dictionary()
foo[""]=""

error: 'Dictionary<String, String>?' does not have a member named 'subscript'
foo[""]=""
^

the error message makes sense to me that Dictionary<String, String>? does not implement subscript method, so you need to unwrap it before able to use subscript.

one way to call method on optional is use ! i.e. foo![""], but...

var foo:Dictionary<String, String>? = Dictionary()
foo![""]=""

error: could not find member 'subscript'
foo![""]=""
~~~~~~~~^~~

whereas

var foo:Dictionary<String, String>? = Dictionary()
foo![""]

works


it is interesting these code failed to compile

var foo:Dictionary<String, String>! = Dictionary() // Implicitly unwrapped optional
foo[""]=""

error: could not find an overload for 'subscript' that accepts the supplied arguments
foo[""]=""
~~~~~~~^~~

var foo:Dictionary<String, String>! = Dictionary() // Implicitly unwrapped optional
foo.updateValue("", forKey: "")

immutable value of type 'Dictionary<String, String>' only has mutating members named 'updateValue'
foo.updateValue("", forKey: "")
^   ~~~~~~~~~~~

the last error message is most interesting, it is saying the Dictionary is immutable, so updateValue(forKey:) (mutating method) can't be called on it

so what happened is probably that the Optional<> store the Dictionary as immutable object (with let). So even Optional<> it is mutable, you can't modify the underlying Dictionary object directly (without reassign the Optional object)


and this code works

class MyDict
{
    var dict:Dictionary<String, String> = [:]

    subscript(s: String) -> String? {
        get {
            return dict[s]
        }
        set {
            dict[s] = newValue
        }
    }
}

var foo:MyDict? = MyDict()
foo!["a"] = "b" // this is how to call subscript of optional object

and this lead me to another question, why Array and Dictionary are value type (struct)? opposite to NSArray and NSDictionary which are reference type (class)

Supersensual answered 7/6, 2014 at 10:58 Comment(4)
I understand perfectly all that ? and ! but you are right with your last statement. I am amazed that Array and Dictionary are structs and not objects. One of the interesting things is that you can't create a truly immutable array, only an array with fixed length and that's quite different.Inheritance
Partly it's because they don't need to be classes - in Swift, a struct is nearly a class, and in fact in some ways classes are more restrictive (pass by reference, no class variables, etc.)Witkin
In fact, NONE of the types defined in the base language is a class; everything is an enum or a structWitkin
@Witkin I guess pass by value is a good thing (so you can't accidentally change something and cause unexpected behavior) but on other hand, array is implemented in such way it is struct but actually passed by reference (like C). Also an object should be passed by value or reference is more about semantic thing (we want different behavior under difference context)Supersensual
C
0

This is because your Dictionary is optional. If it's nil, you won't add an entry to it.

You can do this way:

var dict: [String : String]?
if let dict = dict {
  dict["key"] = "value" // add a value to an existing dictionary
} else {
  dict = ["key" : "value"] // create a dictionary with this value in it
}

Or, if you are given an optional dictionary, for example HTTPHeaders - which in AlamoFire is a [String : String] dictionary - and you want to either add a value if it's non-nil, or create it with this value if it's nil, you could do like so:

let headers: HTTPHeaders? // this is an input parameter in a function for example

var customHeaders: HTTPHeaders = headers ?? [:] // nil coalescing
customHeaders["key"] = "value"
Collin answered 8/6, 2017 at 15:49 Comment(0)
R
0

I tried this for Swift 3.1 and it worked:

if (myDict?[key] = value) == nil {
    myDict = [key: value]
}
Rumpf answered 12/7, 2017 at 11:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.