我通过协议实现了多个服务,以便能够在 ViewModel 初始化时注入模拟服务,并且在启用严格的并发检查后,我收到许多警告
"Capture of 'self' with non-sendable type "ViewModelType" in 'async let' binding"
这是一个最小的可重现示例:
class ViewController: UIViewController {
let viewModel: UserProfileViewModel
init(service: UserProfileServiceProtocol) {
self.viewModel = UserProfileViewModel(service: service)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
getUserProfileData()
// Do any additional setup after loading the view.
}
func getUserProfileData() {
Task {
do {
try await viewModel.getProfileData()
// update UI
} catch {
print(error)
}
}
}
}
struct UserModel {
var userID: String
var username: String
var profilePictureURL: URL?
var profilePhoto: UIImage?
}
class UserProfileViewModel {
private let service: any UserProfileServiceProtocol
var user: UserModel?
var postsCount: Int?
var followersCount: Int?
var followedUsersCount: Int?
init(service: UserProfileServiceProtocol) {
self.service = service
}
func getProfileData() async throws {
async let user = service.getUserData()
async let followersCount = service.getFollowersCount()
async let followedUsersCount = service.getFollowingCount()
async let postsCount = service.getPostsCount()
self.user = try await user
self.followersCount = try await followersCount
self.followedUsersCount = try await followedUsersCount
self.postsCount = try await postsCount
}
}
protocol UserProfileServiceProtocol {
var followService: FollowSystemProtocol { get }
var userPostsService: UserPostsServiceProtocol { get }
var userDataService: UserDataServiceProtocol { get }
func getFollowersCount() async throws -> Int
func getFollowingCount() async throws -> Int
func getPostsCount() async throws -> Int
func getUserData() async throws -> UserModel
}
protocol FollowSystemProtocol {
func getFollowersNumber(for uid: String) async throws -> Int
func getFollowingNumber(for uid: String) async throws -> Int
}
protocol UserPostsServiceProtocol {
func getPostCount(for userID: String) async throws -> Int
}
protocol UserDataServiceProtocol {
func getUser(for userID: String) async throws -> UserModel
}
class UserService: UserProfileServiceProtocol {
let userID: String
let followService: FollowSystemProtocol
let userPostsService: UserPostsServiceProtocol
let userDataService: UserDataServiceProtocol
init(userID: String, followService: FollowSystemProtocol, userPostsService: UserPostsServiceProtocol, userDataService: UserDataServiceProtocol) {
self.userID = userID
self.followService = followService
self.userPostsService = userPostsService
self.userDataService = userDataService
}
func getFollowersCount() async throws -> Int {
let followersCount = try await followService.getFollowersNumber(for: userID)
return followersCount
}
func getFollowingCount() async throws -> Int {
let followersCount = try await followService.getFollowingNumber(for: userID)
return followersCount
}
func getPostsCount() async throws -> Int {
let postsCount = try await userPostsService.getPostCount(for: userID)
return postsCount
}
func getUserData() async throws -> UserModel {
let user = try await userDataService.getUser(for: userID)
return user
}
}
class FollowSystemService: FollowSystemProtocol {
func getFollowersNumber(for uid: String) async throws -> Int {
try await Task.sleep(for: .seconds(1))
return 5
}
func getFollowingNumber(for uid: String) async throws -> Int {
try await Task.sleep(for: .seconds(1))
return 19
}
}
class UserPostsService: UserPostsServiceProtocol {
func getPostCount(for userID: String) async throws -> Int {
try await Task.sleep(for: .seconds(1))
return 27
}
}
class UserProfileService: UserDataServiceProtocol {
func getUser(for userID: String) async throws -> UserModel {
try await Task.sleep(for: .seconds(1))
return UserModel(userID: "testUser_01", username: "testUser", profilePictureURL: nil)
}
}
我没有足够的经验来判断解决这个问题的正确方法是什么,所以我只是紧张地试图挖掘有关此问题的任何信息,但没有运气。
我应该使服务协议符合可发送吗?这样做是一种常见的做法吗?或者我应该做一些完全不同的事情来解决这个问题?
目前,您的视图模型还不是
Sendable
。因为视图模型没有与任何特定参与者隔离,所以它的所有 async
方法(凭借 SE-0338)都在“通用执行器”(即不是主线程)上运行。因此,您有一个后台线程更新视图控制器从主线程访问的属性)。那不是线程安全的。如果将“Concurrency Strict Checking”构建设置设置为“Complete”,您将看到更多有关缺乏线程安全性的警告。
视图模型至少应该将视图访问的属性与主要参与者隔离。更简单的是,我们经常将整个视图模型与主要参与者隔离。视图模型的整个工作就是支持视图(位于主要参与者上),因此将整个视图模型与主要参与者隔离也是有意义的:
@MainActor
class UserProfileViewModel {…}
关于服务,是的,您也会想要制定这些协议
Sendable
。编译器(尤其是“严格并发检查”构建设置为“完整”时)会警告您,Sendable
对象不能具有非Sendable
的属性。即,如果对象的属性不是线程安全的,那么该对象就不是线程安全的。所以,制定协议Sendable
:
protocol UserPostsServiceProtocol: Sendable {
func getPostCount(for userID: String) async throws -> Int
}
然后,当然,也进行实施
Sendable
。例如,如果服务没有可变属性,您可以将其声明为 final
:
final class UserPostsService: UserPostsServiceProtocol {
func getPostCount(for userID: String) async throws -> Int {
try await Task.sleep(for: .seconds(1))
return 27
}
}
但是,如果您想让服务具有一些内部可变状态,请添加一些同步,或者更容易的参与者隔离它。您可以将其隔离到主要参与者(这对于服务而言不如视图模型那么有吸引力),或者只是将其设为自己的参与者:
actor UserPostsService: UserPostsServiceProtocol {
private var value = 0
func getPostCount(for userID: String) async throws -> Int {
try await Task.sleep(for: .seconds(1))
value = 27
return value
}
}
因此,将所有这些放在一起,您最终可能会得到:
class ViewController: UIViewController {
let viewModel: UserProfileViewModel
var task: Task<Void, Error>?
init(service: UserProfileServiceProtocol) {
self.viewModel = UserProfileViewModel(service: service)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
getUserProfileData() // you might consider doing this in `viewDidAppear` … it depends upon whether this view presents other view controllers and whether you want it to re-fetch user profile data when it re-appears
}
// If you use unstructured concurrency, you are responsible for
// canceling the task whenever its results are no longer needed.
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
task?.cancel()
}
// So, make sure to save this unstructured concurrency `Task` in a property so you can cancel it when no longer needed
func getUserProfileData() {
task = Task {
do {
try await viewModel.getProfileData()
// update UI
} catch {
print(error)
}
}
}
}
struct UserModel {
var userID: String
var username: String
var profilePictureURL: URL? = nil
var profilePhoto: UIImage? = nil
}
@MainActor
class UserProfileViewModel {
private let service: any UserProfileServiceProtocol
var user: UserModel?
var postsCount: Int?
var followersCount: Int?
var followedUsersCount: Int?
init(service: UserProfileServiceProtocol) {
self.service = service
}
func getProfileData() async throws {
async let user = service.getUserData()
async let followersCount = service.getFollowersCount()
async let followedUsersCount = service.getFollowingCount()
async let postsCount = service.getPostsCount()
self.user = try await user
self.followersCount = try await followersCount
self.followedUsersCount = try await followedUsersCount
self.postsCount = try await postsCount
}
}
protocol UserProfileServiceProtocol: Sendable {
var followService: FollowSystemProtocol { get }
var userPostsService: UserPostsServiceProtocol { get }
var userDataService: UserDataServiceProtocol { get }
func getFollowersCount() async throws -> Int
func getFollowingCount() async throws -> Int
func getPostsCount() async throws -> Int
func getUserData() async throws -> UserModel
}
protocol FollowSystemProtocol: Sendable {
func getFollowersNumber(for uid: String) async throws -> Int
func getFollowingNumber(for uid: String) async throws -> Int
}
protocol UserPostsServiceProtocol: Sendable {
func getPostCount(for userID: String) async throws -> Int
}
protocol UserDataServiceProtocol: Sendable {
func getUser(for userID: String) async throws -> UserModel
}
final class UserService: UserProfileServiceProtocol {
let userID: String
let followService: FollowSystemProtocol
let userPostsService: UserPostsServiceProtocol
let userDataService: UserDataServiceProtocol
init(userID: String, followService: FollowSystemProtocol, userPostsService: UserPostsServiceProtocol, userDataService: UserDataServiceProtocol) {
self.userID = userID
self.followService = followService
self.userPostsService = userPostsService
self.userDataService = userDataService
}
func getFollowersCount() async throws -> Int {
let followersCount = try await followService.getFollowersNumber(for: userID)
return followersCount
}
func getFollowingCount() async throws -> Int {
let followersCount = try await followService.getFollowingNumber(for: userID)
return followersCount
}
func getPostsCount() async throws -> Int {
let postsCount = try await userPostsService.getPostCount(for: userID)
return postsCount
}
func getUserData() async throws -> UserModel {
let user = try await userDataService.getUser(for: userID)
return user
}
}
final class FollowSystemService: FollowSystemProtocol {
func getFollowersNumber(for uid: String) async throws -> Int {
try await Task.sleep(for: .seconds(1))
return 5
}
func getFollowingNumber(for uid: String) async throws -> Int {
try await Task.sleep(for: .seconds(1))
return 19
}
}
actor UserPostsService: UserPostsServiceProtocol {
var value = 0
func getPostCount(for userID: String) async throws -> Int {
try await Task.sleep(for: .seconds(1))
value = 27
return value
}
}
final class UserProfileService: UserDataServiceProtocol {
func getUser(for userID: String) async throws -> UserModel {
try await Task.sleep(for: .seconds(1))
return UserModel(userID: "testUser_01", username: "testUser")
}
}