Asked  6 Months ago    Answers:  5   Viewed   28 times

Why does the following code produce an error?

protocol ProtocolA {
    var someProperty: ProtocolB { get }
}

protocol ProtocolB {}
class ConformsToB: ProtocolB {}

class SomeClass: ProtocolA { // Type 'SomeClass' does not conform to protocol 'ProtocolA'
    var someProperty: ConformsToB

    init(someProperty: ConformsToB) {
        self.someProperty = someProperty
    }
}

The answer in this similar question makes sense. However, in my example, the property is get-only. Why shouldn't this work? Is it a shortcoming of Swift, or is there some reason this makes sense?

 Answers

75

There's no real reason why this shouldn't be possible, a read-only property requirement can be covariant, as returning a ConformsToB instance from a property typed as ProtocolB is perfectly legal.

Swift just currently doesn't support it. In order to do so, the compiler would have to generate a thunk between the protocol witness table and conforming implementation in order to perform the necessary type-conversion(s). For example, a ConformsToB instance would need to be boxed in an existential container in order to be typed as ProtocolB (and there's no way the caller can do this, as it might not know anything about the implementation being called).

But again, there's no reason why the compiler shouldn't be able to do this. There are multiple bug reports open over this, this one which is specific to read-only property requirements, and this general one, in which Slava Pestov, a member of the Swift team, says:

[...] we want protocol witnesses and method overrides in every case where a function conversion is allowed

So it definitely looks like something the Swift team are looking to implement in a future version of the language.

In the mean time however, as @BallpointBen says, one workaround is to use an associatedtype:

protocol ProtocolA {
    // allow the conforming type to satisfy this with a concrete type
    // that conforms to ProtocolB.
    associatedtype SomeProperty : ProtocolB
    var someProperty: SomeProperty { get }
}

protocol ProtocolB {}
class ConformsToB: ProtocolB {}

class SomeClass: ProtocolA {

    // implicitly satisfy the associatedtype with ConformsToB.
    var someProperty: ConformsToB

    init(someProperty: ConformsToB) {
        self.someProperty = someProperty
    }
}

But this is quite unsatisfactory, as it means that ProtocolA is no longer usable as a type (because it has associatedtype requirements). It also changes what the protocol says. Originally it said that someProperty could return anything that conformed to ProtocolB – now it says that an implementation of someProperty deals with just one specific concrete type that conforms to ProtocolB.

Another workaround is just to define a dummy property in order to satisfy the protocol requirement:

protocol ProtocolA {
    var someProperty: ProtocolB { get }
}

protocol ProtocolB {}
class ConformsToB: ProtocolB {}

class SomeClass: ProtocolA {

    // dummy property to satisfy protocol conformance.
    var someProperty: ProtocolB {
        return actualSomeProperty
    }

    // the *actual* implementation of someProperty.
    var actualSomeProperty: ConformsToB

    init(someProperty: ConformsToB) {
        self.actualSomeProperty = someProperty
    }
}

Here we're essentially writing the thunk for the compiler – but it's also not particularly nice as it adds a needless property to the API.

Tuesday, June 1, 2021
 
Valdas
answered 6 Months ago
42

Because the writer of Baseclass has explicitly declared that Bar has to be a read-only property. It doesn't make sense for derivations to break this contract and make it read-write.

I'm with Microsoft on this one.
Let's say I'm a new programmer who has been told to code against the Baseclass derivation. i write something that assumes that Bar cannot be written to (since the Baseclass explicitly states that it is a get only property). Now with your derivation, my code may break. e.g.

public class BarProvider
{ BaseClass _source;
  Bar _currentBar;

  public void setSource(BaseClass b)
  {
    _source = b;
    _currentBar = b.Bar;
  }

  public Bar getBar()
  { return _currentBar;  }
}

Since Bar cannot be set as per the BaseClass interface, BarProvider assumes that caching is a safe thing to do - Since Bar cannot be modified. But if set was possible in a derivation, this class could be serving stale values if someone modified the _source object's Bar property externally. The point being 'Be Open, avoid doing sneaky things and surprising people'

Update: Ilya Ryzhenkov asks 'Why don't interfaces play by the same rules then?' Hmm.. this gets muddier as I think about it.
An interface is a contract that says 'expect an implementation to have a read property named Bar.' Personally I'm much less likely to make that assumption of read-only if I saw an Interface. When i see a get-only property on an interface, I read it as 'Any implementation would expose this attribute Bar'... on a base-class it clicks as 'Bar is a read-only property'. Of course technically you're not breaking the contract.. you're doing more. So you're right in a sense.. I'd close by saying 'make it as hard as possible for misunderstandings to crop up'.

Thursday, June 3, 2021
 
edsk
answered 6 Months ago
12

You cannot implement a read-write property requirement of type BasePresenterProtocol? with a property of type DashboardPresenterProtocol?.

Consider what would happen if this were possible, and you upcast an instance of DashboardPresenter to DashboardViewProtocol. You would be able to assign anything that conforms to BasePresenterProtocol to a property of type DashboardPresenterProtocol? – which would be illegal.

For this reason, a read-write property requirement has to be invariant (although it's worth noting that a readable-only property requirement should be able to be covariant – but this currently isn't supported).

Friday, June 11, 2021
 
tadman
answered 6 Months ago
41

What am I doing wrong to make it a true read only property to where I can't set it?

There is a difference between a protocol (a set of rules) and the type (i.e. your struct) that adopts the protocol.

  • Your protocol rule says that readOnlyProperty should be readable.

  • Your struct obeys by making it readable, and also makes it writable. That is not illegal, so all is well — and readOnlyProperty in your struct is read-write.

What would have been illegal would be the inverse, i.e. for the protocol to declare a property read-write but the adopter to declare it read-only. That situation didn't arise in your example, but if it had, the compiler would have stopped you.

Monday, August 30, 2021
 
Fogmeister
answered 3 Months ago
78

In Swift 3, generate() has been renamed to makeIterator(). Changing the name of your function should fix the problem. (Note that other names have also changed, like AnyGenerator → AnyIterator, but it looks like that one has already been taken care of in your code.)

This change was implemented as part of SE-0006: Apply API Guidelines to the Standard Library.

Tuesday, November 9, 2021
 
Pedro Dusso
answered 3 Weeks ago
Only authorized users can answer the question. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :  
Share