11

As far as I know and as mentioned in this thread, if I set a property value in its didSet observer, it should not trigger the observer again. OK, then I wrote a piece of code like this:

class B {
    var i = 0 {
        didSet {
            print("didSet called")
            self.i += 1
        }
    }
}

var y = B()
y.i = 2
print(y.i)

This code prints "didSet called" and 3 as output as expected. But I made a small change to this code as follows:

class B {
    var i = 0 {
        didSet {
            print("didSet called")
            doit(val: self)
        }
    }

    func doit(val: B) {
        val.i += 1
    }
}

var y = B()
y.i = 2
print(y.i)

But now it falls into an infinite loop printing "didSet called". Why if I set a value to the variable inside didSet via passing it through a function argument, does it trigger didSet again? Since the passed object should refer to the same object, I don't know why this happens. I tested and if I set it via a closure in didSet rather than a normal function it goes to an infinite loop again.

Update: It is funny that even this triggers an infinite loop:

class B {
    var i = 0 {
        didSet {
            print("called")            
            doit()
        }
    }

    func doit() {
        self.i += 1
    }
}
Boann
  • 48,794
  • 16
  • 117
  • 146
Afshin
  • 8,839
  • 1
  • 18
  • 53
  • This is actually a bug, I am looking for a link. – Sulthan Jan 03 '19 at 19:34
  • It even still happens even if you put the whole increment function within the `didSet` as well... very strange. – George Jan 03 '19 at 19:54
  • Okey, I am not so sure this is a bug anymore. I know this had been discussed on stackoverflow several times but I don't think anyone reported it. It seems currently only direct accesses inside `didSet` won't trigger observers. The documentation is a big vague. – Sulthan Jan 03 '19 at 20:05
  • https://stackoverflow.com/questions/29363170/why-no-infinite-loop-in-didset, maybe related? – KingHodor Jan 03 '19 at 20:13
  • @Sulthan you are correct about ambiguity of this part of reference. I can find sentences that somehow support each side. – Afshin Jan 03 '19 at 20:34
  • @Sulthan I found the one who added this bug. Will try to link him here. – Afshin Jan 04 '19 at 07:08

1 Answers1

7

After checking with Swift github and asking questions about this problem, I find out that this problem is more complex as it seems. But there is a specific rule about this problem:

didSet observer will not trigger only if access to property within its own didSet observer can be done through direct memory access.

Problem is that it is a little ambiguous when access to property will be direct(unless probably if you are developer of Swift). An important feature that has an effect on my question is this:

Class instance method never access class properties directly.

This quote shows problem with my code, even though I can argue that when an instance member should be able to access property directly whenever you call it in didSet observe. When I have a code like this:

class B {
    var i = 0 {
        didSet {
            print("called")            
            doit()
        }
    }

    func doit() {
        self.i += 1
    }
}

doit() function cannot access i directly which triggers didSet again causing infinite loop.

Now what is the workaround?

You can use inout for passing properties from its own didSet to a instance function without triggering didSet. Something like this:

class B {
    var i = 0 {
        didSet {
            print("called")            
            doit(&i)
        }
    }

    func doit(_ i: inout Int) {
        i += 1
    }
}

And one last thing. Starting Swift 5, conditions for selecting direct memory access for properties within its own didSet will become more restricted. Based on github, only conditions that will use direct memory access is are the following:

 Within a variable's own didSet/willSet specifier, access its storage
 directly if either:
 1) It's a 'plain variable' (i.e a variable that's not a member).
 2) It's an access to the member on the implicit 'self' declaration.
     If it's a member access on some other base, we want to call the setter
     as we might be accessing the member on a *different* instance.

This means codes like following will trigger infinite loop while it does not right now:

class B {
    var i = 0 {
        didSet {
            print("called")            
            var s = self
            s.i += 1
        }
    }
}
Afshin
  • 8,839
  • 1
  • 18
  • 53