Inout behavious

Hello, could someone explain to me how inout works in the following scenario? More specific why didSet for bar property is called?

class Foo {
    class Bar {
    }

    var bar: Bar {
        didSet {
            print("didSet bar")
        }
    }

    init() {
        self.bar = Bar()
    }

    func test() {
        // regularBar(bar: bar) // as expected will not trigger didSet

        inoutBar(bar: &bar) // triggers didSet. Why so?
    }

    func regularBar(bar: Bar) {
        print("regularBar")
    }

    func inoutBar(bar: inout Bar) {
        print("inoutBar")
    }
}

let foo = Foo()
foo.test()
1 Like

inout is short for copy-in copy-out. It's as if the variable is read once (copied out), and then written back after.

See Declarations — The Swift Programming Language (Swift 5.7)

4 Likes

In-Out Parameters

In-out parameters are passed as follows:

  1. When the function is called, the value of the argument is copied.
  2. In the body of the function, the copy is modified.
  3. When the function returns, the copy’s value is assigned to the original argument.

Even without any modification of the passed value, the original value is still being written back into. I find this quite bewildering :confused:

In general, the compiler doesn't know, when compiling f(&x), whether f modifies x, because f might be defined in some other module. So at least in those cases it must assume f modifies x. For consistency, it always behaves like f modifies x.

3 Likes

I can see why the quoted documentation from TSPL might be confusing: it reads as though step 2 ("the copy is modified") is obligatory or somehow meaningful to the semantics of inout before step 3 occurs ("the copy's value is assigned to the original argument").

This is not true (cc @Alex_Martini)—it would perhaps be more accurate to say that the copy is modifiable within the body of the function, but there is no step 2 required as it is currently stated in TSPL.

Whether the copy is modified in the body of the function is immaterial to the semantics of inout, and whether the compiler "knows" what's in the body of the function is also immaterial. The function can be an always-inlinable function with an empty body that has guaranteed semantics that it does absolutely nothing. Nevertheless, the semantics of inout are such that a copy is notionally assigned back to the original argument when the function returns. Notionally because no actual copying needs to happen at all. However, with respect to all observable behaviors (including didSet), it is as-if such a copy occurred.

6 Likes

Let me modify this statement to clarify the semantics of inout even stronger: the copy of the inout argument actually doesn’t need to be modifiable at all.

You can explicitly choose the internal argument name of _ in the method signature of inoutBar, guaranteeing at the point of declaration that the copy of the argument’s value cannot be accessed in any way from within the function body regardless of what code is in it, and the didSet will still be triggered:

3 Likes

That's kind of funny, I never thought about using _ within a function's parameter declaration (but of course it makes absolutely sense that it's possible, grammar-wise). :smiley:

If we're talking about this and considering beginners might read this here, however, I feel the need to add that "this probably is a bad idea™". The example with didSet here basically shows how to use a side-effect to do something and that often indicates a less-than-optimal design of your code in a more general sense.

After all, if you don't need a parameter inside your function you should consider why you won't leave it out entirely. (That being said, one argument surely might be "I am adopting a protocol and the definition demands this", which is encountered in a ton of delegate patterns).

Same for the inout part of it. There sure are scenarios in which that can happen and unexpectedly getting didSet invoked is not easy to avoid, but if you're in full control of how you define your functions and/or properties it's probably unwise to do something like this intentionally. You're more likely just "hiding" something from a future reader (who might very well be yourself).

1 Like

Just to add a little bit of context here, I intentionally created a simplified version as an example. But in our real project it was a @Published property that was triggered for ref types which I didn't understand. After some digging I found out that it was used to call a func with an inout param. So I've decided to ask here :smiley:

this is my favorite swift footgun!

  1> func foo() -> [Int: [Void]]
  2. { 
  3.     var dictionary:[Int: [Void]] = [:] 
  4.     for element:Void in [] 
  5.     { 
  6.         dictionary[0, default: []].append(element) 
  7.     } 
  8.     return dictionary  
  9. } 
 10> foo()
$R0: [Int : [Void]] = 0 key/value pairs
 11> func bar() -> [Int: [Void]] 
 12. { 
 13.     var dictionary:[Int: [Void]] = [:] 
 14.     dictionary[0, default: []].append(contentsOf: []) 
 15.     return dictionary
 16. } 
 17> bar()
$R1: [Int : [Void]] = 1 key/value pair {
  [0] = {
    key = 0
    value = 0 values
  }
}