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.)

9
  • By the way, here is a spectacular fragment for correctly putting classes in a dictionary in Swift: stackoverflow.com/a/42459639/294884 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. 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. 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. 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 :) 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
Sign up to request clarification or add additional context in comments.

1 Comment

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 !

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.