+1. Well thought out, it's bothered me too.
> On Jan 4, 2017, at 7:34 PM, Rick Mann via swift-evolution > <[email protected]> wrote: > > +1 > >> On Jan 4, 2017, at 16:50 , Douglas Gregor via swift-evolution >> <[email protected]> wrote: >> >> Hi all, >> >> Here’s a draft proposal to limit inference of @objc to only those places >> where we need it for consistency of the semantic model. It’s in the realm of >> things that isn’t *needed* for ABI stability, but if we’re going to make the >> source-breaking change here we’d much rather do it in the Swift 4 time-frame >> than later. Proposal is at: >> >> >> https://github.com/DougGregor/swift-evolution/blob/objc-inference/proposals/NNNN-objc-inference.md >> >> Introduction >> >> One can explicitly write @objc on any Swift declaration that can be >> expressed in Objective-C. As a convenience, Swift also infers @objc in a >> number of places to improve interoperability with Objective-C and eliminate >> boilerplate. This proposal scales back the inference of @objc to only those >> cases where the declaration must be available to Objective-C to maintain >> semantic coherence of the model, e.g., when overriding an @objc method or >> implementing a requirement of an @objcprotocol. Other cases currently >> supported (e.g., a method declared in a subclass of NSObject) would no >> longer infer @objc, but one could continue to write it explicitly to produce >> Objective-C entry points. >> >> Swift-evolution thread: here >> >> Motivation >> >> There are several observations motivating this proposal. The first is that >> Swift's rules for inference of @objc are fairly baroque, and it is often >> unclear to users when @objc will be inferred. This proposal seeks to make >> the inference rules more straightforward. The second observation is that it >> is fairly easy to write Swift classes that inadvertently cause Objective-C >> selector collisions due to overloading, e.g., >> >> class MyNumber : NSObject >> { >> >> init(_ int: Int >> ) { } >> >> init(_ double: Double) { } // error: initializer 'init' with Objective-C >> selector 'init:' >> // conflicts with previous declaration with the same Objective-C >> selector >> } >> The example above also illustrates the third observation, which is that code >> following the Swift API Design Guidelines will use Swift names that often >> translate into very poor Objective-C names that violate the Objective-C >> Coding Guidelines for Cocoa. Specifically, the Objective-C selectors for the >> initializers above should include a noun describing the first argument, >> e.g., initWithInteger: and initWithDouble:, which requires explicit @objc >> annotations anyway: >> >> class MyNumber : NSObject >> { >> >> @objc(initWithInteger:) init(_ int: Int >> ) { } >> >> @objc(initWithDouble:) init(_ double: Double >> ) { } >> } >> >> The final observation is that there is a cost for each Objective-C entry >> point, because the Swift compiler must create a "thunk" method that maps >> from the Objective-C calling convention to the Swift calling convention and >> is recorded within Objective-C metadata. This increases the size of the >> binary (preliminary tests on some Cocoa[Touch] apps found that 6-8% of >> binary size was in these thunks alone, some of which are undoubtedly >> unused), and can have some impact on load time (the dynamic linker has to >> sort through the Objective-C metadata for these thunks). >> >> Proposed solution >> >> The proposed solution is to limit the inference of @objc to only those >> places where it is required for semantic consistency of the programming >> model. >> >> Constructs that (still) infer @objc >> >> Specifically, @objc will continue to be inferred for a declaration when: >> >> • The declaration is an override of an @objc declaration, e.g., >> >> class Super >> { >> >> @objc func foo >> () { } >> } >> >> >> class Sub : Super >> { >> >> /* inferred @objc */ >> >> >> override func foo >> () { } >> } >> >> This inference is required so that Objective-C callers to the method >> Super.foo() will appropriately invoke the overriding method Sub.foo(). >> >> • The declaration satisfies a requirement of an @objc protocol, e.g., >> >> @objc protocol MyDelegate >> { >> >> func bar >> () >> } >> >> >> class MyClass : MyDelegate >> { >> >> /* inferred @objc */ >> >> >> func bar >> () { } >> } >> >> This inference is required because anyone calling MyDelegate.bar(), whether >> from Objective-C or Swift, will do so via an Objective-C message send, so >> conforming to the protocol requires an Objective-C entry point. >> >> • The declaration has the @IBAction or @IBOutlet attribute. This >> inference is required because the interaction with Interface Builder occurs >> entirely through the Objective-C runtime, and therefore depends on the >> existence of an Objective-C entrypoint. >> >> • The declaration has the @NSManaged attribute. This inference is >> required because the interaction with CoreData occurs entirely through the >> Objective-C runtime, and therefore depends on the existence of an >> Objective-C entrypoint. >> >> The list above describes cases where Swift 3 already performs inference of >> @objc and will continue to do so if this proposal is accepted. >> >> dynamic no longer infers @objc >> >> A declaration that is dynamic will no longer infer @objc. For example: >> >> class MyClass >> { >> >> dynamic func foo() { } // error: 'dynamic' method must be '@objc' >> @objc dynamic func bar() { } // okay >> } >> This change is intended to separate current implementation limitations from >> future language evolution: the current implementation supports dynamic by >> always using the Objective-C message send mechanism, allowing replacement of >> dynamic implementations via the Objective-C runtime (e.g., class_addMethod >> and class_replaceMethod). In the future, it is plausible that the Swift >> language and runtime will evolve to support dynamic without relying on the >> Objective-C runtime, and it's important that we leave the door open for that >> language evolution. >> >> This change therefore does two things. First, it makes it clear that the >> dynamic behavior is tied to the Objective-C runtime. Second, it means that >> well-formed Swift 4 code will continue to work in the same way should Swift >> gain the ability to provide dynamic without relying on Objective-C: at that >> point, the method foo() above will become well-formed, and the method bar() >> will continue to work as it does today through the Objective-C runtime. >> Indeed, this change is the right way forward even if Swift never supports >> dynamic in its own runtime, following the precedent of SE-0070, which >> required the Objective-C-only protocol feature "optional requirements" to be >> explicitly marked with @objc. >> >> NSObject-derived classes no longer infer @objc >> >> A declaration within an NSObject-derived class will no longer infer objc`. >> For example: >> >> class MyClass : NSObject >> { >> >> func foo() { } // not exposed to Objective-C in Swift 4 >> } >> This is the only major change of this proposal, because it means that a >> large number of methods that Swift 3 would have exposed to Objective-C (and >> would, therefore, be callable from Objective-C code in a mixed project) will >> no longer be exposed. On the other hand, this is the most unpredictable part >> of the Swift 3 model, because such methods infer @objconly when the method >> can be expressed in Objective-C. For example: >> >> extension MyClass >> { >> >> func bar(param: ObjCClass) { } // exposed to Objective-C in Swift 3; not >> exposed by this proposal >> func baz(param: SwiftStruct) { } // not exposed to Objective-C >> } >> With this proposal, neither method specifies @objc nor is either required by >> the semantic model to expose an Objective-C entrypoint, so they don't infer >> @objc: there is no need to reason about the type of the parameter's >> suitability in Objective-C. >> >> Side benefit: more reasonable expectations for @objc protocol extensions >> >> Users are often surprised to realize that extensions of @objc protocols do >> not, in fact, produce Objective-C entrypoints: >> >> @objc protocol P >> { } >> >> >> extension P >> { >> >> func bar >> () { } >> } >> >> >> class C : NSObject >> { } >> >> >> let c = C >> () >> >> print(c.respondsToSelector("bar")) // prints "false" >> The expectation that P.bar() has an Objective-C entry point is set by the >> fact that NSObject-derived Swift classes do implicitly create Objective-C >> entry points for declarations within class extensions when possible, but >> Swift does not (and, practically speaking, cannot) do the same for protocol >> extensions. >> >> A previous mini-proposal discussed here suggested requiring @nonobjc for >> members of @objc protocol extensions. However, limiting inference of @objc >> eliminates the expectation itself, addressing the problem from a different >> angle. >> >> Source compatibility >> >> The two changes that remove inference of @objc are both source-breaking in >> different ways. The dynamic change mostly straightforward: >> >> • In Swift 4 mode, introduce an error that when a dynamic declaration >> does not explicitly state @objc, with a Fix-It to add the @objc. >> >> • In Swift 3 compatibility mode, continue to infer @objc for dynamic >> methods. However, introduce a warning that such code will be ill-formed in >> Swift 4, along with a Fix-It to add the @objc. This >> >> • A Swift 3-to-4 migrator could employ the same logic as Swift 3 >> compatibility mode to update dynamic declarations appropriately. >> >> The elimination of inference of @objc for declarations in NSObject >> subclasses is more complicated. Considering again the three cases: >> >> • In Swift 4 mode, do not infer @objc for such declarations. >> Source-breaking changes that will be introduced include: >> >> • If #selector or #keyPath refers to one such declaration, an error >> will be produced on previously-valid code that the declaration is not @objc. >> In most cases, a Fix-It will suggest the addition of @objc. >> >> • The lack of @objc means that Objective-C code in mixed-source >> projects won't be able to call these declarations. Most problems caused by >> this will result in warnings or errors from the Objective-C compiler (due to >> unrecognized selectors), but some might only be detected at runtime. These >> latter cases will be hard-to-detect. >> >> • Other tools and frameworks that rely on the presence of Objective-C >> entrypoints but do not make use of Swift's facilities for referring to them >> will fail. This case is particularly hard to diagnose well, and failures of >> this sort are likely to cause runtime failures that only the developer can >> diagnose and correct. >> >> • In Swift 3 compatibility mode, continue to infer @objc for these >> declarations. When @objc is inferred based on this rule, modify the >> generated header (i.e., the header used by Objective-C code to call into >> Swift code) so that the declaration contains a "deprecated" attribute >> indicating that the Swift declaration should be explicitly marked with >> @objc. For example: >> >> class MyClass : NSObject >> { >> >> func foo >> () { } >> } >> >> will produce a generated header that includes: >> >> @interface MyClass : NSObject >> >> -( >> void)foo NS_DEPRECATED("MyClass.foo() requires an explicit `@objc` in Swift >> 4" >> ); >> @end >> >> This way, any reference to that declaration from Objective-C code will >> produce a warning about the deprecation. Users can silence the warning by >> adding an explicit @objc. >> >> • A Swift 3-to-4 migrator is the hardest part of the story. Ideally, the >> migrator to only add @objc in places where it is needed, so that we see some >> of the expected benefits of code-size reduction. However, there are two >> problems with doing so: >> >> • Some of the uses that imply the need to add @objc come from >> Objective-C code, so a Swift 3-to-4 migrator would also need to compile the >> Objective-C code (possibly with a modified version of the Objective-C >> compiler) and match up the "deprecated" warnings mentioned in the Swift 3 >> compatibility mode bullet with Swift declarations. >> >> • The migrator can't reason about dynamically-constructed selectors >> or the behavior of other tools that might directly use the Objective-C >> runtime, so failing to add a @objc will lead to migrated programs that >> compile but fail to execute correctly. >> >> Overriding of declarations introduced in class extensions >> >> Swift's class model doesn't support overriding of declarations introduced in >> class extensions. For example, the following code produces an amusing error >> message on the override: >> >> class MySuperclass >> { } >> >> >> extension MySuperclass >> { >> >> func extMethod >> () { } >> } >> >> >> class MySubclass : MySuperclass >> { } >> >> >> extension MySubclass >> { >> >> override func extMethod() { } // error: declarations in extensions cannot >> override yet >> } >> However, this does work in Swift 3 when the method is @objc, e.g., >> >> class MySuperclass >> { } >> >> >> extension MySuperclass >> { >> >> @objc func extMethod >> () { } >> } >> >> >> class MySubclass : MySuperclass >> { } >> >> >> extension MySubclass >> { >> >> override func extMethod() { } // okay! Objective-C message dispatch allows >> this >> } >> Removing @objc inference for NSObject subclasses will therefore break this >> correct Swift 3 code: >> >> class MySuperclass >> { } >> >> >> extension MySuperclass : NSObject >> { >> >> func extMethod() { } // implicitly @objc in Swift 3, not @objc in Swift 4 >> } >> >> >> class MySubclass : MySuperclass >> { } >> >> >> extension MySubclass >> { >> >> override func extMethod() { } // okay in Swift 3, error in Swift 4: >> declarations in extensions cannot override yet >> } >> There are several potential solutions to this problem, but both are >> out-of-scope for this particular proposal: >> >> • Require that a non-@objc declaration in a class extension by explicitly >> declared final so that it is clear from the source that this declaration >> cannot be overridden. >> >> • Extend Swift's class model to permit overriding of declarations >> introduced in extensions. >> >> Alternatives considered >> >> Aside from the obvious alternative of "do nothing", there are ways to >> address some of the problems called out in theMotivation section without >> eliminating inference in the cases we're talking about, or to soften the >> requirements on some constructs. >> >> Mangling Objective-C selectors >> >> Some of the problems with Objective-C selector collisions could be addressed >> by using "mangled" selector names for Swift-defined declarations. For >> example, given: >> >> class MyClass : NSObject >> { >> >> func print(_ value: Int >> ) { } >> } >> >> Instead of choosing the Objective-C selector "print:" by default, which is >> likely to conflict, we could use a mangled selector name like >> "__MyModule__MyClass__print__Int:" that is unlikely to conflict with >> anything else in the program. However, this change would also be >> source-breaking for the same reasons that restricting @objc inference is: >> dynamic behavior that constructs Objective-C selectors or tools outside of >> Swift that expect certain selectors will break at run-time. >> >> Completely eliminating @objc inference >> >> Another alternative to this proposal is to go further and completely >> eliminate @objc inference. This would simplify the programming model >> further---it's exposed to Objective-C only if it's marked @objc---but at the >> cost of significantly more boilerplate for applications that use Objective-C >> frameworks. For example: >> >> class Sub : Super >> { >> >> @objc override func foo() { } // @objc is now required >> } >> >> >> class MyClass : MyDelegate >> { >> >> @objc func bar() { } // @objc is now required >> } >> I believe that this proposal strikes the right balance already, where @objc >> is inferred when it's needed to maintain the semantic model, and can be >> explicitly added to document those places where the user is intentionally >> exposing an Objective-C entrypoint for some reason. Thus, explicitly writing >> @objc indicates intent without creating boilerplate. >> >> Proposal add-on: @objc annotations on class definitions and extensions >> >> If the annotation burden of @objc introduced by this proposal is too high, >> we could make @objc on a class definition or extension thereof imply @objc >> on those members that can be exposed to Objective-C. For example: >> >> @objc extension MyClass >> { >> >> // implicitly @objc >> func f >> () { } >> >> >> // Cannot be exposed to Objective-C because tuples aren't representable in >> Objective-C >> func g() -> (Int, Int) { return (1, 2 >> ) } >> } >> >> This would reduce (but not eliminate) the annotation burden introduced by >> this proposal, allowing developers to group Objective-C-exposed declarations >> together under a single @objc annotation. This reduces the amount of >> boilerplate. With such a change, we'd need to decide what to do with >> MyClass.g(), which could be either: >> >> • Make it an error because the context implies that it is @objc, but it >> cannot be. The error would be suppressed with an explicit @nonobjc >> annotation. >> >> • Make it implicitly @nonobjc. >> >> Option (1) seems more in line with the rest of the proposal, because it >> maintains a predictable model. Regardless, this add-on makes sense if we >> like the benefits of the proposal to limit @objc inference but the >> annotation burden turns out to be annoyingly high. >> >> >> - Doug >> >> >> _______________________________________________ >> swift-evolution mailing list >> [email protected] >> https://lists.swift.org/mailman/listinfo/swift-evolution > > > -- > Rick Mann > [email protected] > > > _______________________________________________ > 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
