Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swiftier Swift for 'add to array, or create if not there...'

Tags:

swift

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

like image 842
Fattie Avatar asked Feb 27 '17 13:02

Fattie


1 Answers

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
like image 97
Martin R Avatar answered Oct 12 '22 11:10

Martin R