I think it'd be more accurate to say that getting an address in memory isn't the only thing you do when retrieving a method. Say, we create a closure with a capture variable capturedVar;
for capturedVar in 0..<100 {
let closure: () -> () = { [capturedVar] in ... }
}
the closure may behave differently for each value of capturedVar. They're effectively 100 different functions. However, you'd hardly want to create 100 different functions for each possibility of capturedVar (and sometimes the number of possibilities aren't even known). So what the compiler does instead is to create a single non-capturing function, together with storing the captured variable into a single object. Essentially, it compiles closure down to
// Not actual Swift Code
struct Closure1 {
var capturedVar: Int
var functionPointer: FunctionPointer<(capturedVar: Int) -> ()>
}
for capturedVar in 0..<100 {
let closure: Closure1 = ...
}
this way, the compiler can reuse the same functionPointer over and over regardless of the value of capturedVar. This way of augmenting a (function) pointer with an additional runtime information is usually referred to as "fat pointer". It is prevalent in other languages as well. This also explains why a closure retains its captured variable (and may cause reference cycles).
The trick here is to realize that class method is a closure that captures self. You can replace all someMethod in your code with { [car] in car.someConstant } and otherMethod with { [car] in car.someClosure } or { [self] in self.someClosure }, and it'll behave much the same.
class Car {
init() { print("Car is being initialised") }
let someConstant = 1
lazy var someClosure = { [self] in self.someConstant } // someMethod
lazy var otherClosure = { [self] in self.someClosure } // otherMethod
deinit { print("Car is being deinitialised") }
}
print("---------- block 1 ----------")
do {
let car = Car()
print(type(of: car.someClosure))
}
print("---------- block 2 ----------")
do {
let car = Car()
print(type(of: { [car] in car.someClosure })) // otherMethod
}
print("---------- block 3 ----------")
do {
let car = Car()
print(type(of: car.otherClosure))
}
// The following gets printed:
// ---------- block 1 ----------
// Car is being initialised
// () -> Int
// ---------- block 2 ----------
// Car is being initialised
// () -> () -> Int
// Car is being deinitialised
// ---------- block 3 ----------
// Car is being initialised
// () -> () -> Int
So when you're using car.someMethod, you're only creating a closure and invoke it. This is similar to my small example case 1, where you only create closure x. If you draw the reference graph, you'll see no cycle. Now when you use car.someClosure, you are creating the "same closure", but now also assign it to car.someClosure, which is what causes the reference cycle (similar to my small example case 2).
PS
Class and struct methods in Swift are much closer to global curry functions;
Car.someMethod = { car in return { return car.constant } }
car.someMethod() // a.k.a. Car.someMethod(car)()
but class methods also have nuances about dynamic dispatch as well. So neither model would accurately capture all the intricacies about class method. However, for the purpose of analyzing the reference graph, both models work just fine, and I think "method is a closure that capture self" is easier to work with.