I'm new to swift and is confused by how swift compiler treats the opaque type declarations for a variable.
For example, in the following minimum demo code:
protocol Animal {}
class Dog: Animal {}
func getMeADog() -> some Animal {
return Dog()
}
var animal: some Animal = getMeADog()
@MainActor
func changeADog() {
animal = getMeADog()
}
According to my understanding of swift's documentation of opaque types, opaque types (in this example some Animal
) is a "wrapping" of a specific, compile-time inferred type which is known to the compiler.
If so, the compiler should've known that the animal is of type Dog since getMeADog()
always return a Dog
, causing animal
to have underlying type Dog
Then, when I try to change a dog, which assigns another Dog
(represented as some Animal
) to the animal
, the compiler should notice that they have the same underlying type (Dog
), and complain nothing
However, the compiler DID complain about the type mismatch: Cannot assign value of type 'some Animal' (result of 'getMeADog()') to type 'some Animal' (type of 'animal')
What's more interesting is, when I remove the some Animal
declaration for the animal
variable, and let the compiler does type inference for me (which also produces a some Animal
type shown in the IDE), the compiler NO LONGER complain on animal = getMeADog()
line
Why is swift compiler behaving like this? What's the underlying difference between me specifying explicitly the type of the variable and letting the compiler do it for me?
The meaning of "a type known to the compiler" here is not "the compiler knows it is Dog
". It just means that the compiler knows that it is a concrete type.
You can think of some Animal
as a fresh type variable that is a concrete type. The return type of getMeADog
is not Dog
, but a type that the compiler has "made up", just to represent the return type of getMeADog
. Let's call this type TheReturnTypeOfGetMeADog
.
This is different from declaring the return type as any Animal
, where the concrete type is unknown. With some Animal
, the compiler knows that getMeADog
will return the concrete type of TheReturnTypeOfGetMeADog
every time. This allows you to do things that you can only do with a concrete type, e.g.
struct Container<T: Animal> {
var animal: T
}
func f() {
Container(animal: getMeADog())
}
If getMeADog
had returned an any Animal
instead, the above would have produced an error, because Container<any Animal>
is not valid. With some Animal
, the compiler knows that TheReturnTypeOfGetMeADog
is a concrete type, and so Container<TheReturnTypeOfGetMeADog>
is a valid type.
The reason why you are able to do:
var animal = getMeADog()
@MainActor
func changeADog() {
animal = getMeADog()
}
Because the compiler infers that the type of animal
must be TheReturnTypeOfGetMeADog
. The return type of getMeADog
is also TheReturnTypeOfGetMeADog
, so the assignment in changeADog
is valid.
On the other hand, if you write
var animal: some Animal = getMeADog()
then the type of animal
is not TheReturnTypeOfGetMeADog
. As I said before, some Animal
is a fresh type variable. The type of animal
is another type that the compiler has "made up", just to represent the type of the animal
property. As far as the compiler is concerned, the type of animal
is a different concrete type from TheReturnTypeOfGetMeADog
. Note that these are types that the compiler has "made up" - at runtime, animal
will of course just contain a Dog
.
The point of some
types is that you can change the underlying implementation without breaking existing code. Changing the implementation of getMeADog
to return a Cat
should not break anything.
If this were allowed:
var animal: some Animal = getMeADog()
@MainActor
func changeADog() {
animal = getMeADog()
}
The changing the initialiser of animal
to, e.g. = Cat()
would make the assignment animal = getMeADog()
invalid.