[swift-evolution] [Proposal] Protected Access Level

Brent Royal-Gordon brent at architechies.com
Sun May 29 05:30:47 CDT 2016


> 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.

That's a little bit of a different use case than the `layoutSubviews()` case discussed in the proposal.

My answer is this: There is nothing magical about being a subclass that ought to grant access to those methods. For instance, if your subclass grows very complicated and you extract a helper object, it's perfectly reasonable for that helper object to want to access the "subclass-only" API. Contrarily, simple subclasses might not need that API, and exposing it to them would be an unnecessary risk. And there are things which you don't subclass at all which could benefit from being hidden away—think of the Objective-C runtime, which has some parts which every app needs (like the definition of `BOOL`) and other parts which are extraordinarily dangerous and should only be available to code which needs it (like `method_exchangeImplementations`).

The Objective-C solution—using a separate header file—actually acknowledges this fact. Even though the header is called "UIGestureRecognizerSubclass.h", it is not really limited to subclasses; any code can import and use that API. It's just sectioned off *by default*, like keeping all the kitchen knives in a sharps drawer. And the Objective-C runtime, which doesn't contain (many) classes, can use this approach too: <objc/objc.h> is implicitly available, while <objc/runtime.h> is something you have to ask for explicitly.

There are a few ways you could bring this same "sharps drawer" approach to Swift. For instance—without adding any language features—you could create an ancillary struct which merely serves to segregate all the dangerous APIs:

	public class UIGestureRecognizer {
		public private(set) var state: UIGestureRecognizerState {...}
		
		private func ignoreTouch(touch: UITouch, forEvent event: UIEvent) {...}
		private func reset() {...}
		
		// etc. for the other APIs
		
		/// Contains methods and properties which directly affect the state of the gesture recognizer.
		///
		/// -Warning: Only use the state engine when implementing a custom gesture recognizer yourself.
		/// 		The state engine is delicate and modifying behind a gesture recognizer's back is likely to 
		///		break it.
		public var stateEngine: StateEngine { return StateEngine(gestureRecognizer: self) }
		
		public struct StateEngine {
			private var gestureRecognizer: UIGestureRecognizer
			
			public var state: UIGestureRecognizerState {
				get { return gestureRecognizer.state }
				nonmutating set { gestureRecognizer.state = newValue }
			}
		
			public func ignoreTouch(touch: UITouch, forEvent event: UIEvent) {
				gestureRecognizer.ignoreTouch(touch, forEvent: event)
			}
			
			public func reset() {
				gestureRecognizer.reset()
			}

			/// etc. for the other APIs
		}
	}

Now ordinary clients of UIGestureRecognizer won't see a bunch of random methods strewn around with doc comments warning not to use them; they'll see *one* property with an intimidating name and a scary comment. You could even give the property a name like `internals` to make it clearer that you shouldn't be touching this unless you know what you're doing. On the other hand, any code that needs to *can* access these features, whether or not that code happens to be located in a subclass of the class in question.

Obviously, this approach could benefit from formalization; there are a number of ways that might be done. For instance, you could create a sort of namespace within a class which functions the same way as the `StateEngine` struct and `stateEngine` property in the last example, but without the boilerplate:

	public class UIGestureRecognizer {
		/// Contains methods and properties which directly affect the state of the gesture recognizer.
		///
		/// -Warning: Only use the state engine when implementing a custom gesture recognizer yourself.
		/// 		The state engine is delicate and modifying behind a gesture recognizer's back is likely to 
		///		break it.
		namespace stateEngine {
			// Note that `self` here is still UIGestureRecognizer.
			
			public var state: UIGestureRecognizerState {...}
			
			public func ignoreTouch(touch: UITouch, forEvent event: UIEvent) {...}
			public func reset() {...}
		}
		
		public var state: UIGestureRecognizerState {
			get { return stateEngine.state }
		}
	}

You could tag particular methods and properties such that files have to ask for access to that subset:

	public class UIGestureRecognizer {
		public(@restricted(set: StateEngine)) var state: UIGestureRecognizerState {...}
		
		public(@restricted(StateEngine))  func ignoreTouch(touch: UITouch, forEvent event: UIEvent) {...}
		public(@restricted(StateEngine)) func reset() {...}
	}

	// In some other file...
	import UIKit
	use UIGestureRecognizer.StateEngine

Or you could move the dangerous members into a submodule and thus require a separate import to see them. I will not speculate on a good submodule syntax, but usage would end up looking like this:

	import UIKit
	import UIKit.UIGestureRecognizerStateEngine

All of these approaches share the virtues of the Objective-C approach:

* Subclasses which don't need the dangerous stuff don't have access to it.
* Code which is not technically a subclass but still requires access *does* have access to it.
* They'll work with types which *don't* get subclassed but similarly have some rare-but-dangerous APIs.

So, just as with controlling override point callers, I think that `protected` is at best a rough approximation of the feature you actually want.

-- 
Brent Royal-Gordon
Architechies



More information about the swift-evolution mailing list