16

I've noticed a common pattern in Swift is

var x:[String:[Thing]] = [:]

so, when you want to "add an item to one of the arrays", you can not just

x[which].append(t)

you have to

if x.index(forKey: which) == nil {
    x[which] = []
    }
x[which]!.append(s!)

Really, is there a swiftier way to say something like

  x[index?!?!].append??(s?!)
  • While this is a question about style, performance seems to be a critical issue when touching arrays in Swift, due to the copy-wise nature of Swift.

(Please note, obviously you can use an extension for this; it's a question about Swiftiness.)

10
  • By the way, here is a spectacular fragment for correctly putting classes in a dictionary in Swift: stackoverflow.com/a/42459639/294884
    – Fattie
    Commented Feb 27, 2017 at 13:12
  • 1
    I don't think you can achieve that with a single subscript+append call (without custom extension). – Possible approaches, apart from what you said: x[which] = (x[which] ?? []) + [s] or if x[which]?.append(s) == nil { x[which] = [s] }. I assume that this has been asked before, but cannot find it currently.
    – Martin R
    Commented Feb 27, 2017 at 13:32
  • 1
    Both will make a copy of the array – see stackoverflow.com/q/41079687/2976878 for a way to avoid this. SE-0154 will improve the situation, but AFAIK still won't allow you to use any particulary slick syntax in order to achieve what you want.
    – Hamish
    Commented Feb 27, 2017 at 17:34
  • 1
    @JoeBlow x[which]!.append(s!) will indeed make a copy of the array. The problem is that a temporary variable needs to be used in order to hold the unwrapped value for a given key (regardless of whether you force unwrap or optional chain). It's that temporary array which is appended to, and then re-inserted back into the dictionary. Because both the dictionary and temporary array have a view onto the underlying array buffer, a copy will be triggered upon appending.
    – Hamish
    Commented Feb 27, 2017 at 17:55
  • 1
    @JoeBlow You can easily test this behaviour btw with this test setup, although note that for non-optional subscript returns in the stdlib, it's possible for a pointer to the underlying value to be passed back to the caller (through a mutableAddressWithNativeOwner subscript accessor), allowing for a direct mutation. That's what will allow the proposal SE-0154 to efficiently perform direct mutations of a dictionary's values :)
    – Hamish
    Commented Feb 27, 2017 at 18:15

1 Answer 1

29

Swift 4 update:

As of Swift 4, dictionaries have a subscript(_:default:) method, so that

dict[key, default: []].append(newElement)

appends to the already present array, or to an empty array. Example:

var dict: [String: [Int]] = [:]
print(dict["foo"]) // nil

dict["foo", default: []].append(1)
print(dict["foo"]) // Optional([1])

dict["foo", default: []].append(2)
print(dict["foo"]) // Optional([1, 2])

As of Swift 4.1 (currently in beta) this is also fast, compare Hamish's comment here.


Previous answer for Swift <= 3: There is – as far as I know – no way to "create or update" a dictionary value with a single subscript call.

In addition to what you wrote, you can use the nil-coalescing operator

dict[key] = (dict[key] ?? []) + [elem]

or optional chaining (which returns nil if the append operation could not be performed):

if dict[key]?.append(elem) == nil {
     dict[key] = [elem]
}

As mentioned in SE-0154 Provide Custom Collections for Dictionary Keys and Values and also by @Hamish in the comments, both methods make a copy of the array.

With the implementation of SE-0154 you will be able to mutate a dictionary value without making a copy:

if let i = dict.index(forKey: key) {
    dict.values[i].append(elem)
} else {
    dict[key] = [key]
}

At present, the most efficient solution is given by Rob Napier in Dictionary in Swift with Mutable Array as value is performing very slow? How to optimize or construct properly?:

var array = dict.removeValue(forKey: key) ?? []
array.append(elem)
dict[key] = array

A simple benchmark confirms that "Rob's method" is the fastest:

let numKeys = 1000
let numElements = 1000

do {
    var dict: [Int: [Int]] = [:]

    let start = Date()
    for key in 1...numKeys {
        for elem in 1...numElements {
            if dict.index(forKey: key) == nil {
                dict[key] = []
            }
            dict[key]!.append(elem)

        }
    }
    let end = Date()
    print("Your method:", end.timeIntervalSince(start))
}

do {
    var dict: [Int: [Int]] = [:]

    let start = Date()
    for key in 1...numKeys {
        for elem in 1...numElements {
            dict[key] = (dict[key] ?? []) + [elem]
        }
    }
    let end = Date()
    print("Nil coalescing:", end.timeIntervalSince(start))
}


do {
    var dict: [Int: [Int]] = [:]

    let start = Date()
    for key in 1...numKeys {
        for elem in 1...numElements {
            if dict[key]?.append(elem) == nil {
                dict[key] = [elem]
            }
        }
    }
    let end = Date()
    print("Optional chaining", end.timeIntervalSince(start))
}

do {
    var dict: [Int: [Int]] = [:]

    let start = Date()
    for key in 1...numKeys {
        for elem in 1...numElements {
            var array = dict.removeValue(forKey: key) ?? []
            array.append(elem)
            dict[key] = array
        }
    }
    let end = Date()
    print("Remove and add:", end.timeIntervalSince(start))
}

Results (on a 1.2 GHz Intel Core m5 MacBook) for 1000 keys/1000 elements:

Your method:      0.470084965229034
Nil coalescing:   0.460215032100677
Optional chaining 0.397282958030701
Remove and add:   0.160293996334076

And for 1000 keys/10,000 elements:

Your method:      14.6810429692268
Nil coalescing:   15.1537700295448
Optional chaining 14.4717089533806
Remove and add:   1.54668599367142
1
  • the most dramatic results are if you go 10/100,000 ... Your method: 28 Nil coalescing: 32 Optional chaining 28 Remove and add: 0.76 !
    – Fattie
    Commented Feb 28, 2017 at 12:59

Not the answer you're looking for? Browse other questions tagged or ask your own question.