Why does the compiler warns us only about ownership in closures and not inline funcs?

Here's a dummy example:

class Example {
    var prop: Int = 0
    var closureProp: (() -> ())?
    var funcProp: (() -> ())?

    func test() {
        func otherInline() {
            prop = 2 // no error (!?)
        }
        closureProp = {
            prop = 3 // error, self should be used etc
        }
        funcProp = otherInline
    }
}

Both create a retain cycle, however, only for the second one we get an error (only mentioning that we should use self, nothing about the retain cycle) Am I missing something here? I'm wondering what are the ownership rules in both cases :thinking:

1 Like

I donโ€˜t know the answer to the question but I assume that you could technically outsource the nested function into a type method which will capture self implicitly anyway and youโ€˜re still allowed to create a strong ref. cycle.


class Example {
    var prop: Int = 0
    var closureProp: (() -> ())?
    var funcProp: (() -> ())?

    func test() {
        func otherInline() {
            prop = 2 // no error (!?)
        }
        closureProp = {
            prop = 3 // error, self should be used etc
        }
        funcProp = otherMethod
    }

    func otherMethod() {
        prop = 2
    }
}

Nested functions are not checked very well, but don't tell anyone or they might fix it! :shushing_face:
It's actually really helpful sometimes:

func doSomething(_ value: inout Int) {
  let process: ()->Void = { // ERROR: Escaping closure captures 'inout' parameter 'value'
    value += 1
  }
  process()
  process()
}
func doSomething(_ value: inout Int) {
  func process()->Void { // No problems ๐Ÿ˜Œโ˜•๏ธ
    value += 1
  }
  process()
  process()
}

@Avi I'm not sure what you mean by "doesn't hold state" but these are both retain cycles examples: closure retains self, self retains closure.
@all There's actually a substantial difference between a inner func and a normal method, best illustrated by this example:

class Example {
    var innerFunc: () -> ()
    init() {
        func helloInner() {
            print("hello")
        }
        innerFunc = helloInner
    }
    func hello() {
        print("hello")
    }
}

let example = Example()
let hello = Example.hello
//hello() // will not work, missing the first argument, self
hello(example)() // will work

var innerFunc: (() -> ())? = nil

while true {
    let otherExample = Example()
    innerFunc = otherExample.innerFunc 
    break
}

innerFunc?() // will work

As you can see, the hello static func requires the first parameter to be self, which makes sense. However, innerFunc doesn't, and since it works without crashing when called in the last line, it means it retains self (in the last line otherExample is out of scope, although not necessarily dealloc'ed)
Now, this leads me to believe that every time we use inner funcs like these, we'll have a retain cycle, and there's nothing we can do about it. Based on what you guys have already said, this seems to be a compiler bug/shortcoming.

1 Like

This is a good example of how I think the compiler should act in the retain cycle case as well.

class Example {
    var innerFunc: (() -> ())?
    func add(value: inout Int) {
        func f() {
            value += 1
        }
        innerFunc = f // error: escaping closure captures 'inout' parameter 'value'
        f()
    }
}

It doesn't allow the inner function to escape.

I havenโ€™t tried, but I guess withoutActuallyEscaping would work?

Unfortunately not; that function takes entire closures, not inout parameters, and the mere creation of the closure is enough to trigger the error.

We could certainly use that approach as a template for a better solution, though.