Saturday, October 18, 2014

Mutable collections in Swift

I recently came across this question on Twitter:

This is an interesting question. As a developer coming from Objective-C and the Cocoa frameworks, we're used to distinguishing between NSArray and NSMutableArray. It's generally bad practice to expose an NSMutableArray as a public property on an object.

The problem is that another part of the app could modify the contents of the pizzaMenu array without the knowledge of the Pizzeria object. The Pizzeria still points to the same object it always has; from its perspective, nothing has changed. This is bad.

Instead, a better practice is to only expose an immutable NSArray. This way, whenever any change happens, the setter method will be called. The Pizzeria is always aware of what's happening.

In Swift, the Array type is built into the language. Though you can still use NSArray and NSMutableArray, it's often not necessary; the native Array type is plenty capable.

There are two types of objects in Swift: value types and reference types. Reference types are like traditional Objective-C objects—multiple variables can reference the same object. This was the problem in the examples above above: both the Pizzeria object and external code referenced the same object, so we had to expose an NSArray that does not have APIs for changing its contents.

Value types are identified by their value. They also exist in Objective-C, but only for C types (structs and primitives). If two CGRect variables have the same value, it simply means that the two rects contain the same data. A change to one rect will never change the value of another rect stored in a separate variable.

While Objective-C requires that all objects be reference types, Swift objects can be reference or value types. This means that Swift structs can have methods just like classes can. Swift's built-in collections (Array and Dictionary) are value types.

Because they are value types, two variables can never reference the "same" array. Every time an array is assigned to a variable, it gets a new, separate copy1 which cannot be affected by any other part of the code.

This means there is no need for a separate mutable collection type in Swift. Mutating a value type is always safe because changing a struct only affects the values of that specific variable. That what it means to be a value type. Modifying a Swift Array never affects any other variable, anywhere.

Let's see how this affects our pizzeria example.

Note that even though pizzeria.pizzaMenu is a mutable var property, modifying the array we get back from the pizzeria has no effect on the pizzeria's copy of the data. They are separate arrays, separate values, and we can't change their copy unless we go through the setter.

In fact, this holds true even if we avoid assigning the array to an intermediate variable! In Objective-C, you simply cannot modify a value type held by another object. Many iOS developers have discovered this, to their frustration:

But in Swift, not only does this work, but it guarantees that the setter will be called! Whenever you call a mutating2 method on a value type, the end result is assigned back into the receiving variable (or expression). So calling obj.someInts.append(1) causes a new value to be constructed, which is then assigned back into the someInts property!

Because Swift arrays and dictionaries can never be shared, there is no distinction between mutating an existing collection and re-assigning a new collection. The behavior of the code is exactly the same. In either case, the owner's setter method is called whenever the array is modified.

So to answer the original question, there is no syntax to specify a variable that holds an immutable array because there is nothing that such syntax would add. Swift addresses the issues that made NSArray and NSMutableArray necessary in the first place. If you need a shared array, you can still use the Cocoa types. In every other case, Swift's solution is safer, simpler, and more concise.

1: Note that the implementation is lazy, only performing an actual copy when really necessary.
2: Methods on structs and enums in Swift must be labeled mutating if they're going to modify any data. Otherwise, they cannot be called on a value stored in a let variable.