swiftinout

Inout and NSMutableDictionary


If i run the following code in XCode 12 playground (Swift 5.3) I get the same result from two listings:

import Foundation

var dict = NSMutableDictionary()

dict["x"] = 42

func stuff(_ d: inout NSMutableDictionary) {
    d["x"] = 75
}

stuff(&dict)

dump(dict) // x is 75

the other:

import Foundation

var dict = NSMutableDictionary()

dict["x"] = 42

func stuff(_ d: NSMutableDictionary) {
    d["x"] = 75
}

stuff(dict)

dump(dict) // x is 75 still

As per the documentation here, the second listing should give me an error: https://docs.swift.org/swift-book/LanguageGuide/Functions.html

But it works anyway.

Is this because the enforcement of these in-out rules is constrained to Swift only types, and Cocoa types are exempt?


Solution

  • This works not because Cocoa types are exempt, but because NSMutableDictionary is a class (as opposed to a struct), and the inout does not refer to what you might be thinking.

    Unfortunately, the documentation you link to (and the more in-depth documentation on inout parameters it links to) doesn't make it clear what "value" really means:

    An in-out parameter has a value that is passed in to the function, is modified by the function, and is passed back out of the function to replace the original value

    The following statement hints at it a little, but could be clearer:

    You can only pass a variable as the argument for an in-out parameter. You cannot pass a constant or a literal value as the argument, because constants and literals cannot be modified.

    The "value" the documentation describes is the variable being passed as inout. For value types (structs), this is meaningful because every variable holding a value of those types effectively holds a copy of that value.

    var a = MyGreatStruct(...)
    var b = a
    // a and b are not directly linked in any way
    

    Passing a struct to a function normally copies the value into a new local variable (new variable = copy), whereas you can imagine inout giving you direct access to the original variable (no new variable).

    What's not described is that the effect is identical for classes, which behave differently.

    let a = MyGreatClass(...)
    let b = a
    // modifying `a` will modify `b` too since both point to the same instance
    

    Passing a class to a function also copies the variable into a new local variable, but the copy isn't meaningful — both variables hold the same thing: a reference to the object itself in memory. Copying in that sense doesn't do anything special, and you can modify the object from inside of the function the same way you could from outside. inout for classes behaves the same way as for structs: it passes the original variable in by reference. This has no bearing on the majority of the operations you'd want to perform on the object anyway (though it does allow you to make the variable point to a different object from within the function):

    var a = MyGreatClass("Foo")
    
    // func foo(_ value: MyGreatClass) {
    //     value = MyGreatClass("Bar") // <- not allowed since `value` isn't mutable
    // }
    
    func foo(_ value: inout MyGreatClass) {
        value = MyGreatClass("Bar")
    }
    
    print(ObjectIdentifier(a)) // <some pointer>
    foo(&a)
    print(ObjectIdentifier(a)) // <some other pointer>