在 Swift 中继承 Objective-C 类以及初始化的陷阱






4.22/5 (6投票s)
为了学习 Swift,我决定尝试使用 Sprite Kit。我做的第一件事就是创建一个 SKSpriteNode 的子类。它有一个非常方便的初始化器:init(imageNamed name: string) (在 Swift 中)-(instanceType)initWithImageNamed:(NString*)name (在 Objective-C 中)我从这个派生出来
为了学习 Swift,我决定尝试使用 Sprite Kit。我做的第一件事就是创建一个 SKSpriteNode 的子类。它有一个非常方便的初始化器:init(imageNamed name: string) (在 Swift 中) -(instanceType)initWithImageNamed:(NString*)name (在 Objective-C 中) 我从这个派生出来,结果是
class Ball : SKSpriteNode { init() { super.init(imageNamed: "Ball") } }
并如下添加到我的场景中
var ball = Ball() ball.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame)) self.addChild(ball)
在成功使用 SKSpriteKitNode
直接编写了类似的代码之后
var ball = SKSpriteNode(imageNamed: "Ball") ball.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame)) self.addChild(ball)
我期望它能正常工作。然而,我收到了以下运行时错误
fatal error: use of unimplemented initializer 'init(texture: )' for class 'Ploing.Ball'
奇怪的是,它抱怨 Ball 类中缺少初始化器。为什么调用基类初始化器会导致调用派生类?我不确定,但为了解决这个问题,我决定添加缺失的初始化器
init(texture: SKTexture!) { super.init(texture: texture) }
完成此操作后,我收到了下一个错误
fatal error: use of unimplemented initializer 'init(texture:color:size: )' for class 'Ploing.Ball'
好的,同样的流程,将那个添加到 Ball
类中。
init(texture texture: SKTexture!, color color: UIColor!, size size: CGSize) { super.init(texture: texture, color: color, size: size) }
那次它奏效了,但我对此感到困惑。似乎发生的情况是,派生类调用了超类中的非指定初始化器,而该初始化器又调用了指定初始化器(或中间的一个非指定初始化器)。但是,当调用此初始化器时,它不会调用超类实现,而是调用派生类中的一个,无论它是否存在。在 Ball 的情况下,它们不存在,因此出现了错误。
事实证明,《The Swift Programming Language》的 初始化 部分有以下内容
初始化继承和重写
与 Objective-C 中的子类不同,Swift 子类默认不会继承其超类的初始化器。Swift 的方法可以防止出现这种情况:超类的简单初始化器会被更专业的子类自动继承,并用于创建子类的一个未完全或不正确初始化的新实例。
如果您希望自定义子类呈现与超类相同的初始化器(或多个初始化器),您可以在自定义子类中提供相同初始化器的覆盖实现,也许是为了在初始化期间执行一些自定义操作。
这解释了它。基本上,在 Swift 中,初始化方法就像虚拟函数,实际上是超级虚拟函数。如果调用了超类初始化器,而该初始化器又调用了另一个初始化器(无论是否指定),那么它会将调用转发给派生类。其结果似乎是,对于 Swift 中的任何派生类,都需要重新实现超类的所有初始化器,或者至少是可能从其他初始化器调用的初始化器。
是时候做一些实验了
因此,我着手用一个简单的 Swift 示例来确认这一点
class Foo { var one: Int; var two: Int; init(value value:Int) { self.init(value: value, AndAnother:7) } init(value value:Int, AndAnother value1:Int) { one = value; two = value1; } func print() { println("One:\(one), Two:\(two)") } } class Bar : Foo { init() { super.init(value:1); } } let bar = Bar() bar.print()
这无法编译。
'Foo' 的指定初始化器不能委托(使用 'self.init');您是否打算将其设为便利初始化器?
必须调用超类 'Foo' 的指定初始化器
而在 Objective-C 中,指定初始化器实际上是建议性的,在 Swift 中它是强制执行的。如果一个初始化器没有标记为 convenience 关键字,那么它似乎(至少在编译时)所有初始化器都被视为指定初始化器,而不是同时。在这种情况下,编译失败,因为没有指定初始化器供 Bar.init
调用,并且由于 Foo.init(value value:Int)
没有标记为便利初始化器,因此它被假定为指定初始化器,因此禁止调用其自身的另一个初始化器;这有些矛盾。
这两个规则可以防止 Objective-C 的建议性初始化器出现的问题。除指定初始化器外,所有其他初始化器都必须加上 convenience 关键字前缀。其次,这是唯一允许调用超类初始化器的初始化器,这意味着所有便利初始化器只能(交叉)调用其类的其他便利初始化器或指定初始化器。遵循这些规则可以得到一个可行的示例
class Foo { var one: Int; var two: Int; convenience init(value value:Int) { self.init(value: value, AndAnother:7) } init(value value:Int, AndAnother value1:Int) { one = value; two = value1; } func print() { println("One:\(one), Two:\(two)") } } class Bar : Foo { init() { super.init(value: 7, AndAnother: 8) } } let bar = Bar() bar.print()
这给出了结果
One:7, Two:8
它还完全绕过了超类的便利方法,使其无法从派生类调用。
至于解释原始问题,这也没有帮助,因为新规则可以防止该问题。然而,线索就在这里。它表明问题不在于 Swift 类继承其他 Swift 类,而在于 Swift 类继承 Objective-C 类。
从 Swift 继承 Objective-C
是时候进行另一项实验了,但这次是使用 Objective-C 编写的基类
Base.h
@interface Base : NSObject -(id)initWithValue:(NSInteger) value; -(id)initWithValue:(NSInteger) value AndAnother:(NSInteger) value1; -(void)print; @end
Base.m
#import "Base.h" @interface Base () @property NSInteger one; @property NSInteger two; @end @implementation Base -(id)initWithValue:(NSInteger) value { return [self initWithValue:value AndAnother:2]; } -(id)initWithValue:(NSInteger) value AndAnother:(NSInteger) value1 { if ([super init]) { self.one = value; self.two = value1; } return self; } -(void)print { NSLog(@"One:%d, Two:%d", (int)_one, (int)_two); } @end
main.swift
class Baz : Base { init() { super.init(value: 7) } init(value value:Int, AndAnother value1:Int) { super.init(value: value, andAnother: value1); } } var baz = Baz() baz.print()
查看 Base,很明显指定初始化器是 initWithValue:(int)value AndAnother:(int)value1,而 Baz 调用的是一个“便利”初始化器。这可以顺利编译,但在运行时会产生以下错误
fatal error: use of unimplemented initializer 'init(value:andAnother: )' for class 'Test.Baz'
这与原始 SKSpriteNote 示例的错误类型相同。这里,Baz 的初始化器成功地调用了 Base.initWihValue:(int) value,但当它调用 Base 的指定初始化器时,这就是初始化器的超级虚拟功能发挥作用的时候,并且会尝试在 Baz 上调用这个不存在的方法。通过添加该方法(例如,调用超类方法)可以轻松修复此问题,如下所示
class Baz : Base { init() { super.init(value: 7) } init(value value:Int, AndAnother value1:Int) { super.init(value: value, andAnother: value1); } }
给出结果
2014-06-07 17:40:18.632 Test[47394:303] One:7, Two:2
或者,遵循 Swift 的方式,无参数初始化器显然是一个便利初始化器,应该这样标记。这会导致编译失败,因为现在禁止它调用超类初始化器,而是必须交叉调用指定初始化器,从而得到以下代码
class Baz : Base { convenience init() { self.init(value: 7, AndAnother: 8) } init(value value:Int, AndAnother value1:Int) { super.init(value: value, andAnother: value1); } }
这给出了结果
2014-06-07 17:40:46.345 Test[47407:303] One:7, Two:8
这与前一个版本不同,因为便利初始化器现在调用 Baz 中的指定初始化器,这意味着 -(id)initWithValue:(NSInteger)
value 永远不会被调用,这就是为什么第二个值不再是 2。
结论
虽然这解释了我继承 SKSpriteNode
时看到的行为,并且通过提供其初始化器的镜像而不将任何一个标记为便利,它解决了使用便利的 init(imageNamed name: string)
方法的问题,但它并不特别优雅,也不是完全符合 Swift 风格,因为它让类拥有一组初始化器,其中没有一个被标记为便利。
可以通过将 init(texture texture: SKTexture!, color color: UIColor!, size size: CGSize)
方法以外的所有方法标记为便利并让它们交叉调用这个方法来指定一个指定的初始化器。然而,这将阻止直接调用 init(imageNamed name: string)
,而这比其他方法要方便得多。
讽刺的是,虽然 Swift 正式化了指定初始化器的概念,并且它看起来对纯 Swift 代码非常有效,但在继承 Objective-C 类时,它可能会有些不方便。
附注:对于我们这些拼写“convenience”一词有困难的人来说,它远非方便。标记指定初始化器的关键字可能更方便,而且可能更容易拼写!