Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift: Overriding Self-requirement is allowed, but causes runtime error. Why?

I just started to learn Swift (v. 2.x) because I'm curious how the new features play out, especially the protocols with Self-requirements.

The following example is going to compile just fine, but causes arbitrary runtime effects to happen:

// The protocol with Self requirement
protocol Narcissistic {
    func getFriend() -> Self
}


// Base class that adopts the protocol
class Mario : Narcissistic  {
    func getFriend() -> Self {
        print("Mario.getFriend()")
        return self;
    }
}


// Intermediate class that eliminates the
// Self requirement by specifying an explicit type
// (Why does the compiler allow this?)
class SuperMario : Mario {
    override func getFriend() -> SuperMario {
        print("SuperMario.getFriend()")
        return SuperMario();
    }
}

// Most specific class that defines a field whose
// (polymorphic) access will cause the world to explode
class FireFlowerMario : SuperMario {
    let fireballCount = 42
    
    func throwFireballs() {
        print("Throwing " + String(fireballCount) + " fireballs!")
    }
}


// Global generic function restricted to the protocol
func queryFriend<T : Narcissistic>(narcissistic: T) -> T {
    return narcissistic.getFriend()
}


// Sample client code

// Instantiate the most specific class
let m = FireFlowerMario()

// The call to the generic function is verified to return
// the same type that went in -- 'FireFlowerMario' in this case.
// But in reality, the method returns a 'SuperMario' and the
// call to 'throwFireballs' will cause arbitrary
// things to happen at runtime.
queryFriend(m).throwFireballs()

You can see the example in action on the IBM Swift Sandbox here. In my browser, the output is as follows:

SuperMario.getFriend()
Throwing 32 fireballs!

(instead of 42! Or rather, 'instead of a runtime exception', as this method is not even defined on the object it is called on.)

Is this a proof that Swift is currently not type-safe?

EDIT #1:

Unpredictable behavior like this has to be unacceptable. The true question is, what exact meaning the keyword Self (capital first letter) has. I couldn't find anything online, but there are at least these two possibilities:

  • Self is simply a syntactic shortcut for the full class name it appears in, and it could be substituted with the latter without any change in meaning. But then, it cannot have the same meaning as when it appears inside a protocol definition.

  • Self is a sort of generic/associated type (in both protocols and classes) that gets re-instantiated in deriving/adopting classes. If that is the case, the compiler should have refused the override of getFriend in SuperMario.

Maybe the true definition is neither of those. Would be great if someone with more experience with the language could shed some light on the topic.

like image 335
domin Avatar asked Feb 08 '16 12:02

domin


People also ask

Can we override method in extension?

Extensions cannot/should not override. It is not possible to override functionality (like properties or methods) in extensions as documented in Apple's Swift Guide. Extensions can add new functionality to a type, but they cannot override existing functionality.

Can we override class method in Swift?

In Swift Inheritance, the subclass inherits the methods and properties of the superclass. This allows subclasses to directly access the superclass members. Now, if the same method is defined in both the superclass and the subclass, then the method of the subclass class overrides the method of the superclass.

How do you override property in Swift?

In swift to override inherited properties or methods in subclass we use override keyword which will tells the compiler to check the overriding method or property definition matches with the base class or not.


1 Answers

Yes, there seems to be a contradiction. The Self keyword, when used as a return type, apparently means 'self as an instance of Self'. For example, given this protocol

protocol ReturnsReceived {

    /// Returns other.
    func doReturn(other: Self) -> Self
}

we can't implement it as follows

class Return: ReturnsReceived {

    func doReturn(other: Return) -> Self {
        return other    // Error
    }
}

because we get a compiler error ("Cannot convert return expression of type 'Return' to return type 'Self'"), which disappears if we violate doReturn()'s contract and return self instead of other. And we can't write

class Return: ReturnsReceived {

    func doReturn(other: Return) -> Return {    // Error
        return other
    }
}

because this is only allowed in a final class, even if Swift supports covariant return types. (The following actually compiles.)

final class Return: ReturnsReceived {

    func doReturn(other: Return) -> Return {
        return other
    }
}

On the other hand, as you pointed out, a subclass of Return can 'override' the Self requirement and merrily honor the contract of ReturnsReceived, as if Self were a simple placeholder for the conforming class' name.

class SubReturn: Return {

    override func doReturn(other: Return) -> SubReturn {
        // Of course this crashes if other is not a
        // SubReturn instance, but let's ignore this
        // problem for now.
        return other as! SubReturn
    }
}

I could be wrong, but I think that:

  • if Self as a return type really means 'self as an instance of Self', the compiler should not accept this kind of Self requirement overriding, because it makes it possible to return instances which are not self; otherwise,

  • if Self as a return type must be simply a placeholder with no further implications, then in our example the compiler should already allow overriding the Self requirement in the Return class.

That said, and here any choice about the precise semantics of Self is not bound to change things, your code illustrates one of those cases where the compiler can easily be fooled, and the best it can do is generate code to defer checks to run-time. In this case, the checks that should be delegated to the runtime have to do with casting, and in my opinion one interesting aspect revealed by your examples is that at a particular spot Swift seems not to delegate anything, hence the inevitable crash is more dramatic than it ought to be.

Swift is able to check casts at run-time. Let's consider the following code.

let sm = SuperMario()
let ffm = sm as! FireFlowerMario
ffm.throwFireballs()

Here we create a SuperMario and downcast it to FireFlowerMario. These two classes are not unrelated, and we are assuring the compiler (as!) that we know what we are doing, so the compiler leaves it as it is and compiles the second and third lines without a hitch. However, the program fails at run-time, complaining that it

Could not cast value of type
'SomeModule.SuperMario' (0x...) to
'SomeModule.FireFlowerMario' (0x...).

when trying the cast in the second line. This is not wrong or surprising behaviour. Java, for example, would do exactly the same: compile the code, and fail at run-time with a ClassCastException. The important thing is that the application reliably crashes at run-time.

Your code is a more elaborate way to fool the compiler, but it boils down to the same problem: there is a SuperMario instead of a FireFlowerMario. The difference is that in your case we don't get a gentle "could not cast" message but, in a real Xcode project, an abrupt and terrific error when calling throwFireballs().

In the same situation, Java fails (at run-time) with the same error we saw above (a ClassCastException), which means it attempts a cast (to FireFlowerMario) before calling throwFireballs() on the object returned by queryFriend(). The presence of an explicit checkcast instruction in the bytecode easily confirms this.

Swift on the contrary, as far as I can see at the moment, does not try any cast before the call (no casting routine is called in the compiled code), so a horrible, uncaught error is the only possible outcome. If, instead, your code produced a run-time "could not cast" error message, or something as gracious as that, I would be completely satisfied with the behaviour of the language.

like image 169
Alberto Doda Avatar answered Oct 20 '22 11:10

Alberto Doda