Extension init(): what's the difference between calling `self.init(...)` vs. `self = .init(...)`

struct Foo {
    let n: Int
}

extension Foo {
    init(aaa: Int) {
        self.init(n: aaa)
    }
    init(bbb: Bool) {
        self.init(n: bbb ? 1 : 0)
    }
    init(ccc: Bool) {
        // cannot do this
//        bbb ? self.init(n: 1) : self.init(n: 0)
        // has to assign to self
        self = ccc ? .init(n: 1) : .init(n: 0)
     }
}

So sometimes you can just call self.init(...), other times self = .init(...).

why not just:

self.init(n: ccc ? 1 : 0) 

if it's a more complex case you can still do just this:

    if ccc {
        self.init(n: 1)
    } else {
        self.init(n: 0)
    }

and write it in a single line if that's a concern.

assigning to self is required in some contexts (e.g. to assign it a result of a static method call), but the above is just not one of those cases.

I want to know if there is any difference between the two, whether one is preferred over the other?

.init is called an implicit member expression, which you can read about here. There's no difference when they both work, but an implicit member expression requires a contextual type so it knows where to look up the method/property (provided here by the self = assignment).

in this case self.init(n: xxx) is obviously better as with self = .init(xxx) form the two initialisers will be called instead of just one (*). general rule - use self.init(xxx) unless impossible.

(*) these two: init(n:) and init(ccc:)

hmm. now that i wrote that i realised that these two inits will be called with self.init(...) version as well... don't know why, but it "feels" that the self.init(...) version is more correct...

probably this:
when you call self = .init(n: 1) the "init(n:)" will be called on a different variable (compared to the variable that is currently being initialled in init(ccc:)), which memory contents is later copied to the current variable. with the "self.init(n:)" version the second initialiser is called on the same variable thus there is no need of extra copy. that's why it feels slightly better (and is indeed slightly faster).

self = .init() is just a combination of the two distinct & general features. .init(...) alone is useful when the type of the initializer is known, and/or is a hassle to type, e.g., in a function argument like foo(.init()). On the other side, self = is useful when you already have an object that you want to replace self with.

self.init(), OTOH, will initialize self with that particular initializer. It is much more lenient for structs & protocols, but for classes, it requires that you follow the initialization rules, including two-phase initialization.

Note though, because self = .init() is an assignment, this is permitted on any mutating functions but is not allowed when self is known to be a class instance*, while self.init() is only allowed in another initializer.

Personally, I tend to prefer self.init() when it's inside an initializer. Mostly because it's aligned with how you'd write a class initializer. I of course need to use self = outside of an initializer, though I only do that when I want to reset the instance to a default state.

Performance-wise, I'd be surprised if the compiler wouldn't generate similar code (worthy of bug report even). It may affect the type-checking performance, but at some point, it's just another i++ vs ++i. You'd better spend energy on something else than shaving a few ms off the compile time.

* There are some subtleties when self is a protocol with an underlying class.

1 Like

yet another minor reason to prefer self.init(...) form by default:

struct Foo {
    let n: Int
    let one = 1
}

extension Foo {
    init(ccc: Bool) {
        self = ccc ? .init(n: 1) : .init(n: 0)
        // Error: Immutable value 'self.one' may only be initialized once
     }
}
2 Likes

There seems to be only a very slight difference between self.init(...) and self = .init(...) practically.

Given the following code:

struct Foo {
  let n: Int
}
extension Foo {
  init(a: Int) { self.init(n: a) }
  init(b: Int) { self = .init(n: b) }
}

You can get SIL like below (extract):

// Foo.init(a:)
sil hidden @$s4main3FooV1aACSi_tcfC : $@convention(method) (Int, @thin Foo.Type) -> Foo {
// %0 "a"                                         // users: %5, %3
// %1 "$metatype"                                 // user: %5
bb0(%0 : $Int, %1 : $@thin Foo.Type):
  %2 = alloc_stack $Foo, var, name "self"         // users: %6, %7
  debug_value %0 : $Int, let, name "a", argno 1   // id: %3
  // function_ref Foo.init(n:)
  %4 = function_ref @$s4main3FooV1nACSi_tcfC : $@convention(method) (Int, @thin Foo.Type) -> Foo // user: %5
  %5 = apply %4(%0, %1) : $@convention(method) (Int, @thin Foo.Type) -> Foo // users: %6, %8
  store %5 to %2 : $*Foo                          // id: %6
  dealloc_stack %2 : $*Foo                        // id: %7
  return %5 : $Foo                                // id: %8
} // end sil function '$s4main3FooV1aACSi_tcfC'

// Foo.init(b:)
sil hidden @$s4main3FooV1bACSi_tcfC : $@convention(method) (Int, @thin Foo.Type) -> Foo {
// %0 "b"                                         // users: %5, %3
// %1 "$metatype"                                 // user: %5
bb0(%0 : $Int, %1 : $@thin Foo.Type):
  %2 = alloc_stack $Foo, var, name "self"         // users: %6, %9
  debug_value %0 : $Int, let, name "b", argno 1   // id: %3
  // function_ref Foo.init(n:)
  %4 = function_ref @$s4main3FooV1nACSi_tcfC : $@convention(method) (Int, @thin Foo.Type) -> Foo // user: %5
  %5 = apply %4(%0, %1) : $@convention(method) (Int, @thin Foo.Type) -> Foo // users: %7, %10
  %6 = begin_access [modify] [static] %2 : $*Foo  // users: %7, %8
  store %5 to %6 : $*Foo                          // id: %7
  end_access %6 : $*Foo                           // id: %8
  dealloc_stack %2 : $*Foo                        // id: %9
  return %5 : $Foo                                // id: %10
} // end sil function '$s4main3FooV1bACSi_tcfC'

The difference is whether to access self directly or indirectly.
I'm not familiar with SIL, though. :sweat_smile:

Compile with -O the two produce exactly the same code:

        mov     rax, rdi
        ret
1 Like