ActorのプロパティをActorコンテキスト外で更新するためには、インスタンスメソッドを定義して、その中でプロパティを更新する必要があります。
actor BankAccount {
var balance: Double
// propertyのsetterはActor context外からは呼び出せないため、Setterメソッドを定義してメソッド内でsetterを呼び出す
func setBalance(_ newBalance: Double) {
balance = newBalance
}
}
Task.detached {
print(await account.balance) // okay. getは許可されている
await account.balance = 1000.0 // error: cross-actor property mutations are not permitted. setは許可されていない
await account.setBalance(1000.0) // okay. setはActorメソッドを通して呼び出す
}
Proposal にも、get処理だけであれば非同期での取得が可能であることが記載されています。
Cross-actor references to an actor property are permitted as an asynchronous call so long as they are read-only accesses:
Rationale: it is possible to support cross-actor property sets. However, cross-actor inout operations cannot be reasonably supported because there would be an implicit suspension point between the "get" and the "set" that could introduce what would effectively be race conditions. Moreover, setting properties asynchronously may make it easier to break invariants unintentionally if, e.g., two properties need to be updated at once to maintain an invariant.
(翻訳) クロスアクター間でプロパティの代入をサポートすることは可能です。しかし、inout 操作をクロスアクターでサポートすることは現実的ではありません。なぜなら、get と set の間に暗黙的なサスペンションポイント(中断点)が入り得るため、事実上の競合状態(レースコンディション)を引き起こす可能性があるからです。さらに、非同期にプロパティを設定することで、たとえば2つのプロパティを同時に更新して初めて成り立つ不変条件(インバリアント)を意図せず破ってしまいやすくなる可能性があります。
一方で、MainActorには、static func runメソッドが定義されており、引数のクロージャ内であればMainActorにisolateされたプロパティの値を更新できます。
@MainActor class MainActorClass {
var value: Int = 0
}
let mainActorClass = MainActorClass()
Task.detached {
await MainActor.run {
mainActorClass.value = 1 // okay
}
}
同様のことをActorで行おうとすると、最初の例のように毎回Setterメソッドを定義する必要があります。これは非常に面倒ですし、コード量も増えてしまいます。
解決策
そこで、Actorを拡張してMainActorのrunに相当するメソッドを追加することで、毎回Setterメソッドを追加しなくてもActorコンテキスト外から値を更新できるようにします。
extension Actor {
func run<T>(body: @Sendable (isolated Self) throws -> T) rethrows -> T {
return try body(self)
}
}
GlobalActorであるMainActorとは異なり、Actorはインスタンスごとにisolateされるため、runメソッドはインスタンスメソッドとして定義する必要があります。このコードを使えば、先ほどの例はSetterメソッドなしに書くとこうなります。
Task.detached {
print(await account.balance) // okay. getは許可されている
await account.run { $0.balance = 1000.0 } // okay.
}
これで、毎回Setterメソッドを定義する必要がなくなります。getとsetの間にsuspendポイントが入ることはないため、race conditionの心配もありません。
注意点
Actorの想定している使い方としては、プロパティはあくまでもActor内部の状態を表現するものです。
Apple Forumでも指摘されている通り、Actorはそもそもプロパティの更新処理だけが外部から行われることは想定されておらず、あくまでも何か処理を行った結果としてプロパティが更新されることを想定していると思います。
そして、頻繁に外部からSetterメソッドが呼ばれる場合は、Mutexを使って排他制御を行う方が使い方としては適切なのではとの指摘もあったので、やたらめったらに使うのは避けた方がいいでしょう。
私自身は、このメソッドはプロダクションコードでは使っておらず、Unit Testでテスト条件に合わせた内部状態を持tActorを用意するために使用しています。