Hello everyone. Could someone help me, please, with this piece of code?
protocol Service: AnyObject {}
final class ServiceImpl: Service {}
protocol Container<S> {
associatedtype S: AnyObject
func get() -> S
}
final class CustomContainer<S: AnyObject>: Container {
let factory: () -> S
weak var shared: S?
init(factory: @escaping () -> S) {
self.factory = factory
}
func get() -> S {
if let shared {
return shared
} else {
let shared = factory()
self.shared = shared
return shared
}
}
}
let container: any Container<Service> = CustomContainer { ServiceImpl() }
It seems that both Ss are constrained to be a class, and, in my opinion, the types are OK, but the compiler says "Generic class 'CustomContainer' requires that 'any Service' be a class type".
I also have tried the following variant, but the error remains the same.
let container: any Container<any Service> = CustomContainer<any Service> { ServiceImpl() }
If I delete AnyObject contraint from Container and CustomContainer, it compiles, but in this case I can't use weak var shared. As far as I understand it, seems any Service can't be constrained to a class, but maybe there is another reason or a workaround?
You need to provide a concrete type for <Service>, not a protocol. This compiles:
let container: any Container = CustomContainer { ServiceImpl() }
The problem is that the protocol Service itself is not a class type. protocol Service: AnyObject only means that types conforming to the protocol are class types. So Service (or any Service) doesn't fulfill the requirements of Container.S: AnyObject and CustomContainer.S: AnyObject.
Note that this also compiles (with no constraint on container2.S):
let container2: any Container = CustomContainer { ServiceImpl() }
The difference is that container2 has no information about the concrete type of container2.S, but this also allows you to assign other values to it that use a different concrete Service type. Which variant is better for you depends on your needs.
Only a handful of existential types can conform to AnyObject, namely protocol compositions that contain @objc existentials and marker protocols like Sendable.
If you have protocol P: AnyObject {}, then any P is actually represented as a pointer together with the witness table, so any P does not satisfy an AnyObject requirement.
You might also be able to use existential opening:
func makeCustomContainer<T: Service>(_ service: T) -> any Container {
return CustomContainer<T> { service }
}
let myService: any Service = ServiceImpl()
let container = makeCustomContainer(myService)
Can this ability be extended for other protocols? Is there any limitations for it?
I clearly understand that existential itself is not a class. But from the user perspective it is strange when a protocol inherited form AnyObject can not be used as a class type – it is proven at compile time that existential underlying instance is a class.
After introducing existential unboxing when passing them to generic arguments it seems to be achievable.
Your container has generic restriction to AnyObject, returning any Container from the function as were suggested erases notion of underlying type to be Service/ServiceImpl and leaves you with just AnyObject. You need to expose either constraint or concrete type somewhere. For example, you can constrain Container.S to require its conformance to Service:
protocol Container<S> {
associatedtype S: Service
}
While if you do not want to constraint it to Service protocols only, following @Slava_Pestov suggestion, you can make it work in the following way:
func makeContainer<T: Service>(_ s: T) -> some Container<T> {
return CustomContainer { s }
}
let myService = ServiceImpl()
let container = makeContainer(myService)
let service: any Service = container.get()
Which on interpackage level will work roughly that way, I assume:
enum Pkg {
private final class ServiceImpl: Service {
}
static func makeService() -> some Service {
return ServiceImpl()
}
static func makeContainer<T: Service>(_ s: T) -> some Container<T> {
return CustomContainer { s }
}
}
let myService = Pkg.makeService()
let container = Pkg.makeContainer(myService)
let service: any Service = container.get()
One way or another, if your Container type isn't restricted to have S: Service, returning just any Container will lose information on that type and the only way would be to cast it with as, but that removes any point of using generics here in the first place.
Also, note that with weak reference here,
If instance returned by factory won't be hold somewhere with strong reference to it, your shared instance might be quite often nil.
It seems all the ideas are about one approach: eliminate abstraction of Container and pass a concrete type/source (in your case, Pkg, which provides opaque Container). Or, in my variant, make Container a class, not a protocol.