Sent from my iPad
> On Jul 10, 2016, at 10:38 PM, Jordan Rose via swift-evolution > <[email protected]> wrote: > > [Proposal: > https://github.com/apple/swift-evolution/blob/master/proposals/0117-non-public-subclassable-by-default.md > ] > > (This is my second response to this proposal. The previous message shared a > use case where public-but-non-subclassable made things work out much better > with required initializers. This one has a bit more ideology in it.) > > As many people have said already, this proposal is quite beneficial to > library designers attempting to reason about their code, not just now but in > the future as well. The model laid out in the Library Evolution document > (often referred to as “resilience”) supports Swift libraries that want to > preserve a stable binary and source interface. > > In the Swift 2 model (and what’s currently described in that document), a > public class must be final or non-final at the time it is published. It’s > clearly not safe to add ‘final' in a later version of the library, because a > client might already have a subclass; it’s also not safe to remove ‘final’ > because existing clients may have been compiled assuming there are no > subclasses. > > (Of course, we can remove this optimization, and make ‘final’ a semantic > contract only. I’m deliberately avoiding most discussion of performance, but > in this parenthetical I’ll note that Swift makes it possible to write code > that is slower than Objective-C. This is considered acceptable because the > compiler can often optimize it for a particular call site. For those who want > more information about the current implementation of some of Swift’s > features, I suggest watching the “Understanding Swift Performance” talk from > this year’s WWDC.) > > With this proposal, a public class can be non-publicly-subclassable or > publicly-subclassable. Once a class is publicly-subclassable (“open”), you > can’t go back, of course. But a class that’s not initially open could become > open in a future release of the library. All existing clients would already > be equipped to deal with this, because there might be subclasses inside the > library. On the other hand, the class can also be marked ‘final’, if the > library author later realizes there will never be any subclasses and that > both client authors and the compiler should know this. > > One point that’s not covered in this proposal is whether making a class > ‘open’ applies retroactively, i.e. if MagicLib 1.2 is the first version that > makes the Magician class ‘open’, can clients deploy back to MagicLib 1.0 and > expect their subclasses to work? My inclination is to say no; if it’s > possible for a non-open method to be overridden in the future, a library > author has to write their library as if it will be overridden now, and > there’s no point in making it non-open in the first place. That would make > ‘open’ a “versioned attribute” in the terminology of Library Evolution, > whatever the syntax ends up being. > > --- > > Okay, so why is this important? > > It all comes down to reasoning about your program’s behavior. When you use a > class, you’re relying on the documented behavior of that class. More > concretely, the methods on the class have preconditions > (“performSegue(withIdentifier:sender:) should not be called on a view > controller that didn’t come from a storyboard”) and postconditions (“after > calling loadViewIfNeeded(), the view controller’s view will be loaded”). When > you call a method, you’re responsible for satisfying its preconditions so it > can deliver on the postconditions. > > I used UIViewController as an example, but it applies just as much to your > own methods. When you call a method in your own module—maybe written by you, > maybe by a coworker, maybe by an open source contributor—you’re expecting > some particular behavior and output given the inputs and the current state of > the program. That is, you just need to satisfy its preconditions so it can > deliver on the postconditions. If it’s a method in your module, though, you > might not have taken the trouble to formalize the preconditions and > postconditions, since you can just go look at the implementation. Even if > your expectations are violated, you’ll probably notice, because the conflict > of understanding is within your own module. > > Public overriding changes all this. While an overridable method may have > particular preconditions and postconditions, it’s possible that the overrider > will get that wrong, which means the library author can no longer reason > about the behavior of their program. If they do a poor job documenting the > preconditions and postconditions, the client and the library will almost > certainly disagree about the expected behavior of a particular method, and > the program won’t work correctly. > > "Doesn’t a library author have to figure out the preconditions and > postconditions for a method anyway when making it public?" Well, not to the > same extent. It’s perfectly acceptable for a library author to document > stronger preconditions and weaker postconditions than are strictly necessary. > (Maybe 'performSegue(withIdentifier:sender:)’ has a mode that can work > without storyboards, but UIKit isn’t promising that it will work.) When a > library author lets people override their method, though, they're promising > that the method will never be called with a weaker precondition than > documented, and that nothing within their library will expect a stronger > postcondition than documented. > > (By the way, the way to look at overriding a method is the inverse of calling > a method: you need to deliver on the postconditions, and you can assume the > caller has satisfied the preconditions. If your understanding of those > preconditions and postconditions is wrong, your program won’t work correctly, > just like when you’re calling a method.) > > This all goes double when a library author wants to release a new version of > their library with different behavior. In order to make sure existing callers > don’t break, they have to make sure all of the library’s documented > preconditions are no stronger and postconditions are no weaker for public > API. In order to make sure existing subclassers don’t break, they have to > make sure all of the library’s documented preconditions are no weaker and > postconditions are no stronger for overridable API. > > (For a very concrete example of this, say you’re calling a method with the > type '(Int?) -> Int’, and you’re passing nil. The new version of the library > can’t decide to make the parameter non-optional or the return value optional, > because that would break your code. Similarly, if you’re overriding a method > with the type ‘(Int) -> Int?’, and returning nil, the new version of the > library can’t decide to make the parameter optional or the return value > non-optional, because that would break your code.) > > So, "non-publicly-subclassable" is a way to ease the burden on a library > author. They should be thinking about preconditions and postconditions in > their program anyway, but not having to worry about all the things a client > might do for a method that shouldn’t be overridden means they can actually > reason about the behavior—and thus the correctness—of their own program, both > now and for future releases. > > --- > > I agree with several people on this thread that > non-publicly-subclassable-by-default is the same idea as internal-by-default: > it means that you have to explicitly decide to support a capability before > clients can start relying on it, and you are very unlikely to do so by > accident. The default is “safe” in that a library author can change their > mind without breaking existing clients. > > I agree with John that even today, the entry points that happen to be public > in the types that happen to be public classes are unlikely to be good entry > points for fixing bugs in someone else's library. Disallowing overriding > these particular entry points when a client already can't override internal > methods, methods on structs, methods that use internal types, or top-level > functions doesn’t really seem like a loss to me. > > Library design is important. Controlling the public interface of a library > allows for better reasoning about the behavior of code, better security (i.e. > better protection of user data), and better maintainability. And whether > something can be overridden is part of that interface. > > Thanks again to Javier and John for putting this proposal together. > Jordan Thanks for this really excellent, detailed analysis of the rationale for making sealed the default Jordan! > > P.S. There’s also an argument to be made for public-but-not-conformable > protocols, i.e. protocols that can be used in generics and as values outside > of a module, but cannot be conformed to. This is important for many of the > same reasons as it is for classes, and we’ve gotten a few requests for it. > (While you can get a similar effect using an enum, that’s a little less > natural for code reuse via protocol extensions.) FYI - I have been planning to propose exactly this feature for protocols once we get past the Swift 3 additive feature freeze. > > P.P.S. For those who will argue against “better security”, you’re correct: > this doesn’t prevent an attack, and I don’t have much expertise in this area. > However, I have talked to developers distributing binary frameworks (despite > our warnings that it isn’t supported) who have asked us for various features > to keep it from being easy. > _______________________________________________ > swift-evolution mailing list > [email protected] > https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________ swift-evolution mailing list [email protected] https://lists.swift.org/mailman/listinfo/swift-evolution
