[swift-evolution] [Review] SE-0160: Limiting @objc inference

Douglas Gregor dgregor at apple.com
Mon Mar 27 12:37:22 CDT 2017


> On Mar 25, 2017, at 3:46 PM, Brent Royal-Gordon <brent at architechies.com> wrote:
> 
>> On Mar 24, 2017, at 10:09 AM, Douglas Gregor <dgregor at apple.com> wrote:
>> 
>>> I'm actually not worried about methods so much as properties. KVC is completely untyped on the Objective-C side, and there are several different mechanisms there which use KVC with poorly validated external strings, like bindings, sort descriptors, and predicates. Tons of migration errors are going to escape into production if we do this,
>> 
>> We can avoid these by migrating conservatively (have the migrator add @objc everywhere it’s inferred in Swift 3).
> 
> We can do that, but personally, I really hate these kinds of conservative migrations. It might be unavoidable, though.
> 
>>> Have you considered a deprecation cycle (for instance, having Swift 4 thunks log a warning that they're going away in Swift 5)?
>> 
>> I think Swift 3 -> Swift 4 is the deprecation cycle, no?
> 
> But there was no indication during Swift 3 that this feature was going away. As I understand it, a deprecation cycle introduces advanced warning of a change so you have time to prepare for it; that's not available here.

The deprecation cycle is however long Swift 3 compatibility mode is around: Swift 3 compatibility mode can start warning about uses of the deprecated @objc inference—both in the compiler proper and at runtime via logging.

> 
> My concern is that, because the tools are not really aware of KVC, we can't count on the compiler to lead developers to missing `@objc` properties. Folks are only going to find those mistakes through testing, and they're inevitably going to miss a few spots. So some poor schmuck is going to migrate their code to Swift 4 without realizing this is an issue at all, accidentally miss a few spots in their testing, ship it, and have to deal with weird crashes out of nowhere. They're going to say, "My code worked just fine before. Swift 4 broke it!" And they won't be wrong.
> 
> I'd be more comfortable with a version-long deprecation cycle that gave developers plenty of time to notice these bugs. Failing that, I'd at least like to see them get backtraces containing a symbol name like `YouCantInvokeASwiftMemberThroughTheObjectiveCRuntimeUnlessItsMarkedWithAtObjc` so the nature of the problem and its solution will be more obvious. (Preferably, this function would log the instance and selector, so if people got both the logs and the backtrace, the diagnosis would be as simple as we can make it.)

We can do logs with a backtrace. This is fantastic suggestion also made earlier.

> (Actually, I wonder if we could install a `-doesNotRecognizeSelector:` override in Swift classes which looked for a matching member in the Swift runtime metadata and, if it found one, called the `YouCantInvoke…` function? That would be lower overhead than generating stubs at compile time, and the slowness of searching the runtime metadata wouldn't matter much since it was going to crash anyway. I'm not sure if it might remove useful information from the backtrace, though. Maybe in Swift 5, when these bugs will be more rare. Or maybe in `SwiftObject`.)

The Objective-C thunks are nontrivial code patterns that we won’t be able to generate at runtime without a ton of infrastructure, so we’re stuck with statically generating the stubs. Also, we don’t have Swift runtime metadata for methods now.

> 
>> Plus, inheritance from an Objective-C class is often incidental: you do it because you need an NSObjectProtocol conformance, or something else expects NSObject. I haven’t heard of developers inheriting from NSObject solely to get @objc inference for their members.
> 
> You do it because you need a particular object to interact with Objective-C. In that circumstance, I don't think the compiler is wrong to assume that you want to expose as many members as possible to Objective-C.

You don’t need it for a particular object to interact with Objective-C; you need it so that the class itself is visible to Objective-C source code. Or because something forced you to inherit from NSObject (e.g,, the NSObjectProtocol, which I mentioned in my reply to Charles).

> 
>>> you already have to specify `dynamic` to avoid optimizations;
>> 
>> Conceptually, ‘dynamic’ is orthogonal to ‘@objc’. In today’s implementation, we can only implement ‘dynamic’ via the Objective-C runtime, hence this proposal’s requirement to write both.
> 
> I understand that, but again, I think it's defensible for the compiler to assume that, if you want dynamic behavior in a class where you've already enabled Objective-C interop, you probably want that dynamic behavior to be compatible with Objective-C.
> 
> I guess we just take different standpoints on Objective-C interop. My belief is that, if you state an intention to have a type interoperate with Objective-C, Swift should try to expose as many of its members to Objective-C as possible. I think you believe that Swift should expose as *little* as possible to Objective-C.

I think that exposing as many of its members to Objective-C as possible is both hard to reason about (something your idea below would address) and has an unacceptable impact on code size.

> Because of that difference, I actually think I'd be *more* likely to support removing inference by requiring an explicit `@nonobjc` on members of Objective-C-compatible classes which aren't compatible with Objective-C. That is, writing:
> 
> 	class Foo: NSObject {
> 		var bar: Int?
> 	}
> 
> Is an error; you have to write:
> 
> 	class Foo: NSObject {
> 		@nonobjc var bar: Int?
> 	}
> 
> I don't really like that answer very much, but I like it more than I would like requiring `@objc` if `bar` were a plain `Int`.

Interesting. I floated the idea of doing this for members of @objc protocol extensions

	extension UITableViewController {
	    func objCWontSeeMee() { } // error: must have explicit @nonobjc to acknowledge that this will not be available in @objc.
	}

but almost nobody liked that idea due to the boilerplate it caused. I think they’ll yelp louder if they have to do it in all of their NSObject-derived classes.

	- Doug



More information about the swift-evolution mailing list