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からの非同期処理をうまく書きたい方には必見の本となっています。

詳しい内容はこちらをご覧ください。
同人誌として先にリリースしましたが、商業版も同じ内容となっています。

同人版はBOOTHで販売中です。



Swift Concurrency入門

https://personal-factory.booth.pm/items/3888109

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

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を使用し、明示的なキャプチャを作成してください。

ただし、そのままvalueプロパティにselfをつけると循環参照が起こり、メモリリークが発生します。

func addOne() {
    add(value: 1) {
        print(self.value)
    }
}

理由は、selfであるSampleClousercompletionクロージャーをプロパティとして強参照している一方で、completionクロージャーもクロージャー内でselfを強参照しており、これが循環参照を引き起こすからです。

----------2022-09-20-10.10.11

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

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

----------2022-09-20-10.12.17

エスケープクロージャ自体を解説は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のバージョンがあがれば修正されるかもしれません。

非同期処理のキャンセル

[weal self]とguard文で早期リターン

クロージャーでのコールバックで非同期処理を書く場合は、[weak self]でキャプチャした後、guard文で早期リターンをすることが多いです。

request { [weak self] in
    // 早期リターン
    guard let self = self else { return }
    // selfがなかったら以降の処理は実行されない
    print(self.number)
}

処理をキャンセルすることを目的に書いている方もいるかと思います。

しかし、Task.initのクロージャー内ではこの早期リターンが意味がある場面は多くはないでしょう。
次のコードはView ControllerのviewDidLoadメソッドでTask.initを呼び出すコードです。

override func viewDidLoad() {
    super.viewDidLoad() // ①
    Task { [weak self] in
        guard let self = self else { return } // ③
        try await Task.sleep(nanoseconds: 3 * NSEC_PER_SEC) // ④
    }
    print(number) // ②
}

コードが呼ばれる順番は①、②、③、④の順番になります。
②が終わったらすぐ Task.initのクロージャーが呼ばれ③が実行されます。
②から③の実行間隔はとても短いので、その間にselfがなくなることは多くの場合はないでしょう。
先ほどの例では、ほとんどの場合④に到達して、非同期処理を開始してしまいます。
④は3秒待つという重い処理です。
③で早期リターンしてもselfがなくなる場面が限られているので、ほとんどの場合④の重い処理が開始されてしまいます。
Task.initのクロージャーに早期リターンをしてもTask.initのクロージャー処理をキャンセルされる場合は限定的です。

ではどうすればいいでしょうか?

Task.cancelを使おう

Task.cancelを使いましょう。
今回の例ではselfがView Controllerでした。
画面が非表示になったらTaskをキャンセルをするコードは次のとおりです。

var task: Task<Void, Error>?
override func viewDidLoad() {
    super.viewDidLoad()
    // taskを保持
    task = Task {
        try await Task.sleep(nanoseconds: 3 * NSEC_PER_SEC)
        print(number)
    }
}
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    // 画面非表示でキャンセル
    task?.cancel()
}

Taskのインスタンスをtaskとして保持します。
viewDidAppeartaskcancelメソッドを呼び出すことでキャンセルができます。

cancelメソッドでTask.initクロージャーの処理が必ず停止するわけではない

ただし、cancelメソッドを呼び出せば、必ずTask.initクロージャーの処理が必ず停止するわけではありません。
Taskの処理を実際に止めるにはCancellationErrorというエラーをスローする必要があり、エラーをスローしない関数を呼んでいる場合はcancelしても処理は止まりません。

例えば、先ほどの例ではスリープ処理にTask.sleep(nanoseconds:)を使っていました。
これはTaskがキャンセルされるとCancellationErrorをスローするのですぐに処理が停止します。

task = Task {
    // Taskがキャンセルされるとすぐに停止する
    try await Task.sleep(nanoseconds: 3 * NSEC_PER_SEC)
}

Taskを使ったもう一つのスリープ処理はTask.sleep()です。
これはTaskがキャンセルされても停止しません。
画面が閉じても、きっちり3秒処理を待たないといけません。

task = Task {
    // Taskがキャンセルされても停止しない
    await Task.sleep(3 * NSEC_PER_SEC) 
}

キャンセルされた場合に処理を適切に停止したい場合はTask.checkCancellation()Task.isCancelledが利用できます。

  • Task.checkCancellation()Taskがキャンセルされた場合にCancellationErrorをスローするメソッド
    • とりあえず処理を止める場合に使える
  • Task.isCancelledTaskがキャンセルされた場合にtrueを返す
    • キャンセルされた場合に独自処理が記述できる

使い方としてはCancellationErrorスローしない処理の前後に呼び出すことで、すでにキャンセルしていた場合に処理を止めることができます。

task = Task {
    // すでにキャンセルされていたら処理を停止
   try Task.checkCancellation() 
    // CancellationErrorスローしない処理
    await Task.sleep(3 * NSEC_PER_SEC)
    // すでにキャンセルされていたら処理を停止
    try Task.checkCancellation()
}

上の例ではTask.sleep()が始まる前にTask.checkCancellationを呼び出して、すでにキャンセルされていたら処理を停止します。
Task.sleep()が実行されると、その間にキャンセルされても3秒待ってしまいます。
が、その後に再びTask.checkCancellationを呼び出すことでなるべく無駄な時間を作らずに処理を停止できます。

Task.isCancelledを使った例も見ていきましょう。

