Task.initのクロージャーに[weak self]はいらない。Task.detachedとTaskGroup.addTaskも同様

2022年7月29日、インプレスR&D社よりSwift Concurrencyの解説本をリリースしました。 こちらの本は一冊でSwift Concurrencyの機能をほぼ網羅したConcurrency機能の解説本です。 日本語でSwift Concurrencyを学べる解説本はまだ少ないので、Swift 5.5からの非同期処理をうまく書きたい方には必見の本となっています。 さて、せっかくリリースしたばかりなのですが、一部のサンプルコードがあまり良いコードではありませんでした。 すでにサンプルコードのリポジトリは修正済みですが、どのような修正があったのかをこの記事で説明したいと思います。

Task.initのクロージャーに[weak self]はいらない。Task.detachedとTaskGroup.addTaskも同様
Photo by Algi / Unsplash

2022年7月29日、インプレスR&D社よりSwift Concurrencyの解説本をリリースしました。

一冊でマスター!Swift Concurrency入門です。

一冊でマスター!Swift Concurrency入門

こちらの本は一冊でSwift Concurrencyの機能をほぼ網羅したConcurrency機能の解説本です。
日本語でSwift Concurrencyを学べる解説本はまだ少ないので、Swift 5.5からの非同期処理をうまく書きたい方には必見の本となっています。

詳しい内容はこちらをご覧ください。
同人誌として先にリリースしましたが、商業版も同じ内容となっています。
https://blog.personal-factory.com/2022/05/29/self-published-book-swift-concurrency/

さて、せっかくリリースしたばかりなのですが、一部のサンプルコードがあまり良いコードではありませんでした。
すでにサンプルコードのリポジトリは修正済みですが、どのような修正があったのかをこの記事で説明したいと思います。

Task.initのクロージャーに[weak self]はいらない

もともとのサンプルコードではこのようにTask.initのクロージャーに[weak self]をつけていました。
[weak self]をして素朴に循環参照を回避しようとしたコードにしていました。

Task { [weak self] in
     let mypageData = await self?.fetchMyPageData()
     print(mypageData ?? "")
 }

ところが、Task.initのクロージャー内でselfを参照しても循環参照の恐れがないそうなんですよね。
しかも@_implicitSelfCaptureという特別な属性が機能していて、クロージャー内に明示的にselfと書かなくてもコンパイルが通ります。

Task {
     let mypageData = await fetchMyPageData() // self.fetchMyPageData()でなくてよい
     print(mypageData)
 }

なぜこの書き方ができるのかを順を追って説明します。

エスケープクロージャのおさらい

Task.initの定義をXcodeの定義ジャンプで見てみます。

extension Task where Failure == Never {
    public init(priority: TaskPriority? = nil, operation: @escaping @Sendable () async -> Success)
}

operation引数はクロージャーになっていて@escaping属性がついていることが分かります。

@escaping属性をおさらいしてみましょう。
@escaping属性は関数の引数として渡されたクロージャが、関数本文が終了した後に呼び出される場合につけられる属性です。
@escaping属性をつけられたクロージャー内ではselfインスタンスのアクセスは明示的にselfをつけないとコンパイルエラーになります。
これは誤って循環参照が発生しないように開発者に注意を促す目的があります。

例えば、次のようなコードがあるとします。

class SampleClouser {
    var value: Int = 0
    var completion: () -> Void = {}
    func add(value: Int, completion: @escaping () -> Void) {
        self.value += value
        completion()
    }
    func addOne() {
        add(value: 1) {
            print(value) // Reference to property 'value' in closure requires explicit use of 'self' to make capture semantics explicit
        }
    }

    init() {}
}

addメソッドの引数completion@escaping属性がついたクロージャーです。
別のメソッドaddOneaddメソッドを呼び出した際にクロージャー内でselfのインスタンス、ここではvalueを参照しています。
しかし、このままですと循環参照の危険があるためコンパイルエラーとなります。

Reference to property 'value' in closure requires explicit use of 'self' to make capture semantics explicit
// [筆者訳] クロージャー内の`value`プロパティの参照は明示的にselfを使用し、明示的なキャプチャを作成してください。

