Search code examples
iosswiftswift-protocolsassociated-types

Downcasting on protocols in Swift


I have the following code:

protocol Step { /* body */ }

enum StepA: Int, CaseIterable, Step {
    case one
    case two
    case three
}

enum StepB: Int, CaseIterable, Step {
    case one
    case two
    case three
}

protocol Component {
    var step: Step { get }
}

protocol ComponentA: Component {
    var step: StepA { get }
}

protocol ComponentB: Component {
    var step: StepB { get }
}


struct SomeComponentOfTypeA: ComponentA {
    var step: StepA = StepA.one
}


let a = SomeComponentOfTypeA()

print(a.step)

The type of a.step I would like to be StepA, since I am initalizing this way. But, I can't do that because:

struct SomeComponentOfTypeA: ComponentA {
    var step: StepA = StepA.one
}

the compiler tells me that:

Type 'SomeComponentOfTypeA' does not conform to protocol 'Component'

Basically, it doesn't know that since I am implementing the ComponentA, my step should be of type StepA too.

As a workaround I found that if I modify this part:

protocol Component {
    var step: Step { get }
}

into:

protocol Component {
    associatedtype SomeStep: Step
    var step: SomeStep { get }
}

Everything works just fine, a.step is of StepA, no issues at all...

Question: why is this working? I am basically hiding the fact that step property from Component is of type Step, by using the associated type SomeStep. This is kinda weird, isn't it? I was expecting this to work just fine without having to hide anything, why is this happening? How does it work?

Thanks. Any help would be much appreciated!


Solution

  • Protocol can be adopted by a struct or enum so it don't do class polymorphism, because they can not be inherited. This is also one of the reasons that why associatedtype exists.

    Back to your question, you defined a variable with the same name as the parent protocol in the sub-protocol, but the type is different, this is allowed because the protocol is not a concrete type. But this is not a type overload, because the protocol works differently from the class.

    In this case, you'd better use generics, which is associatedtype. But if you have to use these three protocols, you only need to declare the sub-protocol.

    ...
    
    protocol ComponentA: Component {}
    
    protocol ComponentB: Component {}
    
    struct SomeComponentOfTypeA: ComponentA {
      var step: Step = StepA.one
    }
    
    struct SomeComponentOfTypeB: ComponentB {
      var step: Step = StepB.two
    }
    
    let a = SomeComponentOfTypeA()
    
    print(a.step)
    print(type(of: a))
    
    let b = SomeComponentOfTypeB()
    
    print(b.step)
    print(type(of: b))
    

    Output:

    one
    SomeComponentOfTypeA
    two
    SomeComponentOfTypeB
    

    Another way is to meet your needs. Limit SomeComponentOfTypeA only allows StepA type.

    protocol StepAProtocol { /* body */ }
    protocol StepBProtocol { /* body */ }
    
    enum StepA: Int, CaseIterable, StepAProtocol {
        case one
        case two
        case three
    }
    
    enum StepB: Int, CaseIterable, StepBProtocol {
        case one
        case two
        case three
    }
    
    protocol Component {
      associatedtype T
      var step: T { get }
    }
    
    protocol ComponentA: Component where T: StepAProtocol  {}
    protocol ComponentB: Component where T: StepBProtocol  {}
    
    struct SomeComponentOfTypeA: ComponentA {
      var step: StepA = .one
    }
    
    struct SomeComponentOfTypeB: ComponentB {
      var step: StepB = .two
    }
    
    struct SomeComponentOfTypeC: ComponentB {
      var step: StepA = .two // error: Type 'StepA' does not conform to protocol 'StepBProtocol'
    }