[swift-evolution] [Proposal] Protected Access Level

Charlie Monroe charlie at charliemonroe.net
Sun May 29 04:23:56 CDT 2016


I agree - both will need to be addressed in the future to make the language complete. There are some valid points, however, in the blog post - would you allow extensions to access protected variables and potentially expose them to other classes? I.e:

/// Class in module A:
class View {
	protected func layoutSubviews()
}

/// In module B:
extension View {
	func doLayoutSubviews() {
		self.layoutSubviews()
	}
}

Or would you disallow access from extensions to prevent this kind of abuse?

> On May 29, 2016, at 11:19 AM, Goffredo Marocchi <panajev at gmail.com> wrote:
> 
> Without wither abstract classes or a protected access modifier, the status quo, that kind of ExceptionThisMethodShouldBeOverridden are really ugly bad code there is no alternative to beyond a religious stop using classes and sub classing... did you not know about your pop saviour ;)? 
> 
> Sent from my iPhone
> 
>> On 29 May 2016, at 08:38, Charlie Monroe via swift-evolution <swift-evolution at swift.org> wrote:
>> 
>> Ditto - I would love to be able to disallow non-subclasses accessing/modifying some variables.
>> 
>> Though, I'm not sure what would be the stand on this from the core team - according to Apple's blog they've already considered protected access level:
>> 
>> https://developer.apple.com/swift/blog/?id=11
>> 
>> Charlie
>> 
>> 
>>> On May 29, 2016, at 7:56 AM, Riley Testut via swift-evolution <swift-evolution at swift.org> wrote:
>>> 
>>> 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 at 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 at swift.org
>>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>> _______________________________________________
>>> swift-evolution mailing list
>>> swift-evolution at swift.org
>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>> 
>> _______________________________________________
>> swift-evolution mailing list
>> swift-evolution at swift.org
>> https://lists.swift.org/mailman/listinfo/swift-evolution



More information about the swift-evolution mailing list