task = Task {
    await Task.sleep(3 * NSEC_PER_SEC)
    if Task.isCancelled {
        // 独自のキャンセル処理
        // 最後にエラーをスローすると処理が停止
        throw CancellationError()
    }

キャンセルされた場合はTask.isCancelledtrueになるので、if文のブロックに独自のキャンセル処理を実装できます。
最後にエラーをスローすることで処理を停止できます。
ここでは便宜上CancellationErrorをスローしています。
独自のError型でも問題ないです。

Task.initクロージャーのメモリ解放タイミング

Task.initクロージャーのメモリ解放タイミングはクロージャーの処理が全て完了してからです。
その間はselfインスタンスの参照が残っています。
selfを消したと思っても処理が継続する可能性があるので、注意が必要です。

例えば、viewDidLoadで3秒かかる処理をTask.initで実行した場合、3秒未満に画面を非表示する場合を考えます。
画面非表示で特にTask.cancelの処理は行わない想定です。

override func viewDidLoad() {
    super.viewDidLoad()
    Task { 
        // 3秒かかる処理。
        // 3秒以内画面が非表示になっても処理は継続する
        try await Task.sleep(nanoseconds: 3 * NSEC_PER_SEC)
        print(number)
    }
}
override func viewDidAppear(_ animated: Bool) {
    // Taskのキャンセル処理はしていない
    super.viewDidAppear(animated)
}

viewDidLoadで呼んでいるTask.initの処理は3秒待つ処理があります。
viewDidLoadが呼ばれて3秒未満に画面を消す動作をしても、Task.initのクロージャーの処理は終わらず、最後まで実行され続けます。
つまり、3秒経ってクロージャーの処理がすべて終わってからselfを解放します。

この例ではTask.sleep(nanoseconds:)を使っていたのでスリープする秒数間待てばselfは解放されますが、中には処理が無限に終わらない場合もあります。

次のようにNotificationのイベントをfor await inで待ち合わせるコードを書いた場合、for await inのコードはキャンセルを明示的にしない限り無限にイベントを待ち受けます。
Task.initにクロージャーの処理は終わらず、selfインスタンスはいつまで経っても解放されません。

override func viewDidLoad() {
    super.viewDidLoad()
    Task { 
        // イベントを待ち受ける
        // キャンセルしない限り、この処理は終わらない。つまりselfも解放されない
        for await notification in NotificationCenter.default.notifications(named: UIScene.willEnterForegroundNotification) {
            print(notification)
        }
    }
}

このコードは、UIScene.willEnterForegroundNotificationのイベントをfor await inで待ち受けています。
キャンセルのコードは書いていないので、無限にイベントを待ち合わせます。
selfのViewControllerが非表示になってもselfインスタンスは解放されません。

Task.cancelを使ってタスクをキャンセルしましょう。

Taskをインスタンスとして保持しておいて、Task.cancelを行う

「Task.cancelを使おう」の章でお伝えした通りで、selfがなくなるタイミングで確実にTask.cancelを行い、タスクをキャンセルする方法です。
特にfor await inでは無限にイベントを待ち合わせるので明示的にキャンセルをして、無駄なメモリを解放することが重要です。

override func viewDidLoad() {
    super.viewDidLoad()
    // taskインスタンスを保持しておく
    task = Task { [weak self] in
        for await notification in NotificationCenter.default.notifications(named: UIApplication.willEnterForegroundNotification) {
            print(notification)
        }
    }
}

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    // 画面非表示でtaskをキャンセルする
    // for await inの処理もキャンセルされる -> メモリの無駄がなくなる
    task?.cancel()
}

重い処理が終わった後にselfインスタンスの有無を確認する

一応重い処理が終わった後にselfインスタンスの有無を確認することでもキャンセルに似た動きをすることができます。
先ほど「非同期処理のキャンセル」の章の「[weal self]とguard文で早期リターン」では、Task.initのクロージャーの最初でselfインスタンスの有無を確認するのは不要と話しました。
一方で、重い処理が終わった後にselfインスタンスの有無を確認するのは場合によっては処理をキャンセルする意味合いを持たせられます。

次のコードでは3秒待つ処理の後にselfの有無をチェックしています。

override func viewDidLoad() {
    super.viewDidLoad()

    task = Task { [weak self] in
        try await Task.sleep(nanoseconds: 3 * NSEC_PER_SEC)
        // 3秒のスリープの後にselfの有無チェック
        guard let self = self else { return }
        // selfがなければこの処理は行わない
        print(self.number)
    }
}

こうすることで処理が終わった後にselfが解放されている場合は、後続の処理は行わないことを表現できます。

guard文を使わなくてもself?を使って、その都度selfの有無をチェックできます。

override func viewDidLoad() {
    super.viewDidLoad()

    task = Task { [weak self] in
        try await Task.sleep(nanoseconds: 3 * NSEC_PER_SEC)
        // 3秒のスリープの後にselfの有無チェック
        // selfがnilならprint文は実行されない
        print(self?.number)
    }
}

ただし、個人的には、Task.cancelを利用したほうが読みやすいとは思います。

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 {
...
}

struct MypageInfo { ... }

fetchMyPageDataメソッドはMypageInfoというstructを返すメソッドです。
せっかく通常の型を戻り値としているのに、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

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

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

本ブログのサンプルコード

Task.initでselfキャプチャを強参照するコードを次のgistに残しました。
動きの確認に使ってください。

https://gist.github.com/SatoTakeshiX/c3cb6fe57ff1424931c21d97c5f1496a

参考記事

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

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

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

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