> 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

_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to