Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SR-1437] defer block in init triggers property observers #44046

Open
lilyball mannequin opened this issue May 7, 2016 · 8 comments
Open

[SR-1437] defer block in init triggers property observers #44046

lilyball mannequin opened this issue May 7, 2016 · 8 comments
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. compiler The Swift compiler in itself

Comments

@lilyball
Copy link
Mannequin

lilyball mannequin commented May 7, 2016

Previous ID SR-1437
Radar None
Original Reporter @lilyball
Type Bug
Environment

Apple Swift version 2.2 (swiftlang-703.0.18.8 clang-703.0.31)
Target: x86_64-apple-macosx10.9

Additional Detail from JIRA
Votes 0
Component/s Compiler
Labels Bug
Assignee None
Priority Medium

md5: 606297041624b4a5d750aefd4d201953

relates to:

  • SR-9000 of deinitializers, property observers, and defer

Issue Description:

Property observers are not normally invoked when the property is set from within init, but if it's set from inside a defer block in init it does fire.

Example:

struct Foo {
    var x: Int {
        didSet {
            print("Foo.x.didSet: \(oldValue) -> \(x)")
        }
    }

    init() {
        x = 1
        defer {
            x = 2
        }
    }
}

_ = Foo()

This prints

Foo.x.didSet: 1 -> 2

Similarly, this can be triggered with defer blocks inside the property observer itself. Example:

struct Foo {
    var flag: Bool = false {
        didSet {
            print("Foo.flag.didSet: \(oldValue) -> \(flag)")
            if flag { // don't loop forever
                defer {
                    flag = false
                }
            }
        }
    }
}

var f = Foo()
f.flag = true

This prints

Foo.flag.didSet: false -> true
Foo.flag.didSet: true -> false

@swift-ci
Copy link
Collaborator

Comment by Paul Eipper (JIRA)

I would expect this to be the case since it's true also for other methods called from init or a closure:

struct Foo {
    var x: Int {
        didSet {
            print("Foo.x.didSet: \(oldValue) -> \(x)")
        }
    }
    init() {
        x = 1
        // closure invokes didSet
        ({ x = 2 })()
    }
}
_ = Foo()

And:

struct Foo {
    var x: Int {
        didSet {
            print("Foo.x.didSet: \(oldValue) -> \(x)")
        }
    }
    init() {
        x = 1
        // mutating method invokes didSet
        setX(2)
    }

    mutating func setX(newX: Int) {
        x = newX
    }
}
_ = Foo()

@lilyball
Copy link
Mannequin Author

lilyball mannequin commented Aug 26, 2016

But a defer block isn't a method. The fact that it seems to be treated basically like a block under the hood is entirely an implementation detail, and the fact that you can observe this detail should be considered a bug.

@swift-ci
Copy link
Collaborator

Comment by Paul Eipper (JIRA)

From the docs:

A defer statement defers execution until the current scope is exited.

It is pretty clear a defer is executed after the current scope is exited, in this case, after init has finished.

@lilyball
Copy link
Mannequin Author

lilyball mannequin commented Aug 26, 2016

That's the complete opposite of how I read it. What that quoted bit means to me is a defer statement is executed at the time the current scope is executed. It does mean it's executed after the scope is exited. I'm not sure how you consider that a valid interpretation at all, because how can a function execute code after the function has returned? Clearly the execution is happening prior to the function actually returning control to its caller.

@lilyball
Copy link
Mannequin Author

lilyball mannequin commented Aug 26, 2016

To back up my interpretation, here's a bit of sample code:

final class DeallocSpy {
    deinit {
        print("deinit")
    }
}

func foo() {
    let foo = DeallocSpy()
    defer {
        print("defer block")
    }
}

foo()

Under your interpretation, the output would have to be

deinit
defer block

But in fact the output is

defer block
deinit

This proves that the defer block is being executed before previously-declared local variables are cleaned up.

@swift-ci
Copy link
Collaborator

Comment by Paul Eipper (JIRA)

Getting back to why observers are not called on initialization, it's because they might depend on other properties that might not be set yet.

Executing from defer makes sure all initialization is done, so the observers don't have the dependency issue anymore and are cleared to trigger.

@lilyball
Copy link
Mannequin Author

lilyball mannequin commented Aug 27, 2016

Code executed in the 2nd step of 2-phase initialization also guarantees that all properties are set, and yet property observers are still not fired. This leads to a very simple rule: Modifying properties (that were declared on the current class) from within an initializer doesn't fire observers. And I see no reason why defer blocks should behave any differently. As far as the user is concerned, a defer block is identical to copying & pasting the contents immediately prior to every return. The actual fact of how it's implemented is just an implementation detail and should not be exposed to the user. The fact that it is exposed is a bug.

@swift-ci
Copy link
Collaborator

Comment by Paul Eipper (JIRA)

PS: regarding the code sample, yes, it will run while the stack is still available to allow access to variables, but it will run after the defining scope has finished all execution.

@swift-ci swift-ci transferred this issue from apple/swift-issues Apr 25, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. compiler The Swift compiler in itself
Projects
None yet
Development

No branches or pull requests

1 participant