Swift Arrays - The Bugs, the Bad and the Ugly (and a best practice suggestion)

UPDATE 2: I've added a new post about the changes in Beta 3 (I like them)

UPDATE: Apple have indicated that the array behaviour in Swift is going to change - see this post

My previous post suggests an explanation for why the behaviour of Swift Arrays is as it currently (in the very first Swift release) is as it is. In this post I intend to explain the behaviours, show the problems and give examples. Many examples are based on those found in this thread on the Apple Developer forums. Feel free to add comments if I've missed any categories and I'll incorporate them if they form a different case.

The relevant part of the documentation covers most of the behaviour and apart from the issues specifically mentioned in the Bugs section at the bottom these are descriptions of the specification not just the implementation.

Is it Value? Is it Reference? No its Confused!

In Swift the semantics are clear for most things. Objects defined as structs are passed by value, objects defined by class are passed by reference. Dictionaries are passed by reference when they are declared with var. When they are passed declared with let they are  immutable (although if it contains objects defined by class then they are shared and mutable).

For Arrays on the other hand are always mutable and passed by reference. The only differences between using let and var are that if use let you cannot change the length of the array or assign a different array to the reference. This means that if you pass an array to a function or call a method on it (even one that isn't mutating) there can changes to the contents of the array. The kicker is that with a var array when you perform an action that will alter the length it will at that point perform a copy. This leave the behaviour like a mixture of a value and a reference type depending on whether the change that you make affects the length. This means that you can get behavour like this (example based on one by Rhetenor on the Apple Developer Forums):

  var a = [1,2,3]
  var b = a       // Same array a being referred to b
  b[0] = 99       // modify a value (applies to a too).
  b += [4]
  println(a)      | [99,2,3]
println(b) | [99,2,3,4]

This is also the behaviour with passed functions

Mutable Arrays (Declared with let)

This is one of my key problems. let works differently for arrays than for any other type. For arrays it means only that the length of the array should be immutable so you can do things like this:

  let a = [0]
  println(a)             |     [0]
  a[0] = 2
  println(a)             |     [2]

It also means that any array passed to or returned from a function may have had its contents modified so you must pass as a copy (according to the language reference unshare() cannot be called on a constant by which I believe they mean defined with let array).

  extension Array {
      func swapFirstTwoElements()->() {
          let tmp = self[0]
          self[0] = self[1]
          self[1] = tmp
      }
  }
  let z = [1,2]
  z.swapFirstTwoElements()
  println(z)             |    [2,1]

Note that this function is declared without the mutating keyword. If func is declared with the mutating keyword it is an error to call z.swapFirstTwoElements(). This fits with the definition of changing an array contents as not being changing the array but that isn't a definition I'm really happy with.

Now to be clear a fixed length mutable array is a a very useful efficient data type. It can provide safe access to buffers for images and many other types of data being processed. I only object to the way it is declared which is likely to confuse (and to a slightly lesser degree) the lack of a real immutable array type.

Argument Passing

  func changeArr(arr: Int[]) -> Int[] {
      arr[2] = 99
      return arr
  }
  let c = [1,2,3]
  let d = changeArr(c)
  println(c)             |    [1,2,99]
  println(d)             |    [1,2,99]

As you can see "immutable" arrays are passed by reference so unless the caller or the callee copies the array (unshare() is not allowed remember) then operations on the argument will affect the array visible to the caller (and vice versa if the array is store or worse used in another thread). This means that either the caller and/or the callee has to copy the array. Unless these semantics are changed there probably needs to a decision made about responsibility for copying the array where it is appropriate.

Suggestions for Best Practices With Current Swift Array Behaviour

These are my suggestions for the pattern of how to handle arrays. It is meant as a discussion started not as a final word or the only way to operate. I expect some libraries might choose different patterns as their normal case; particularly if they handle bulk data like images when they might want to establish a general pattern of modifying arguments rather than these defensive approaches.

What is small/large/medium is a judgement issue and may depend on platform and the size of the object being stored but I picture small being

For Functions with Array Arguments or Methods on Array

  1. Are you passing the array to another thread or block?
  • Always copy the array before passing it. Generally you might as well do it before making any modifications
  •     2.  Are you changing the length of a large array?
    • You will need to set the method as mutating (if it is an extension of Array) or the argument as inout for a function. This is just the fact of the situation.
    • A copy is the alternative option but it will be expensive. Better to document the changes you will make so the caller can decide to copy the data if they require it but they don't pay the price every time.
  •     3. Are you expecting large amounts of data and need to modify the content without changing the length?
    • This is the case when you want to document the modifications to the argument that you will make.
    • Document it again to make sure the user knows what to expect.
    • Directly modify the arguments.
      • This case will be pretty common for things like audio and image processing.
  •     4. Are you modifying (contents) of a medium sized array?
    • I suggest you mark the method as mutating if it is an Array extension or the keyword inout set on the argument for a function.
      • You won't be able to pass a constant array so there will be an efficiency loss if it only modifies the content but that is probably a worthwhile trade-off for safety.
  •     5. Are you storing the array?
    • Unless the array is very large or you need to share state I recommend copying the array received as an argument. That will protect you from further use in the caller and allow you to modify it freely.
  •     6. Are you dealing with small arrays and need to modify the argument?
    • Copy arrays received and return the new ones rather than modifying in place. This will allow you to perform any modifications and accept constant or variable arrays.

         7. You have small arrays that you don't need to modify/store or pass to another thread or block

    • Use the array passed in directly. This will usually be safe.

    Calling Functions

    If you follow the suggestions above you will have clear documentation of the locations in your code that are not safe just to pass your array into directly. The key exception being whenever you are passing or capturing the variable into a block when (if any edits take place either locally or in the block) you should generally copy (or unshare) and capture or pass that version.

    When dealing with libraries and frameworks outside of your scheme the safest thing to do is to copy (or unshare) your constant (or variable) before you pass it in if you still need to operate on the value

    Bugs

    Sort function doesn't work as described in the language reference

    The Sort Function
    Swift’s standard library provides a function called sort, which sorts an array of values of a known type, based on the output of a sorting closure that you provide. Once it completes the sorting process, the sort function returns a new array of the same type and size as the old one, with its elements in the correct sorted order.”

    Excerpt From: Apple Inc. “The Swift Programming Language.” iBooks. https://itun.es/gb/jEUH0.l

    The above is not true of the current implementation which does not return an new array but the same array as the argument (modified).

      let a = [2,1]
      let b = sort(a)
      println(a)              | [1, 2]
      println(b)              | [1, 2]
      println(a === b)        | true
    

    As you can see the returned array IS the array passed in rather than a new sorted copy.


    Discuss on Hacker News