How to allow subclasses inherit func implementations

I am in the process of writing a small set of classes to implement some messages. I've defined a base class called NetworkMessage which contains the core functionality of all messages. At this point, unless performance becomes an issue, I'm planning to use NSCoding to handle the serialization because it's pretty straightforward to use, or at least I thought so.

Swift is getting in my way, or perhaps my understanding of it is.

I think I understand the need for "required" initializers, and the fact that a subclass needs to be sure to call one as part of its initialization.

In my case, and I've only just started these classes, I've created a second, intermediary class called TileRelatedMessage that subclasses NetworkMessage to provide some extra functionality specific to a subset of the message kinds.

I've then created the first of several subclasses of the intermediary, only to find that even though TileRelatedMessage ensures that the rules of initialization are followed, my lowest level class will not compile unless I create what seems to me, a completely pointless override of the NSCoding initializer.

In the code below, if I comment out the 'init(coder:)' initializer I get an error from the compiler. I understand why I get that if I don't include it in TileRelatedMessage, however it makes no sense to me that I have to include it in DiscardATileMessage. The initializer I have in DiscardATileMessage calls an initializer in TileRelatedMessage which in turn calls a required initializer in NetworkMessage. What is missing that leaves this unsafe in any way? Through this chain, it's guaranteed that every class property has been initialized.

I think I understand why Swift enforces some of these rules, however they seem so hit and miss a lot of the time, that even after dabbling with Swift for 12 months now, it is incredibly frustrating to say the least.

Here is the code in question; I've tried to limit it:

enum MessageKind : Int {
    /// The sender (opponent) wants a new tile
    case requestATile
    /// ... and others...
}

public class NetworkMessage : NSCoding {

    let kind: MessageKind

    /// Initialises the message object with the specified kind.
    ///
    /// - Parameter withMessageKind: the message kind
    init(withMessageKind: MessageKind) {
        self.kind = withMessageKind
    }

    /// Encodes the message.
    ///
    /// - Parameter aCoder: the coder that will contain the results of the encoding.
    public func encode(with aCoder: NSCoder) {
        aCoder.encode(self.kind.rawValue, forKey: "kind")
    }

    /// Decodes a message from the supplied decoder.
    ///
    /// - Parameter aDecoder: the decoder
    public required init?(coder aDecoder: NSCoder) {
        self.kind = MessageKind.init(rawValue: aDecoder.decodeInteger(forKey: "kind"))!
    }

    /// Encodes the message as a stream of bytes, encapsulated within a Data object.
    ///
    /// - Returns: A Data object containing the byte stream representation of the message.
    public func data() -> Data {
        return NSKeyedArchiver.archivedData(withRootObject: self)
    }

    /// Constructs a NetworkMessage from the supplied byte stream.
    ///
    /// - Parameter fromData: A Data object containing the byte stream representation of the message.
    /// - Returns: A NetworkMessage, decoded from the input byte stream.
    static func networkMessage(fromData: Data) -> NetworkMessage? {
        return NSKeyedUnarchiver.unarchiveObject(with: fromData) as? NetworkMessage
    }

}

public class TileRelatedMessage : NetworkMessage {

    var tileID : Int = 0

    override init(withMessageKind: MessageKind) {
        super.init(withMessageKind: withMessageKind)
    }

    init(withTileID tileID: TileID, andMessageKind kind: MessageKind) {
        self.tileID = tileID

        super.init(withMessageKind: kind)
    }

    public override func encode(with aCoder: NSCoder) {
        aCoder.encode(self.tileID, forKey: "tileID")

        super.encode(with: aCoder)
    }

    public required init?(coder aDecoder: NSCoder) {
        self.tileID = aDecoder.decodeInteger(forKey: "tileID")

        super.init(coder: aDecoder)
    }

}

public class DiscardATileMessage : TileRelatedMessage {

    init(withTileID: TileID) {
        super.init(withTileID: withTileID, andMessageKind: .discardATile)
    }

    /// Compiler returns:
    ///     'required' initializer 'init(coder:)' must be provided by subclass of 'TileRelatedMessage'
    ///
    ///public required init?(coder aDecoder: NSCoder) {
    ///    super.init(coder: aDecoder)
    ///}

}

The compiler doesn't know that there'll never be a DiscardATileMessage in an archive somewhere, so it does require (heh) that the initializer be present. I think you're okay with that part.

So, why doesn't it just inherit the parent's init(coder:)? As you say, it would be memory-safe…but the compiler doesn't know if it's correct. It will assume that's okay if DiscardATileMessage doesn't have any other initializers, but because you provided init(withTileID:), the compiler assumes there might be some Very Important Setup logic for DiscardATileMessage instances, and just inheriting the superclass's initializers wouldn't be correct in that case. (This is as much about what API you want to expose on the class as what needs to be available at run time.)

Either calling super.init(coder:) or including a fatalError("should never be called") in the body of DiscardATileMessage's init(coder:) is a valid answer to this problem.

Thanks for that.

I would have thought that, given that DiscardATileMessage has no locally defined properties, the compiler would realise there is nothing that needs to be done that is "Very Important" beyond the initializer provided.

Sure, if I'd included a local property there, it would make complete sense to me.

I guess I'll just have to implement these relatively empty init(code:) initializers in each of the subclasses. What a shame. I dislike repeating code that adds nothing.

If you can abstract out the work init(withTileID:) needs to do, then the initializers would be inheritable. For example, you could make kind be a computed property that subclasses override, rather than a value that has to get passed up the chain. Or you could add a static var defaultKind: MessageKind that TileRelatedMessage's init(withTileID:) could invoke. Or you could forego the MessageKind enum and put all the relevant info on the classes. (But I imagine you're switching over it somewhere else, so maybe you don't want to do that one.)

You know, the computed property idea sounded brilliant except that it's a little hard to serialize, and deserialize. I wanted to include the kind as part of the byte stream so that on deserializing, I could use it in a (you guessed it...) switch statement to ensure the correct class is used to finish the message content off.

I've got most of my messages coded now, however I'm seriously thinking that NSCoding and NSKeyedArchiver are not the best way forward anymore. It just feels to heavy for what I want, but thats a different topic.

Thanks for your suggestions.

Is there a reason you did not use Codable?

Well, the honest answer to that one is ignorance. You know, (here we go), when I was a kid, we had to write our own APIs and stuff. Nowadays, there's APIs for everything, and when you're silly enough not to try googling or searching the copious quantities of doc around, you miss stuff.

Oh, and just to follow up after reading the Codable doc for 5 minutes.

OMG; I have wasted so much time.

Thanks.

Sorry, didn’t mean to sound patronising.

You didn't. I was laughing along with my family as I read, and responded. Sometimes it's so easy to fall into old habits, and not look beyond what we know. Starting to read through tutes on Codable to find the gotchas before I start doing too much. Why "Character" isn't Codable is interesting.

Terms of Service

Privacy Policy

Cookie Policy