SE-0470 has the following:
Specifically, if a type parameter T does not conform to either Sendable or to a new protocol, SendableMetatype, then its metatype, T.Type, is not considered Sendable and cannot cross isolation boundaries.
But the current implemention actually allows to transfer metatype not conforming to SendableMetatype. In the following code fn1 compiles even though there is no constrait like T: SendableMetatype and test1 compiles even though it passes Foo.self to fn1.
protocol GlobalLookup {
static func lookupByName(_ name: String) -> Self?
}
@MainActor
class Foo: @MainActor GlobalLookup {
var name: String
init(name: String) {
self.name = name
}
static var instances: [Foo] = []
static func lookupByName(_ name: String) -> Self? {
if let i = instances.first(where: { $0.name == name }) {
return (i as! Self)
} else {
return nil
}
}
}
func fn1<T>(_ metatype: T.Type, isolation: isolated (any Actor)? = #isolation, name: String) async {
print(metatype)
}
@MainActor
func test1() async {
await fn1(Foo.self, isolation: nil, name: "testing") // OK
}
In contrast, compiler produces diagnostic only when the code would actually calls methods of isolated confomrance in a different isolation. See the following code. Note fn2 compiles even though there is no constrait like T: SendableMetatype. Only test2 fails to compile.
func fn2<T: GlobalLookup>(_ metatype: T.Type, isolation: isolated (any Actor)? = #isolation, name: String) async {
let _ = T.lookupByName(name)
}
@MainActor
func test2() async {
// error: main actor-isolated conformance of 'Foo' to 'GlobalLookup' cannot be used in @concurrent context [#IsolatedConformances]
await fn2(Foo.self, isolation: nil, name: "testing")
}
I guess I understand the current behaviors. test1 is OK because transferring Foo.self to a different isolation in a generic function whose type parameter has no constraint can't do any harm. I think this is true even in regular (not generic) function where the metatype is known at compile time.
Example 1: unable to create an instance because `init` can only be called in the original isolation
@MainActor
class Bar: @MainActor Equatable {
var name: String
init(name: String) {
self.name = name
}
static func ==(lhs: Bar, rhs: Bar) -> Bool {
lhs.name == rhs.name
}
}
func fn(metatype: Bar.Type, isolation: isolated (any Actor)? = #isolation) async {
print(metatype) // OK
let foo = metatype.init(name: "foo1") // error: main actor-isolated initializer 'init(name:)' cannot be called from outside of the actor
}
@MainActor
func test() async {
await fn(metatype: Bar.self, isolation: nil)
}
Example 2: even after successfully creating two instances in a differnt isolation, it's not allowed to call methods defined in isolated conformance on them
@MainActor
final class Bar: @MainActor Equatable {
var name: String = ""
nonisolated init() {}
func modify(name: String) {
self.name = name
}
static func ==(lhs: Bar, rhs: Bar) -> Bool {
lhs.name == rhs.name
}
}
func fn(metatype: Bar.Type, isolation: isolated (any Actor)? = #isolation) async {
print(metatype) // OK
let bar1 = metatype.init() // OK
let bar2 = metatype.init() // OK
print(bar1 == bar2) // error: main actor-isolated operator function '==' cannot be called from outside of the actor
}
@MainActor
func test() async {
await fn(metatype: Bar.self, isolation: nil)
}
However, IIUC compiler doesn't depend on metatype's SendableMetatype conformance to catch potential issues in abvoe code. This is different from the original design in SE-0470, which is based on SendableMetatype. Does this mean SendableMetatype has become irrelevant?