Unless I'm missing something Brent, your suggestions still wouldn't allow the developer to provide a public class in a module designed to be subclassed by clients in another module and access these "private" details, which is a real problem I'm having in my current project.
I have a framework which provides an abstract model object designed to be subclassed (and yes it has to be a class and not a struct for a multitude of reasons ๐) by clients of the framework. There are several convenience methods + properties I have exposed for subclasses to use, but they should really be implementation details; a client using these model objects should not have to know about them. Even worse, several of the properties are mutable so the subclasses can modify them, but they certainly should *not* be modified by anything else. Right now, I'm limited to simply commenting something akin to "DO NOT CALL" next to these methods/properties, which definitely goes against Swift's safety focus. For these reasons, I'm 100% in support of a protected access control modifier. > On May 28, 2016, at 8:11 PM, Brent Royal-Gordon via swift-evolution > <swift-evolution@swift.org> wrote: > > To begin with, I'm not a fan of `protected` access. But even leaving that > aside, I have a few questions and critiques. > >> A common case is the UIView from UIKit. Many developers are tempted to make >> this call: >> >> view.layoutSubviews() >> The documentation says: "You should not call this method directly. If you >> want to force a layout update, call the setNeedsLayoutmethod instead to do >> so prior to the next drawing update. If you want to update the layout of >> your views immediately, call the layoutIfNeeded method." > > This example is illuminating in several ways. > > * The rule is not simply that "only the class should call > `layoutSubviews()`"; it is effectively "*you* should never call > `layoutSubviews()` except when `super`ing up from your override". Calling > `layoutSubviews()` from `insertRows(at:)` is just as much a mistake if > `insertRows(at:)` is part of the class as if it is not. So isn't `protected` > insufficiently strict to properly serve this use case? > > * At the same time, something outside `layoutSubviews()` has to be able to > call `layoutSubviews()`. In the case of UIKit, though, that "something" is > always within UIKit itself, never outside it. So should `protected` have a > "bottom", a level below which calls are unrestricted? For instance, in > UIKit's case you might have `protected fileprivate`, meaning "anything up to > `fileprivate` has unrestricted use; anything above that can override and > `super` up from its override, but not use it any other way". > > protected fileprivate func layoutSubviews() > > * `layoutSubviews()` is also something you should probably always `super` up > to. Have you considered addressing `super` requirements at all? > > In short, is a traditional `protected` really the feature you want to handle > this use case, or would a very different design actually suit it a lot better? > >> When declarated by a class the protected member will be visible to the class >> itself and all the derived classes. > > In what scope? The same as the class? > > Is there not room for, for instance, "usable without restriction in this > file, override-only in the rest of this module, invisible outside it"? For > instance, `internal(protected) fileprivate`, or perhaps `internal(override) > fileprivate`? `layoutSubviews()` might then be `public(override) > fileprivate`โthe ability to override is public, the ability to use it > unrestricted is filewide. > > public(override) fileprivate func layoutSubviews() > internal(override) fileprivate func privateSubclassingHook() > >> public protected(set) var x = 20 > > Of course, that might be difficult to combine with the `(set)` syntax. > `public(set: override)`, maybe? With, for instance, `public internal(set: > override) private(set)` if you want the property's getter public and its > setter overridable internally and callable in private scope. > > public(override) fileprivate func layoutSubviews() > internal(override) fileprivate func privateSubclassingHook() > public(get, set: override) internal(set) var x = 20 > > But there's something about this that's starting to seem a little rotten. I > think the problem is that we're not really trying to widen the ability to > override, we're trying to restrict the ability to call. Let's try > restructuring along those lines: > > public fileprivate(call) func layoutSubviews() > internal fileprivate(call) func privateSubclassingHook() > public internal(set: call) var x = 20 > > That seems much cleaner to me. > >> If the member is declared as final then it will be visible but not can be >> overrided by the derived classes. Just like it works with other access >> levels. > > With the "overridable but otherwise unusable" conception I'm suggesting, this > would not be the case, of course. > >> Protocols >> >> Protocols do not declare access level for their members. So the protected >> access level is not applicable here. > > But `protected` is quite different from other access levels; it does not > limit the visibility of the symbols, but rather their use. And protocols face > the same sort of problem as classes, where certain members are essentially > override hooks and shouldn't be called directly outside a particular scope. > > So I think we ought to allow `accesslevel(call)`, but not a plain > `accesslevel`: > > public fileprivate(call) func layoutSubviews() > internal fileprivate(call) func privateSubclassingHook() > public internal(set: call) var x = 20 > internal(call) func protocolConformanceHook() > fileprivate(set: call) var onlyProtocolSetsThis: Int { get set } > >> Extensions >> >> Extensions will not be able do be protected nor their members. > > This is very vague. There are several things extensions might try to do with > protected members: > > * Declare new ones > * Override existing ones > * Call existing ones > > Which of these, if any, are permitted? Why? > > In my conception, I would permit extensions to behave as the type they > extended did. Extensions could declare new members with restricted calling > and override existing ones. They would not be able to call, except when > supering from an override, unless they were within scope of the `call` access > control. In other words, they'd behave just like any other code at that > location. That's how we want extensions to work. > >> But nested declarations will be allowed, so this code will compile: >> >> // We can declare a protected class (or struct, enum, etc.) if >> // and only if they are nested inside other type. >> public class MyPublicClass { >> protected >> class MyProtectedClass { > > What does it mean to "use" a protected class, though? Clearly you can call > its methods, if only through AnyObject or a non-protected superclass or a > protocol it conforms to. Does it mean you can't instantiate it? Does it mean > you can't subclass it? Does it mean you can't call methods that aren't on its > supertypes? All of the above? None? > > One more thing that didn't come up: Testability. I believe that importing a > module with `@testable` should disable its call restrictions, even ones > inherited from outside that module. Thus, even if *you* cannot call your > `layoutSubviews()`, your test suite can. > > So, in short, my counter-proposal is: > > public fileprivate(call) func layoutSubviews() > internal fileprivate(call) func privateSubclassingHook() > public internal(set: call) var x = 20 > internal(call) func protocolConformanceHook() > fileprivate(set: call) var onlyProtocolSetsThis: Int { get set } > > In other words: > > * There is a new aspect of the member, `call`, which controls the ability to > actually call the member, as opposed to overriding it. No `call`, no calling > (except when `super`ing up from an override). > > * `call` is used in combination with one of the existing access modifiers: > `public(call)` `internal(call)` `fileprivate(call)` `private(call)`. `call`'s > visibility is always less or equal to the member itself. > > * To control the callability of a setter independently from both the getter > and the overridability of the setter, use `set: call`. > > * Extensions behave just like type definitions at the same location with > regards to `call`. > > * Protocols can use access modifiers with `call` to prevent unauthorized code > from calling a member. The access control level to implement a member > continues to be as wide as the access control level of the protocol itself. > > * `@testable` disables `call` restrictions on the types it imports, so the > test suite can call any visible member, even ones inherited from other > modules. > > * There should probably also be some sort of "super required" warning/error, > but this is an orthogonal feature and can be left for a separate proposal. > > I think that feature will be closer to the one you actually *want*, as > opposed to the one that other languages have cargo-culted from SIMULA-67. > > -- > Brent Royal-Gordon > Architechies > > _______________________________________________ > swift-evolution mailing list > swift-evolution@swift.org > https://lists.swift.org/mailman/listinfo/swift-evolution _______________________________________________ swift-evolution mailing list swift-evolution@swift.org https://lists.swift.org/mailman/listinfo/swift-evolution