swiftgenericsprotocolsfactory-method

Type specific method is unavailable for a var returned with `some` directive


Consider a factory method pattern implementation:

import UIKit

protocol TransportProtocol: CustomStringConvertible {
    func techReview()
}
// Make default implemetation opposed to @objc optional methods and properties
extension TransportProtocol {
    func techReview() {
        print("Reviewing \(type(of: self))")
    }
    var description: String {
        "This is a \(type(of: self))"
    }
}

final class Car: TransportProtocol {
    func changeOil() {
        print("Changed oil")
    }
}

final class Ship: TransportProtocol {
}


protocol LogisticProtocol {
    associatedtype Transport: TransportProtocol
    func createTransport() -> Transport
    func delivery(from A: Any, to B: Any)
    func techRewiew(for transport: TransportProtocol)
}

extension LogisticProtocol {
    
    func delivery(from A: Any, to B: Any) {
        print("Moving \(type(of: self)) from \(A) to \(B)")
    }
    
    func techRewiew(for transport: TransportProtocol) {
        transport.techReview()
    }
}

final class RoadLogistics: LogisticProtocol {
    
    func createTransport() -> some TransportProtocol {
        Car()
    }
    
}

final class SeaLogistics: LogisticProtocol {
    
    func createTransport() -> some TransportProtocol {
        Ship()
    }
}

// Usage:

class Client {
    // ...
    static func someClientCode<L: LogisticProtocol>(creator: L) -> some TransportProtocol {
        let transport = creator.createTransport()
        print("I'm not aware of the creator's type, but it still works.\n"
              + transport.description + "\n")
        creator.delivery(from: "Source", to: "Destination")
        return transport
    }
    // ...
}

              
let someTransport = Client.someClientCode(creator: RoadLogistics())
type(of: someTransport.self) // Car
someTransport.changeOil() // Error: Value of type 'some TransportProtocol' has no member 'changeOil'

The question is why I can't call changeOil() on someTransport if the compiler knows it is a Car not just a TransportProtocol. Are there any benefits we can get from using some directive, not just bare protocol types?


Solution

  • The return type of the method is some TransportProtocol, not Car. So even though the returned instance is of type Car, you cannot call any methods on it that only exist on Car, but not on TransportProtocol.

    The runtime knows that the concrete type of someTransport is Car, but the compiler only knows that the return type conforms to TransportProtocol.

    If you want to be able to access Car methods on someTransport, you need to downcast it to Car.

    if let car = someTransport as? Car {
        car.changeOil()
    }
    

    Using the some keyword has type-system benefits when used for opaque returns types (introduced in SE-0244 as part of Swift 5.1), since it actually enables returning a protocol with associated types without having to explicitly make the method generic. This is what drives SwiftUI, since it enables you to return some View.

    On the other hand, using opaque return types for protocols without associated types holds no benefits, so in your particular example, there's no reason to use it.