Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Delegate methods not called if implementing class is marked private?

Here's a short snippet of Swift code that works fine (where "fine" is defined as "Parsing!" being printed a whole bunch in response to calling the class method Parse.parse):

import Foundation

class Parse {
  class func parse(stream: NSInputStream) {
    return Parser().parse(stream)
  }

  class Parser: NSObject, NSXMLParserDelegate {
    func parse(stream: NSInputStream) {
      let XMLParser = NSXMLParser(stream: stream)
      let delegate = XMLParserDelegate()
      XMLParser.delegate = delegate
      XMLParser.parse()
    }

    class XMLParserDelegate: NSObject, NSXMLParserDelegate {
      func parser(
        parser: NSXMLParser,
        didStartElement elementName: String,
        namespaceURI: String?,
        qualifiedName qName: String?,
        attributes attributeDict: [NSObject : AnyObject])
      {
        NSLog("Parsing!")
      }
    }
  }
}

The problem comes when I attempt to use Swift's visibility features. In particular, I do not want to make the Parser class visible to other files (because there is no reason for it to be visible). If I declare it via private class Parser … however, the code ceases to work! parser:didStartElement:namespaceURI:qualifiedName:attributes: is no longer called!

This all seems bizarre to me and unlike how it would work in any other language. As such, I feel like one of the following two things must be true:

  1. Swift's system for namespacing is, at best, weird. More plainly, it just seems broken to me.

  2. Swift is fine and I am just doing something very silly! If that's the case, then great! Please let me know what it is!

Thanks for the help, everyone!


Edit: Here's a slightly trimmed-down version. As before, the code works fine until the Parser class is marked private:

import Foundation

class Parse {
  class func parse(stream: NSInputStream) {
    return Parser().parse(stream)
  }
}

class Parser: NSObject, NSXMLParserDelegate {
  func parse(stream: NSInputStream) {
    let XMLParser = NSXMLParser(stream: stream)
    XMLParser.delegate = self
    XMLParser.parse()
  }

  func parser(
    parser: NSXMLParser,
    didStartElement elementName: String,
    namespaceURI: String?,
    qualifiedName qName: String?,
    attributes attributeDict: [NSObject : AnyObject])
  {
    NSLog("Parsing!")
  }
}
like image 678
John Nowak Avatar asked Feb 12 '15 20:02

John Nowak


1 Answers

This shouldn't be surprising. NSXMLParserDelegate includes the following:

optional func parser(_ parser: NSXMLParser, didStartElement elementName: String, namespaceURI namespaceURI: String, qualifiedName qualifiedName: String, attributes attributeDict: [NSObject : AnyObject])

Since it's optional, there must somewhere in NSXMLParser be a doesRespondToSelector() call. It shouldn't be surprising that function would fail if the underlying class is private. (Given its interaction with dynamic ObjC calls, it wouldn't be shocking if it worked, either; but neither approach should be considered broken, and what you describe better matches what you asked for; which is that these methods be private.)

The right answer here is that XMLParserDelegate needs to be public along with its implementation of NSXMLParserDelegate. Parser doesn't have to be public, and any non-protocol methods don't need to be public. But NSXMLParser needs to be able to see its delegate methods if you want it to call them.

What's a bit more surprising is that this isn't a compiler error.


EDIT: While it still surprises me that this doesn't create a compiler error (and I feel that is probably a bug), the key finding is that private means private. It means that other files cannot see the method, and so respondsToSelector() will fail. To demonstrate this in a simpler form:

main.swift

import Foundation
private class Impl : NSObject, P {
    func run() {
        println("Running")
    }
}
let c = Container(p: Impl())
c.go()

more.swift

import Foundation

@objc internal protocol P: NSObjectProtocol {
    optional func run()
}

// Change internal to private to see change
internal struct Container {
    let p: P
    func go() {
        println(p.dynamicType) // Impl for internal, (Impl in ...) for private
        if p.respondsToSelector(Selector("run")) {
            p.run!() // if run is internal or public
        } else {
            println("Didn't implement") // if run is private
        }

        // Or the Swiftier way:
        if let run = p.run {
            run() // if run is internal or public
        } else {
            println("Didn't implement") // if run is private
        }
    }
}

To see a little more of the details on why this is true, we can look at p.dynamicType. If p is internal, then we see that its type is Impl. If it is private, we see its type is (Impl in _9F9099C659B8A128A78BAA9A7C0E0368). Making things private makes their type and internal structure private.

Private just hides much more than internal. It impacts the runtime, not just the compile time.

And the more I think about it, the more I see why it can't give us a compiler error. It is legal to implement an internal protocol with a private class. The place it goes sideways is when we pass it as a parameter to another access area and then try to introspect it dynamically. And the answer is "don't do that."

It is possible that will change in the future. See https://devforums.apple.com/message/1073092#1073092. It's worth bringing up as a bugreport, but I still wouldn't assume it's a bug; it could certainly be intended behavior.

like image 93
Rob Napier Avatar answered Nov 07 '22 08:11

Rob Napier