@escaping属性がついたクロージャーでよく行う循環参照の解消方法は[weak self]selfをキャプチャする方法です。
これによって、循環参照を起こさずselfのインスタンスにクロージャー内でアクセスができるようになります。

func addOne() {
    add(value: 1) { [weak self] in
        print(self?.value ?? "")
    }
}

エスケープクロージャ自体を解説はThe Swift Programming Language(日本語版)を参照ください。

https://www.swiftlangjp.com/language-guide/closures.html#エスケープクロージャescaping-closures

Task.initのクロージャーのself参照

ところが、Task.initのクロージャー内では@escaping属性があるにも関わらず、明示的なself参照を考慮しなくても循環参照の問題が発生しません。

Taskが提案されたSE-304 Structured concurrencyのプロポーザルを見てみましょう。
Implicit "self" という節に書かれています。

https://github.com/apple/swift-evolution/blob/main/proposals/0304-structured-concurrency.md#implicit-self

The intent behind requiring self. when capturing self in an escaping closure is to warn the developer about potential reference cycles. The closure passed to Task is executed immediately, and the only reference to self is what occurs in the body. Therefore, the explicit self. isn't communicating useful information and should not be required.
[筆者訳]エスケープクロージャ内でselfをキャプチャする際にself.が必須な背景は開発者に循環参照の可能性を警告するためです。一方でTaskに渡されたクロージャーは即時に実行され、selfへの唯一の参照はクロージャー本文で発生するだけです。したがって、selfを明示することは有効な情報を伝えておらず、必須にすべきでもありません。

Task.initのクロージャーは即時に実行され、selfインスタンスへの循環参照の恐れがないので暗黙的にself参照しても問題ないという記載がされています。

更にSwiftのリポジトリからTask.initの定義をみてみましょう。

https://github.com/apple/swift/blob/dcea14716cb17b080dc374c831b5717d96800738/stdlib/public/Concurrency/Task.swift#L564

extension Task where Failure == Error {
  @discardableResult
  @_alwaysEmitIntoClient
  public init(
    priority: TaskPriority? = nil,
    @_inheritActorContext @_implicitSelfCapture operation: __owned @Sendable @escaping () async throws -> Success
  ) 

operationクロージャーの引数の前に@_implicitSelfCaptureという属性がついています。
アンダースコアがついている属性は正式な言語機能としてはまだ議論中の属性です。
@_implicitSelfCaptureselfが参照型であってもクロージャー内でキャプチャをしなくてもselfにアクセスができるようになる属性です。

class C {
  func f() {}
  func g(_: @escaping () -> Void) {
    g({ f() }) // error: call to method 'f' in closure requires explicit use of 'self'
  }
  func h(@_implicitSelfCapture _: @escaping () -> Void) {
    h({ f() }) // ok
  }
}

https://github.com/apple/swift/blob/main/docs/ReferenceGuides/UnderscoredAttributes.md#_implicitselfcapture

Task.initのクロージャーには@_implicitSelfCaptureがつけられていたため、明示的にselfをつけなくてもselfのメソッドやプロパティにアクセスができたということです。

冒頭のサンプルコードをもう一度掲載します。

Task {
     let mypageData = await fetchMyPageData() // self.fetchMyPageData()でなくてよい
     print(mypageData)
 }

fetchMyPageDataメソッドはselfに定義されているメソッドです。
通常の@escapingがつけられたクロージャーではself.fetchMyPageData()としなければコンパイルエラーになりますが、Task.init@_implicitSelfCaptureもつけられているため、selfをつけなくてもコンパイルエラーが起こらないのです。

Xcodeの定義ジャンプでは@_implicitSelfCaptureが確認できない

ただ、奇妙なことはXcodeの定義ジャンプではTask.initの定義に@_implicitSelfCaptureのついていることは確認できませんでした。

手元で確認したのは次のバージョンです。

