This was discussed previously either to always require self
SE-0009 (rejected), or have an optional compiler warning (rejected).
Let's discuss whether and when this should be a recommended practice.
Required vs Optional
There is only one case where you are required to write explicit self
– to refer to a member shadowed by another variable.
Other cases are optional (for self
in closures see below), and are open to discussion whether it should be a recommended practice.
Criteria
To decide whether or not to use explicit self
when it is optional.
The arguments are:
- it reduces errors
- it improves code readability
The errors reported from not using explicit self
boil down to:
- confused shadowing
- unexpected strong reference to
self
1. (Required) Instance member is shadowed
1.a Argument to initializer
class Style1a {
init(bar: Int) {
self.bar = bar
}
let bar: Int
}
1.b Local variable
class Style1b {
var bar: Int = 0
func foo() {
let bar = 42
self.bar = bar
}
}
2. Confused shadowing
2.1 Readability of shadowed code
class Style21a {
func foo() {
if var bar = bar {
bar = bar + 1
}
bar = 42
}
var bar: Int?
}
class Style21b {
func foo() {
if let bar = self.bar {
self.bar = bar + 1
}
self.bar = 42
}
var bar: Int?
}
Pros of explicit self
:
- Easier to reason, you can always tell when it refers to instance member
Cons:
- When reading,
self.
adds noise
Alternatives:
- Review the code in full, follow logic flow
IDE
Review with "highlight all appearances of the selected symbol in the enclosing code scope"IDE
Rely on syntax highlighting difference for instance and non-instance variables
2.2 Adding shadowing
I just copy-pasted a working code block
Starting with this code:
class Style22 {
func foo() {
bar += 1
}
var bar: Int = 0
}
And then at some point introducing shadowing on line 3:
class Style22 {
func foo() {
var bar = 42
bar += 1
}
var bar: Int = 0
}
This happens when pasting code, or just by adding local variable.
Pros of explicit self
:
- There will be no shadowing
- Compiler might warn you of unused local variable, or assigning to read-only variable, but this depends on code pasted
- When reading, you can always tell it refers to instance member
Cons:
- You might never add shadowing,
self
is unneeded - When reading,
self.
adds noise
Alternatives:
- Cover the code with tests to ensure proper behavior
- Review the code in full, follow logic flow
IDE
Review with "highlight all appearances of the selected symbol in the enclosing code scope"IDE
Rely on syntax highlighting difference for instance and non-instance variablesIDE
could warn you that pasted code introduces shadowing
2.3 Removing shadowing
I thought it was a local variable
Starting with this code:
class Style23 {
func foo() {
var bar = 42
bar += 1
}
var bar: Int = 0
}
And then at some point removing line 3:
class Style23 {
func foo() {
bar += 1
}
var bar: Int = 0
}
In a more complex code you might believe you still have a local variable.
Pros of explicit self
:
- If you miss explicit
self
, linter will warn you, forcing a review
Cons:
- You have to update more code with
self
, if this was intentional
Alternatives:
- Cover the code with tests to ensure proper behavior
- Review the code in full, follow logic flow
IDE
Review with "highlight all appearances of the selected symbol in the enclosing code scope"IDE
Rely on syntax highlighting difference for instance and non-instance variablesIDE
could warn you of removed shadowing
3. Unexpected strong reference to self
Note that confused shadowing could lead to unexpected strong reference, but we need to reason about them separately.
Exercise 1
Can you tell the line that introduces a reference cycle?
let s = Style30()
s.some = s.bar
s.some = s.nada
s.some = s.niente
What if I move this code inside a method? (bar
, nada
, and niente
are still members of this class)
class Style30 {
var some: Any?
func foo() {
some = bar
some = nada
some = niente
}
And if I add self
?
class Style30 {
var some: Any?
func foo() {
self.some = self.bar
self.some = self.nada
self.some = self.niente
}
Does self
reveal a reference cycle?
Here is the rest of the class:
class Style30 {
var some: Any?
func bar() {}
let nada: Int = 42
let niente = Inner()
class Inner {}
}
Writing self
does not reveal captured reference. To reason about references, you need to see the types involved, and have an implicit knowledge that method reference captures self
. Nothing conveys this in language syntax – this is an unfortunate design deficiency.
Exercise 2
Can you tell the line that introduces a reference cycle?
class Style30b {
var some: Any?
func foo() {
self.some = {
self.bar()
self.nada
self.niente
self.niente.x
}
}
func bar() {}
let nada: Int = 42
let niente = Inner()
class Inner { let x = 11 }
}
Any reference to instance member inside a closure needs that instance captured. So all the lines 6-9 need self
captured in that closure, which introduces reference cycle when that closure stored back on the instance.
3.1 Method reference
class Style31a {
init() {
some = bar
}
var some: Any?
func bar() {}
}
class Style31b {
init() {
self.some = self.bar
}
var some: Any?
func bar() {}
}
Line 3 introduces reference cycle.
Pros of explicit self
:
- If you miss
self
, linter will warn you, forcing a review
Cons:
- Explicit
self
doesn't reveal capturedself
- When reading,
self.
adds noise - More typing
Alternatives:
- Review the code in full, with the types involved
The compiler might be patched SR-8536, requiring you to write self
in this case. But if you write self
everywhere, how does that help?
3.2 Closure
class Style32 {
init() {
some = {
bar()
}
}
var some: Any?
func bar() {}
}
Call to method 'bar' in closure requires explicit 'self.' to make capture semantics explicit
class Style32 {
init() {
self.some = {
self.bar()
}
}
var some: Any?
func bar() {}
}
Since any instance reference will need self
captured, compiler now requires explicit self
in this case.
Pros of explicit self
:
- If you miss
self
,lintercompiler emits error, forcing a review
Cons:
- When reading,
self.
adds noise - More typing
Pitch: I believe, forcing explicit self
in this case was a confused decision. You should be able to tell instance members by other means, see shadowing above. And to convey explicit capture of self
there is a cleaner syntax – capture list:
class Style32c {
init() {
some = { [self] in
bar()
}
}
var some: Any?
func bar() {}
}
Forcing explicit self
in closures on every member access introduced confusion: if it is required in closures, should I do it everywhere else? Why is it not required everywhere else?
Instead compiler should require explicit self
in capture list, and let linter check the rest.
3.3 Auto-Closure
class Style33a {
let bar: Int = 42
func foo() {
nada(bar)
zilch(bar)
}
func nada(_ cb: @autoclosure () -> Int) {}
func zilch(_ cb: @escaping @autoclosure () -> Int) {}
}
Reference to property 'bar' in closure requires explicit 'self.' to make capture semantics explicit
class Style33b {
let bar: Int = 42
func foo() {
self.nada(self.bar)
self.zilch(self.bar)
}
func nada(_ cb: @autoclosure () -> Int) {}
func zilch(_ cb: @escaping @autoclosure () -> Int) {}
}
Pros of explicit self
:
- If you miss
self
, linter warns you, forcing a review
Cons:
- Explicit
self
doesn't reveal capturedself
- When reading,
self.
adds noise - More typing
Pitch: Again, adding explicit self
doesn't "make capture semantics explicit". It was a confused decision adding it to the compiler. To make it explicit, we need a different syntax, like:
func foo() {
nada(bar)
zilch([self] bar)
}
3.4 Nested function
class Style34a {
var bar: Int = 0
func foo() {
func niente() {
bar = 42
}
nada(niente)
zilch(niente)
}
func nada(_ cb: () -> Void) {}
func zilch(_ cb: @escaping () -> Void) {}
}
No compiler warnings, but niente
captures self
.
class Style34b {
var bar: Int = 0
func foo() {
func niente() {
self.bar = 42
}
self.nada(niente)
self.zilch(niente)
}
func nada(_ cb: () -> Void) {}
func zilch(_ cb: @escaping () -> Void) {}
}
Pros of explicit self
:
- If you miss
self
, linter warns you, forcing a review
Cons:
- Explicit
self
doesn't reveal capturedself
- When reading,
self.
adds noise - More typing
Alternatives:
- Replace nested function with a closure
Pitch: Compiler should warn you of captured self
, and suggest using a closure with capture list. Or have an extended syntax for nested functions with a capture list, like:
func foo() {
func niente() { [self] in
bar = 42
}
}
Section notes
A compiler/linter warning for a missed self
is not to be blindly "fixed" by adding self
, it is an opportunity to review that line. So if you type explicit self
everywhere, there will be less compiler/linter warnings, triggering less reviews. Explicit self
does not mean you have reviewed that line for captured self
.
4. Improve readability
4.1 Explicit self always
class Style41a {
var bar: Int = 2
var nada: Int = 3
var baz: Int {
return bar + nada
}
func foo() {
bar = baz + nada
}
}
class Style41b {
var bar: Int = 2
var nada: Int = 3
var baz: Int {
return self.bar + self.nada
}
func foo() {
self.bar = self.baz + self.nada
}
}
Pros of explicit self
:
- Easier to reason, you can always tell when it refers to instance member
Cons:
- When reading,
self.
adds noise - More typing
4.2 Always self in initializers
Given the common initializer scenario, prefer explicit self on every instance reference in initializer.
class Style42a {
init(bar: Int) {
self.bar = bar
nada = 42
}
let bar: Int
let nada: Int
}
class Style42b {
init(bar: Int) {
self.bar = bar
self.nada = 42
}
let bar: Int
let nada: Int
}
Pros of explicit self
:
- Easier to reason, you can always tell when it refers to instance member
- Uniform style easier to read
Cons:
- When reading,
self.
adds noise - More typing
IDE Support
Highlight selected symbol
This is very useful, but relies on lexical scope, thus beyond simple text editors.
Syntax highlighting
Have different style for instance and non-instance variables. This relies on lexical scope.
Notify of added/removed shadowing
Pitch: IDE could notify you with a confirmation dialog, or a less intrusive inline warning, or a short-lived highlight.
Discussions
- SE-0009 Require self for accessing instance members
- [Rejected] SE-0009 Require self for accessing instance members
- SwiftLint: Enforce explicit self
- Proposal: Re-instate mandatory self for accessing instance properties and functions (David Hart)
- Proposal: Re-instate mandatory self for accessing instance properties and functions
- Proposal: Allow `[strong self]` capture in closures and remove the `self` requirement therein
- [Review] Require self for accessing instance members
- Proposal SE-0009 Reconsideration
- Should explicit `self.` be required when providing method as closure?
- [Pitch] Improving capturing semantics of local functions
- Explicit capture semantics applied too broadly?
- Revisiting requiring explicit `self.` when passing a method as an escaping closure
- Pitch: Weak method storage modifiers (aka weak references)
- Request explicit capture of self instead of defaulting to a strong capture of self
- Implicit retain cycle