Is there any syntax can make this work? I need a property can determine its type in the compile time.
protocol P {}
struct A: P {
var onlyAHas: String
}
struct B: P {
var onlyBHas: String
}
var ins1: any P = A()
var ins2: any P = B()
ins1.onlyAHas = "a only"
ins2.onlyBHas = "b only"
Before getting to the solution, let's break down what
anymeans, and while we're at it, we'll includesomeas well:When you write:
You are telling the compiler that you want to use
ins1asP. It's the protocol oriented equivalent of this OOP code:This code tells the compiler that
objis aBase. You can assign it from aConcrete, but because that's a subclass ofBase, butobjis still known locally as aBasenot as aConcrete, so it can't access the properties ofConcretethat weren't inherited fromBase.It's the same in your example.
ins1is known locally as aPnot as anA, andPdoesn't have anonlyAHasproperty.You'd get similar behavior with
someinstead ofany. There are a few differences between the two, but let's just talk about the main one:sometells the compiler that it will be a type that it can resolve to one specific concrete type, but that it should enforce the abstraction to the protocol in source code. This allows it to generate more efficient code internally, because knowing the concrete type allows the compiler to call the concrete's implementation directly instead of going through its protocol witness table, which is the protocol-oriented analog of a "vtable" in OOP, so the effect is like in OOP when the compiler devirtualizes a method call because despite the syntax, it knows the actual concrete type. This avoids the runtime overhead of dynamic dispatch while still letting you use the abstraction of the existential type... well it's more like it requires you to use the abstraction of the existential type than lets you, because from a source code point of view, the abstraction is enforced.anyalso enforces the abstraction, but it goes the other way in terms of the kind of optimizations the compiler can do. It says that the compiler must go through the protocol witness table, because, as the keyword suggests, its value could be any concrete type that conforms to the protocol, even if the compiler could determine that it's actually just one specific type locally. It also allows relaxation of some rules regarding using the protocol as a type when it hasSelfandassociatedtypeconstraints.But either way, you are telling the compiler that you want to use
ins1as aPand not as anA.The solutions
There are a few solutions, actually:
Downcasting
The first is to downcast to the concrete type, as was suggested in comments by Joakim Danielson:
Downcasting is a code smell, but sometimes is actually the clearest or simplest solution. As long as it's contained locally, and doesn't become a wide-spread practice for using instances of type,
P, it might be fine.However, that example does have one problem: A is a value type, so the
ins1whoseonlyAHasproperty is being set is a copy of the originalins1you explicitly created. Having the same name confuses it slightly. If you only need the change to be in effect in the body of theif, that works just fine. If you need it to persist outside, you'd have to assign back to the original. Using the same name prevents that, so you'd need to use different names.Execute concrete-specific code only at initialization
This only applies if the concrete type just configures some things for the protocol up-front, and thereafter protocol-only code can be used:
Or your could delegate the initialization to a function that internally knows the concrete type, but returns
any P.Declare protocol methods/computed properties that do the work.
This is the usual way to use protocols. Declaring a method in the protocol is similar to declaring a method in a base class. Implementing the method in a conforming concrete type is like overriding the method in a subclass. If you don't also provide a default implementation in a protocol extension, the protocol will enforce that conforming types implement the protocol - which is a big advantage over the OOP approach.
I'm doing this with a
setStringmethod, but you could certainly use a computed variable in the protocol to do the same thing, and that would be more "Swifty." I didn't do that just to emphasize the more general idea of putting functionality in the protocol, and not get hung up on the fact that the functionality in question happens to be setting a property.If you don't need all conforming types to be able to set a String, one solution is to provide a do-nothing default implmentation in an extension on P:
Most often though, setting/getting some concrete property is an implementation detail of doing some task that varies with the concrete type. So instead, you'd declare a method in the protocol to do that task:
Bwould be defined similarly doing whatever is specific to it. If the stuff in comments is common code, you could break it down into prologue, main action, and epilogue.In this example, the
frobnicatein the protocol extension is called, because it's defined only in the protocol extension.For
ins1,frobnicatethen calls the extension'sprepareToFrobnicate, because even though it's declared directly in the protocol,Adoesn't implement that and a default implementation is provided in the extension.Then it calls
A'sactuallyFrobnicatebecause it's defined directly in the protocol, andAdoes implement it, so the default implementation isn't used. As a result theonlyAHasproperty is set.Then it passes the result from
A'sactuallyFrobnicateto thefinishFrobnicationin the extension, because it's defined directly in the protocol, butAdoesn't implement it, and the extension provides a default implementation.For
ins2,frobnicatestill calls the defaultprepareToFrobnicate, and then call'sB's implementation ofactuallyFrobnicate, butB's implementation doesn't set itsonlyBHasproperty there. Instead, it just returns a string, whichfrobnicatepasses tofinishFrobnication, which callsB's implementation, because unlikeA,Bprovides its own implementation, and that's whereBsets it.Using this approach, you can simultaneously standardize the general algorithm of a task like
frobnicate, while allowing for dramatically different implementation behavior. Of course, in this case, bothAandBjust set a property in their respective concrete types, but they do it at different phases of the algorithm, and you can imagine adding other code, so that the two effects really would be very different.The point of this approach is that when we call
inst1.frobnicate(), it doesn't know or care about exactly whatinst1is doing internally do accomplish it. The fact that it internally sets theonlyAHasproperty in the concrete type is an implementation detail the calling code doesn't need to be concerned with.Just use the concrete type
In your code example, you are creating and using
ins1, andins2in the same context. So they could just as easily be defined like this:If you have some function,
mungethat you want to do on bothAandB, you can define it terms of the protocol.If
mungeneeds to do things that depend on concrete-specific properties or methods, you can use one of the previously described approaches...OR...
If you know for sure that you only will ever have a small number of concrete types conforming to
P, which admittedly is sometimes impossible to really know, but occasionally you do, then you can just write specialized overloaded versions ofmungefor each concrete type:This kind of regresses to older solutions to problems like this. When I say it's an old solution, I'm referring to the fact that even back when the C++ compiler was just a preprocessor that converted C++ source code to C source code which would then be compiled, didn't have templates, and standardization wasn't even on the horizon, it would let you overload functions. You can do that with Swift too, and it's a perfectly valid solution. Sometimes it's even the best solution. More often it leads to code duplication, but it's in your toolbox to use when it's appropriate.