Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement copy constructor in Swift subclass?

Tags:

swift

I have the following example in a Swift playground, in an attempt to implement a copy constructor in Swift:

class Shape : NSObject {
    var color : String

    override init() {
        color = "Red"
    }

    init(copyFrom: Shape) {
        color = copyFrom.color
    }
}

class Square : Shape {
    var length : Double

    override init() {
        super.init()
        length = 10.0
    }

    init(copyFrom: Square) { /* Compilation error here! */
        super.init(copyFrom: copyFrom)
        length = copyFrom.length
    }
}

let s : Square = Square()      // {{color "Red"} length 10.0}

let copy = Square(copyFrom: s) // {{color "Red"} length 10.0}

s.color = "Blue"               // {{color "Blue"} length 10.0}
s                              // {{color "Blue"} length 10.0}
copy                           // {{color "Red"} length 10.0}

The problem is that this doesn't actually compile in its current form. On the init(copyFrom: Square) method in the Square subclass, this error is reported:

Overriding method with selector 'initWithCopyFrom:' has incompatible type '(Square) -> Square'

This issue would make sense if it wasn't a constructor, as if it were a regular func, you could potentially pass in a type that is expected in the superclass, but that has been overridden in the subclass to be more restrictive:

let mySquare : Shape = Square()  // Note the var is a SHAPE
mySquare.someShapeMethod("Test") // If Square overrides someShapeMethod() to expect Int, compiler errors out to protect us here.

But the fact that it's a constructor leads me to believe that I should be able to override it and provide a different method signature, since it's absolutely known at compile time what the type of the object is.

This issue disappears if I alter Shape to no longer extend NSObject. However, due to inclusion with an existing Objective-C code, it needs to extend NSObject.

How can I update my copy constructor to allow a Shape to know it's copying from a Shape, and allow a Square to know it's copying from a Square?

like image 624
Craig Otis Avatar asked Sep 12 '14 13:09

Craig Otis


2 Answers

init(copyFrom: Square) is an overload, not an override, of init(copyFrom: Shape). What I mean is that they are unrelated methods because they accept different types. In Swift that's acceptable. In ObjC, that's illegal. There are no overloads in ObjC.

Swift initializers don't automatically inherit. So in Swift, you couldn't try to copy a random Shape as a Square. The initializer isn't available. But in ObjC, initializers do automatically inherit (and you can't stop them from doing so). So if you have a method initWithCopyFrom:(*Shape), it is required that every subclass be willing to accept it. That means you could (in ObjC) try to create a copy of a Circle as a Square. That's of course nonsense.

If this is an NSObject subclass, you should use NSCopying. Here's how you would go about that:

import Foundation

class Shape : NSObject, NSCopying { // <== Note NSCopying
  var color : String

  required override init() { // <== Need "required" because we need to call dynamicType() below
    color = "Red"
  }

  func copyWithZone(zone: NSZone) -> AnyObject { // <== NSCopying
    // *** Construct "one of my current class". This is why init() is a required initializer
    let theCopy = self.dynamicType()
    theCopy.color = self.color
    return theCopy
  }
}

class Square : Shape {
  var length : Double

  required init() {
    length = 10.0
    super.init()
  }

  override func copyWithZone(zone: NSZone) -> AnyObject { // <== NSCopying
    let theCopy = super.copyWithZone(zone) as Square // <== Need casting since it returns AnyObject
    theCopy.length = self.length
    return theCopy
  }

}

let s = Square()      // {{color "Red"} length 10.0}

let copy = s.copy() as Square // {{color "Red"} length 10.0} // <== copy() requires a cast

s.color = "Blue"               // {{color "Blue"} length 10.0}
s                              // {{color "Blue"} length 10.0}
copy                           // {{color "Red"}

Swift 3

class Shape: NSObject, NSCopying {

    required override init() {
        super.init()
    }    

    func copy(with zone: NSZone? = nil) -> Any {
        let copy = type(of: self).init()
        return copy
    }

}

class Square: Shape {

    required override init() {
        super.init()
    }    

    func copy(with zone: NSZone? = nil) -> Any {
        let copy = super.copy(with: zone) as! Square
        copy.foo = self.foo
        ......
        return copy
    }

}
like image 195
Rob Napier Avatar answered Oct 01 '22 05:10

Rob Napier


The simplest way to do it would simply be to change the name of the subclass initialiser to init(copyFromSquare: Square), leaving Square with the init(copyFrom: Shape) method intact (as you have contracted by inheriting from Shape).

You could of course override init(copyFrom: Shape), and test whether copyFrom is a Square, in which case you take one course of action (set the length), otherwise not.

Note also that you need to set self.length before you call the super.

class Shape : NSObject {
    var color : String

    override init() {
        color = "Red"
    }

    init(copyFrom: Shape) {
        color = copyFrom.color
    }
}

class Square : Shape {
    var length : Double

    override init() {
        self.length = 10.0
        super.init()
    }

    override init(copyFrom: Shape) {
        if copyFrom is Square {
            self.length = (copyFrom as Square).length
        } else {
            self.length = 10.0 // default
        }
        super.init(copyFrom: copyFrom)
    }
}
like image 39
Grimxn Avatar answered Oct 01 '22 04:10

Grimxn