  • Xcode 13.4.1
  • Xcode 14 beta4

このようにoperation引数は@escapingがつけられた通常のクロージャーにみえます。

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension Task where Failure == Never {
    public init(priority: TaskPriority? = nil, operation: @escaping @Sendable () async -> Success)
}

アンダースコアがつけられた属性は正式な言語機能として決定したものではないので、定義ジャンプからでは隠されてしまっているのかもしれません。

Task.initでクロージャー内にselfキャプチャが不要な理由は@_implicitSelfCaptureが付与されていることを覚えておきましょう。
そのうち正式な属性になった際にはXcodeからでも確認ができるようになるでしょう。

Task.detachedとTaskGroup.addTaskのクロージャーも[weak self]いらない

SE-304のImplicit "self"節では、Task.initの他にもTask.detachedTaskGroup.addTaskのクロージャーにもselfの明示的なキャプチャは必要ないと書かれています。
つまりこの2つのクロージャー引数も[weak self]はいりません。

なので次のようにTask.detached[weak self]でクロージャー内でselfキャプチャを弱参照にする必要もありません。

// ✗ クロージャーで[weak self]する必要なし
Task.detached(priority: .low) { [weak self] in 
    guard let self = self else { return }
    async let _ = await self.sendLog(name: "didTapButton")
}
 
// ○ クロージャー内でselfに直接アクセスしてよい
Task.detached(priority: .low) { 
    async let _ = await self.sendLog(name: "didTapButton")
}

しかし、Task.initとは異なり暗黙的なselfキャプチャはできないようです。

SwiftのリポジトリでTask.detachedの定義をみてみます。

https://github.com/apple/swift/blob/dcea14716cb17b080dc374c831b5717d96800738/stdlib/public/Concurrency/Task.swift#L621-L624

@discardableResult
@_alwaysEmitIntoClient
public static func detached(
priority: TaskPriority? = nil,
operation: __owned @Sendable @escaping () async -> Success
) -> Task<Success, Failure> {

Task.initとは異なりoperationクロージャーの引数に@_implicitSelfCapture属性はつけられていませんでした。
ですので、クロージャー内では明示的にselfを記述が必要です。

// 暗黙的なselfキャプチャはできない。
Task.detached(priority: .low) { 
    async let _ = await sendLog(name: "didTapButton") // Call to method 'sendLog' in closure requires explicit use of 'self' to make capture semantics explicit
}

上記のコードではsendLogメソッドにself.を消すとCall to method 'sendLog' in closure requires explicit use of 'self' to make capture semantics explicitのコンパイルエラーが表示されます。
明示的にself.sendLogselfのキャプチャが必要です。

同じことがTaskGroup.addTaskThrowingTaskGroup.addTaskにも言えます。

TaskGroup.addTaskの定義はこちらです。

@_alwaysEmitIntoClient
public mutating func addTask(
priority: TaskPriority? = nil,
operation: __owned @Sendable @escaping () async -> ChildTaskResult
) {

https://github.com/apple/swift/blob/dcea14716cb17b080dc374c831b5717d96800738/stdlib/public/Concurrency/TaskGroup.swift#L233-L237

@_implicitSelfCapture属性がありません。

ThrowingTaskGroup.addTaskの定義はこちらです。

@_alwaysEmitIntoClient
public mutating func addTask(
priority: TaskPriority? = nil,
operation: __owned @Sendable @escaping () async throws -> ChildTaskResult
) {

https://github.com/apple/swift/blob/dcea14716cb17b080dc374c831b5717d96800738/stdlib/public/Concurrency/TaskGroup.swift#L485-L489

同じく@_implicitSelfCapture属性はありません。

なのでTaskGroup.addTaskThrowingTaskGroup.addTaskのクロージャー内でもselfの記述は省略できません。
しかし循環参照の恐れはないので明示的にself.と書いて問題ないでしょう。

Task.initでは@_implicitSelfCaptureがつけられ、Task.detachedTaskGroup.addTaskThrowingTaskGroup.addTaskではつけられていないのは単純に一貫性がないと思われるので、Swiftのバージョンがあがれば修正されるかもしれません。

Task.initで[weak self]がいらなくなるのはとても嬉しい

Task.initTask.detachedTaskGroup.addTaskThrowingTaskGroup.addTaskでクロージャーでselfの循環参照を気にする必要がなくなるのはとてもよいですね。

Swift Concurrencyが解決した非同期処理コードの問題点の一つに、コールバックが呼ばれない問題というものがあります。

例えば、次のrequest関数ですが、エラー発生時にコールバックが呼ばれていません。

func request(url: URL, completionHandler: @escaping (Result<UIImage, Error>) -> ()) {
    let task = URLSession.shared.dataTask(with: url) { data, response, error in
        guard error == nil else { return } // コールバック呼んでいない
        downloadImage(data: data) { result in
            let image = try? result.get()
            resizeImage(image: image) { result in
                completionHandler(result)
            }
        }
    }
    task.resume()
}

このままではエラー発生時に呼び出し側で後続の処理が進まずにバグの原因になる可能性があります。

Swift Concurrencyではasyncで非同期関数を定義することで必ず戻り値かエラーをスローさせる関数を定義できます。

非同期関数: asyncがつけられた関数・メソッドのことを便宜的にこう呼びます。asyncがない従来の関数・メソッドは対比のために同期関数と呼んでいます。

func request(url: URL) async throws -> UIImage {
    let (data, response) = try await URLSession.shared.data(from: url, delegate: nil)
    let image = try await downloadImage(data: data)
    let resizedImage = try await resizeImage(image: image)
    return resizedImage
}

呼び出し元では戻り値かエラーをキャッチすることで全てのパスをハンドリングが可能になります。

つまりSwift Concurrencyでは非同期処理でも戻り値がある関数・メソッドの定義が重要になります。
今まではクロージャーを引数に渡して関数・メソッド自体の戻り値はVoidにすることが多かったと思いますが、それとは異なるということです。

戻り値が重要になることが多くなると、[weak self]selfをオプショナルで扱うと書きづらくなる場面が多々出てきます。

例えば、Task.init[weak self]で書いてみます。

Task { [weak self] in
    let mypageData = await self?.fetchMyPageData() // 戻り値がオプショナル
    print(mypageData ?? "")
}
func fetchMyPageData() async -> MypageInfo {
...
}

fetchMyPageDataメソッドはMypageInfoという構造体を返すメソッドです。
せっかく通常の型を戻り値としているのに、self?.fetchMyPageDataとしているがために戻り値mypageDataもオプショナルをアンラップしなければならなくなります。

またTask.initのクロージャーは値を返すことで後でその値を利用できます。

// taskを保持しておいて
let task = Task { () -> MypageInfo in
    let mypageData = await fetchMyPageData()
    return mypageData
}

Task {
    // 後で使う
    let value = await task.value
}

その際に[weak self]を書いてしまうとオプショナルのアンラップ処理をどこかで入れる必要が出てきてしまいます。

// self?.で記述
let task = Task { [weak self] () -> MypageInfo? in // クロージャーの戻り値をオプショナルにする?
    let mypageData = await self?.fetchMyPageData()
    return mypageData
}

// guard letで記述
let task = Task { [weak self] () -> MypageInfo? in
    guard let self = self else { return nil }
    let mypageData = await self.fetchMyPageData()
    return mypageData
}

これは面倒ですね。
なのでselfの循環参照を気にする必要がないのはとてもありがたいことです。

動作環境

下記の環境で動作を確認しました。

  • Xcode 13.4.1
  • Xcode 14beta4

サンプルコードの修正状況

本書のサンプルコードはすでに修正しています。
修正内容は下記からご確認をお願いします。

参考記事

一冊でマスター!Swift Concurrency入門

改めて、「一冊でマスター!Swift Concurrency入門」はAmazonで販売中です。
Swift Concurrencyを網羅した解説書はまだ他にはないです。
ご興味あればぜひ手にとってご覧いただければ幸いです。

一冊でマスター!Swift Concurrency入門です。

一冊でマスター!Swift Concurrency入門