如何在Swift子类中实现复制构造函数?

19

我在一个 Swift playground 中有以下示例,试图在 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}

问题在于,这段代码在当前形式下实际上无法编译。在Square子类的init(copyFrom: Square)方法中,会报告以下错误:

覆盖具有选择器 'initWithCopyFrom:' 的方法的类型不兼容 '(Square) -> Square'

如果这不是一个构造函数,而是一个普通的func,那么这个问题就有意义了,因为你可能会传入一个预期在超类中的类型,在子类中已被重写为更严格的类型:

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.

但它是一个构造函数,这使我相信我应该能够重写它并提供不同的方法签名,因为在编译时绝对知道对象的类型。

如果我修改Shape以不再扩展NSObject,则此问题消失。 但是,由于与现有的Objective-C代码一起使用,它需要扩展NSObject

如何更新我的拷贝构造函数以允许Shape知道它正在从Shape复制,并允许Square知道它正在从Square复制?

2个回答

28

init(copyFrom: Square)是一种重载而不是覆盖 init(copyFrom: Shape)的方法。我的意思是它们是不相关的方法,因为它们接受不同类型的参数。在Swift中,这是可以接受的,在ObjC中则是不合法的。在ObjC中没有重载。

Swift初始化程序不会自动继承。所以在Swift中,你不能尝试将随机的Shape复制为Square。该初始化程序不可用。但在ObjC中,初始化程序会自动继承(而且你不能阻止它们这样做)。因此,如果你有一个方法initWithCopyFrom:(*Shape),每个子类都必须愿意接受它。这意味着你可以(在ObjC中)尝试将Circle的副本创建为Square。当然,这是无意义的。

如果这是一个NSObject子类,你应该使用NSCopying。下面是如何操作:

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
    }

}

谢谢Rob - 回答得很好。 - Craig Otis
2
您可能会发现这个关于在纯Swift中制作副本的讨论很有用:https://dev59.com/z18e5IYBdhLWcg3wjaxI - Rob Napier
对于Swift 2 / Xcode 7,它将是 let theCopy = self.dynamicType.init(),当然要使用as!而不是as(仅提及,因为这里出现了相关的问题:https://dev59.com/F43da4cB1Zd3GeqP5t50)。 - Martin R
感谢@RobNapier的回答!我有些改进,加入了Swift 3版本。 - Luca Davanzo
NS_UNAVAILABLE 是 Objective-C 中避免继承初始化方法的一种方式,这与不继承它非常相似。但可能是在此答案之后发布的。 - Tommy

2
最简单的方法就是将子类初始化器名称更改为init(copyFromSquare: Square),同时保留Square中的init(copyFrom: Shape)方法(通过从Shape继承来实现)。当然你也可以重写init(copyFrom: Shape)方法,测试一下copyFrom是否为Square,如果是则执行某些操作(设置长度),否则不执行。请注意,在调用超级初始化器之前必须先设置self.length
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)
    }
}

+1 我喜欢只更改方法名称为 copyFromSquare: 的想法,让子类自己命名为 copyFromTriangle: 等。 - Craig Otis
1
请注意,这将使copyFrom:Shape在ObjC中对所有子类可用,这将创建一个不正确的副本。如果您使用类命名方法,请确保要么从超类中删除复制初始化程序,要么将其标记为“required”,以便强制所有子类以某种方式实现它(即使只是通过抛出precondition())。 - Rob Napier

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接