Swift Digest
SE-0365 | Swift Evolution

Allow implicit self for weak self captures, after self is unwrapped

Proposal
SE-0365
Authors
Cal Stephens
Review Manager
Saleem Abdulrasool
Status
Implemented (Swift 5.8)

01 何が問題だったのか

クロージャ内では、意図しない retain cycle を防ぐ目的で、長らく self. を明示することが求められてきました。SE-0269 ではこの要件が緩和され、キャプチャリストで明示的に self をキャプチャしているクロージャでは self. を省略できるようになりました。

button.tapHandler = { [self] in
    dismiss()
}

しかし SE-0269 では weak self キャプチャの扱いは future direction として残されており、[weak self] の場合は self をアンラップしたあとも self. を明示的に書く必要がありました。

button.tapHandler = { [weak self] in
    guard let self else { return }
    self.dismiss()
}

weak self の場合もキャプチャリストで self を明示しており、すでに retain cycle の意図は十分に表現されています。それにもかかわらず本文で self. を書かせ続けるのは一貫性を欠き、クロージャ本体に余計なノイズを増やしているだけでした。

02 どのように解決されるのか

[weak self] でキャプチャしたクロージャでも、self がアンラップされたあとのスコープでは self. を省略できるようにします。

class ViewController {
    let button: Button

    func setup() {
        button.tapHandler = { [weak self] in
            guard let self else { return }
            dismiss()
        }
    }

    func dismiss() { ... }
}

アンラップとして認められる形

次のいずれの書き方でも、self が non-optional として扱われるスコープで self. が省略可能になります。

button.tapHandler = { [weak self] in
    guard let self else { return }
    dismiss()
}

button.tapHandler = { [weak self] in
    guard let self = self else { return }
    dismiss()
}

button.tapHandler = { [weak self] in
    if let self {
        dismiss()
    }
}

button.tapHandler = { [weak self] in
    while let self {
        dismiss()
    }
}

アンラップ前に self のメンバを暗黙で呼び出そうとするとエラーになり、self?. を明示するよう促す fix-it が提示されます。

button.tapHandler = { [weak self] in
    // error: explicit use of 'self' is required when 'self' is optional,
    // to make control flow explicit
    // fix-it: reference 'self?.' explicitly
    dismiss()
}

また、アンラップ結果がクロージャの self キャプチャ以外の値も指しうる場合(例えば self ?? someOptionalWithSameTypeOfSelf のような形)は、暗黙の self. は許可されません。意味が曖昧になるのを避けるためで、その場合は従来どおり self. を明示する必要があります。

ネストしたクロージャ

ネストしたクロージャは retain cycle の温床になりやすいため、SE-0269 と同じ方針が適用されます。[weak self] クロージャの内側にさらにクロージャを書く場合、内側のクロージャで暗黙の self を使うには、内側でも改めて self を明示的にキャプチャする必要があります。

// 不可:
couldCauseRetainCycle { [weak self] in
    guard let self else { return }
    foo()

    couldCauseRetainCycle {
        // error: call to method 'method' in closure requires
        // explicit use of 'self' to make capture semantics explicit
        bar()
    }
}

// 可:
couldCauseRetainCycle { [weak self] in
    guard let self else { return }
    foo()

    couldCauseRetainCycle { [weak self] in
        guard let self else { return }
        bar()
    }
}

// これも可(内側で self. を明示):
couldCauseRetainCycle { [weak self] in
    guard let self else { return }
    foo()

    couldCauseRetainCycle {
        self.bar()
    }
}

// これも可(内側で [self] キャプチャ):
couldCauseRetainCycle { [weak self] in
    guard let self else { return }
    foo()

    couldCauseRetainCycle { [self] in
        bar()
    }
}

アンラップ前の暗黙 self は導入しない

「アンラップ前の dismiss() を暗黙に self?.dismiss() として扱う」といった拡張は意図的に見送られています。そうした挙動は、self が nil のときに式が実行されないという暗黙の制御フローを生み、読み手にそれが伝わりません。これに関しては従来どおり self?.dismiss() を書く必要があります。