【Swift Concurrency】@TaskLocalについて
概要
@TaskLocal
は、特定のコンテキストごとにローカルな値を保持するためのプロパティラッパーです。
基本的に以下のような形で定義します。@TaskLocal
は、staticプロパティとして定義される必要があり、尚且つデフォルト値をもつ必要があります。
enum Sample {
@TaskLocal static var value: String?
}
SwiftUIの@Environment
に似ています。例外を除きタスクローカル値は、特定のタスク内で作成された全て子タスクからアクセスできます。(例外は注意点として後述します。)
使い方
タスクローカル値(TaskLocal<Value>
)がもつwithValue
で値を設定し、そのスコープ内で設定した値を使用するといった形です。コンテキストの作成と捉えていただいても差し支えないと思います。
Swift Concurrencyのキャッチアップ中に@TaskLocal
の存在を知り、本記事に辿り着いたという方も多いと思いますが、@TaskLocal
に関しては、非同期コンテキスト(Task内やrefreshable内)でなくても使用できます。
サンプル1
以下のサンプルコードでは、MyTaskLocal.$value.withValue("🟥")
でタスクローカル値が"🟥"
に設定されているコンテキストを作成し、その中でaction
を呼び出しています。action
はMyTaskLocal.value
を出力します。呼び出したコンテキストでは、MyTaskLocal.value
は"🟥"
に設定されているため、Optional("🟥")
が出力されます。
enum MyTaskLocal {
@TaskLocal static var value: String?
}
func action() {
print(MyTaskLocal.value as Any)
}
MyTaskLocal.$value.withValue("🟥") {
action()
}
// 出力結果
// Optional("🟥")
サンプル2
タスクローカル値は設定後、特定のタイミング(階層)で値を変更することもできます。
enum MyTaskLocal {
@TaskLocal static var value: String?
}
func action(_ prefix: String) {
print(prefix, MyTaskLocal.value as Any)
}
MyTaskLocal.$value.withValue("🟥") {
action("①")
MyTaskLocal.$value.withValue("🟦") {
action("②")
}
}
// 出力結果
// ① Optional("🟥")
// ② Optional("🟦")
サンプル3
タスクローカル値が設定されていないコンテキストで、タスクローカス値にアクセスした場合は、デフォルト値が返ります。
enum MyTaskLocal {
@TaskLocal static var value: String?
}
func action() {
print(MyTaskLocal.value as Any)
}
action()
// 出力結果
// nil
サンプル4
以下のサンプルコードでは、MyTaskLocal.value
に
"🟥"
が設定されているコンテキスト、
"🟦"
が設定されているコンテキスト、
"🟨"
が設定されているコンテキスト
を作成し、action
を実行しています。
action
はMyTaskLocal.value
を出力しますが、それぞれのコンテキストでMyTaskLocal.value
の値は異なるので、以下のような出力結果となります。
enum MyTaskLocal {
@TaskLocal static var value: String?
}
Task {
await withDiscardingTaskGroup { group in
for memberName in ["🟥", "🟦", "🟨"] {
group.addTask {
MyTaskLocal.$value.withValue(memberName) {
action()
}
}
}
}
}
func action() {
print(MyTaskLocal.value as Any)
}
// 出力結果
// Optional("🟥")
// Optional("🟦")
// Optional("🟨")
サンプル5
先ほどのコードでは、コンテキスト内で同期的な関数action
を呼び出していました。タスクローカル値を設定したコンテキストで非同期関数を呼ぶ場合は、非同期関数として定義されているwithValue
を使用します。(先ほどのコードにawait
をつけるだけです。)
withValue(同期)
@discardableResult
final func withValue<R>(
_ valueDuringOperation: Value,
operation: () throws -> R,
file: String = #fileID,
line: UInt = #line
) rethrows -> R
withValue(非同期)
@backDeployed(before: macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0)
@discardableResult
final func withValue<R>(
_ valueDuringOperation: Value,
operation: () async throws -> R,
isolation: isolated (any Actor)? = #isolation,
file: String = #fileID,
line: UInt = #line
) async rethrows -> R
enum MyTaskLocal {
@TaskLocal static var value: String?
}
Task {
await withDiscardingTaskGroup { group in
for memberName in ["🟥", "🟦", "🟨"] {
group.addTask {
await MyTaskLocal.$value.withValue(memberName) {
await asynchronousAction()
}
}
}
}
}
func asynchronousAction() async {
try? await Task.sleep(for: .seconds(3))
print(MyTaskLocal.value as Any)
}
// 出力結果(順序が異なることが多い)
// Optional("🟥")
// Optional("🟦")
// Optional("🟨")
注意点
Task
はun-structured(非構造化)
となりますが、タスクローカル値を継承します。しかし、detached
で作成されたTask
に関しては、タスクローカル値を継承しません。
enum MyTaskLocal {
@TaskLocal static var value: String?
}
MyTaskLocal.$value.withValue("🟥🟦🟨") {
Task {
print("①", MyTaskLocal.value as Any)
}
Task.detached {
print("②", MyTaskLocal.value as Any)
}
}
// 出力結果
// ① Optional("🟥🟦🟨")
// ② nil
公式ドキュメント
Discussion