The following generates a warning during compilation (error with Swift 6 mode):
final class Yolo : Sendable {
init(fileManager: FileManager) {
self.fileManager = fileManager
}
let fileManager: FileManager
}
print(Yolo(fileManager: .default))
However, if I store a closure that returns the same thing, I get no such warning/error:
final class Yolo2 : Sendable {
init(fileManagerFactory: @escaping @Sendable () -> FileManager) {
self.fileManagerFactory = fileManagerFactory
}
let fileManagerFactory: @Sendable () -> FileManager
var fileManager: FileManager {fileManagerFactory()}
}
print(Yolo2(fileManagerFactory: { .default }))
I guess my questions would be:
Why is the closure “{ FileManager.default }” Sendable if FileManager is not Sendable?
Why is Yolo2 Sendable when Yolo is not? Both can use fileManager the same way from what I can tell, so they risk the same memory corruption error, don’t they?
Sendable Conformance means that your object is safe to be passed across multiple thread and be accessed asynchronously. If you have class that conforms to Sendable all of your properties has to be Sendable and Constant.
The reason why swift is complaining here is that FileManager is a class and it is NOTSendable which makes it impossible for swift to guaranty that it will not be changed by some one else.
Imagine this scenario :
let manager = FileManager()
let yolo1 = Yolo(fileManager: manager)
let yolo2 = Yolo(fileManager: manager)
Task{
yolo1. changeFileManager()
}
Task{
yolo2. changeFileManager()
}
because both of them share the same manager instance and FileManager is not Sendable this might cause data race at run time.
On the other hand, your second implementation :
let fileManagerFactory: @Sendable () -> FileManager
var fileManager: FileManager {fileManagerFactory()}
this will always produce a fresh instance of FileManager every single time that you call yolo.fileManager thus it doesn't make data race issue.
this code will be safe to run cause every time you access yolo1.fileManager property a new instance is created
PS: when you put @Sendable before a closure declaration it means that your closure can't CAPTURE non-Sendable types but it might still produce(return) a non-Sendable type
I still don’t get how this is safer though!
The closure effectively returns the sameFileManager instance every time, so in you example, the two changeFileManager calls will indeed change the same file manager, won’t they?
Your closure will return a brand-new instance of FileManager every time. so the two calls will actually change 2 separate instances which will not affect the other one.
Yeah, there’s no guarantee that FileManager.default actually gives you the same instance each time. In practice I bet it does, meaning that whatever instance it returns must be Sendable-in-practice, but that still doesn’t mean that all FileManager subclasses are Sendable.
(Yes, I realize there are no public FileManager subclasses. Maybe Apple has some for testing or something though, we don’t know.)
Interesting.
However what prevents me from doing a “FileManager” which does not return a Sendable-in-practice instance when accessing default?
Something like this:
import Foundation
final class NotSendable {
static let `default`: NotSendable = .init()
var accessCount = 0
init() {}
}
final class Yolo2 : Sendable {
init(notSendableFactory: @escaping @Sendable () -> NotSendable) {
self.notSendableFactory = notSendableFactory
}
func letsBeDangerous() {
notSendable.accessCount += 1
}
let notSendableFactory: @Sendable () -> NotSendable
var notSendable: NotSendable {notSendableFactory()}
}
let yolo1 = Yolo2(notSendableFactory: { .default })
let yolo2 = Yolo2(notSendableFactory: { .default })
let t1 = Task{ yolo1.letsBeDangerous() }
let t2 = Task{ yolo1.letsBeDangerous() }
await t1.value
await t2.value
print(NotSendable.default.accessCount)
Unless I’m mistaken this is dangerous, is it not?
I am not warned about anything at all though (Swift 6.1).
EDIT:
I actually am prevented to compile with an error when compiling with -swift-version 6. Phew! static property 'default' is not concurrency-safe because non-'Sendable' type 'NotSendable' may have shared mutable state
No, that’s not it either. It works because you’re not allowed to assume you’ll get the same instance every time. It is an implementation detail that you do and that it is thread-safe nonetheless. (If probably an implementation detail that many many apps have ended up relying on before there was Sendable checking.)
By just digging in Foundation source code i just came up with this :
open class FileManager : @unchecked Sendable {
// Sendable note: _impl may only be mutated in `init`
private var _impl: _FileManagerImpl
private let _lock = LockedState<State>(initialState: .init(delegate: nil))
private static let _default = FileManager()
open class var `default`: FileManager {
_default
}
//...