0

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?

zaitsman
  • 8,984
  • 6
  • 47
  • 79

1 Answers1

1

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>
Itai Ferber
  • 28,308
  • 5
  • 77
  • 83
  • So why then Array of NSMutableDictionary is a value type? – zaitsman Oct 26 '20 at 22:07
  • @zaitsman `Array` is a value type (no matter the `T`) because it is a `struct` that maintains value semantics. It doesn't matter what it holds, you will get the same effect: `let a = Array(...)` will not be mutable, for instance. However, the things _inside_ of the array might be. For instance, `Array` is a value type which holds reference types. I cannot reassign `a` to another array, but I can write `a[0]["my_key"] = `. That doesn't mutate the array, but changes the object `a[0]` _refers_ to. – Itai Ferber Oct 26 '20 at 22:10
  • @zaitsman You can imagine `a[0]["my_key"] = ` like this: `let dict: NSMutableDictionary = a[0]; dict["my_key"] = `. (Notice how it doesn't matter that `dict` was originally inside of `a`? Because `a[0]` is a pointer, not the whole dictionary value.) – Itai Ferber Oct 26 '20 at 22:11
  • For what it's worth, it's relatively rare to need to mix value type containers and reference-type containers in this way. For most use-cases, `Array` and `Dictionary` are the way to go in Swift. There are definitely use-cases for `NSArray/NSMutableArray` and `NSDictionary/NSMutableDictionary`, but they're not the norm. – Itai Ferber Oct 26 '20 at 22:13
  • Unfortunately my app is built on this third party sdk which spits out those containers from Cocoaland and thus the whole app is ‘not the norm’ – zaitsman Oct 26 '20 at 22:16
  • @zaitsman If the objects are of a consistent type, you should be able to `as`-cast to `Dictionary` and bridge them (https://developer.apple.com/documentation/swift/dictionary#2846239) – Itai Ferber Oct 26 '20 at 22:18
  • they are not. It's a dictionary that may contain arrays, dictionaries, strings, numbers, dates.. They get it out of `NSJSONSerialization` so while i can cast it, the cast is as `[String:Any]` and then to give it back to SDK for storage/API calls i have to cast back to `NSDictionary` which sort of defeats the purpose. In some cases where the work is constrained i use `DictionaryCoding` package and `Codable` models but that is not possible in this specific block I am working on as I'd have to create some 80-100 different codables and then have a massive switch to decide what to decode to – zaitsman Oct 26 '20 at 22:20
  • Got it. That makes sense. Depending on the specifics of exactly what you need to do, it _may_ be worth exploring writing some of the more dynamic aspects of the data processing in Objective-C and exposing some of the more constrained, type-safe areas into Swift, but that's between you and the problem domain :) – Itai Ferber Oct 26 '20 at 22:35