Swift类中的错误:super.init调用未初始化属性


218

我有两节课,ShapeSquare

class Shape {
    var numberOfSides = 0
    var name: String
    init(name:String) {
        self.name = name
    }
    func simpleDescription() -> String {
        return "A shape with \(numberOfSides) sides."
    }
}

class Square: Shape {
    var sideLength: Double

    init(sideLength:Double, name:String) {
        super.init(name:name) // Error here
        self.sideLength = sideLength
        numberOfSides = 4
    }
    func area () -> Double {
        return sideLength * sideLength
    }
}

通过上面的实现,我得到了错误:

property 'self.sideLength' not initialized at super.init call
    super.init(name:name)

为什么要self.sideLength在致电之前进行设置super.init


可以肯定,这与良好的编程习惯有关,而不是实际的技术限制。如果Shape调用了Square已覆盖的函数,Square可能要使用sideLength,但尚未初始化。Swift可能会通过强制您在调用基类之前先初始化实例来限制您的操作。
cwharris 2014年

3
书中的例子很糟糕。您应该始终最后调用super.init(),以确保所有属性都已初始化,如下所述。
帕斯卡

Answers:


173

引用Swift编程语言,它回答了您的问题:

“ Swift的编译器执行四项有用的安全检查,以确保两阶段初始化完成且没有错误:”

安全检查1“指定的初始值设定项必须确保由其类引入的所有属性在将其委托给超类初始值设定项之前都已初始化。”

摘录自:苹果公司“ The Swift Programming Language”。iBooks。 https://itunes.apple.com/cn/book/swift-programming-language/id881256329?mt=11


47
与C ++,C#或Java相比,这是一个惊人的变化。
MDJ 2014年

12
@MDJ当然可以。老实说,我也看不到它的附加价值。
鲁宾2014年

20
实际上似乎有一个。用C#说,超类构造函数不应调用任何可重写的(虚拟)方法,因为没有人知道它们对未完全初始化的子类将如何反应。在Swift中还可以,因为当超类构造函数正在运行时,可以使用子类额外状态。而且,在Swift中,除最终方法之外的所有方法都是可重写的。
MDJ 2014年

17
我特别觉得很烦,因为这意味着,如果我要创建一个创建自己的子视图的UIView子类,则必须初始化这些没有框架的子视图,然后再添加框架,因为我无法引用该视图的绑定直到调用super.init之后。
2014年

5
@Janos如果将属性设为可选,则无需在中初始化它init
JeremyP,2015年

105

Swift具有非常清晰,特定的操作序列,这些序列在初始化程序中完成。让我们从一些基本示例开始,然后逐步发展到一般情况。

让我们看一个对象A。我们将其定义如下。

class A {
    var x: Int
    init(x: Int) {
        self.x = x
    }
}

注意,A没有超类,因此它不能调用super.init()函数,因为它不存在。

好的,现在让我们使用名为B的新类作为A的子类。

class B: A {
    var y: Int
    init(x: Int, y: Int) {
        self.y = y
        super.init(x: x)
    }
}

这与Objective-C有所不同,在Objective-C中[super init],通常会先调用它。在Swift中不是这样。在执行任何其他操作(包括调用方法(包括您的超类的初始化程序))之前,您有责任确保实例变量处于一致状态。


1
这是一个非常有用的例子。谢谢!
FullMetalFist

如果我需要用于计算y的值怎么办,例如:init(y:Int){self.y = y * self.x super.init()}
6rod9

1
使用类似init(y:Int,x:Int = 0){self.y = y * x; self.x = x; super.init(x:x)},也不能参考上面的示例直接为超类调用空的构造函数,因为超类名称A没有空的构造函数
Hitendra Solanki

43

来自文档

安全检查1

指定的初始值设定项必须确保由其类引入的所有属性在委托给超类初始值设定项之前都已初始化。


为什么我们需要这样的安全检查?

为了解决这个问题,让我们迅速进行初始化过程。

两阶段初始化

Swift中的类初始化是一个两阶段的过程。在第一阶段,每个存储的属性都由引入它的类分配一个初始值。一旦确定了每个存储属性的初始状态,便开始第二阶段,并且在认为新实例可以使用之前,每个类都有机会自定义其存储属性。

两阶段初始化过程的使用使初始化安全,同时仍然为类层次结构中的每个类提供完全的灵活性。两阶段初始化可防止在初始化属性值之前访问属性值,并防止其他初始化程序意外地将属性值设置为其他值。

因此,为确保按照上述定义完成两步初始化过程,需要进行四项安全检查,其中一项是,

安全检查1

指定的初始值设定项必须确保由其类引入的所有属性在委托给超类初始值设定项之前都已初始化。

现在,两阶段初始化从未讨论过顺序,但是super.init在所有属性初始化之后,此安全检查将引入有序命令。

安全检查1似乎无关紧要,因为如果没有此安全检查1 ,则 两阶段初始化会阻止在属性值被满足之前访问属性值

像这个样本一样

class Shape {
    var name: String
    var sides : Int
    init(sides:Int, named: String) {
        self.sides = sides
        self.name = named
    }
}

class Triangle: Shape {
    var hypotenuse: Int
    init(hypotenuse:Int) {
        super.init(sides: 3, named: "Triangle") 
        self.hypotenuse = hypotenuse
    }
}

Triangle.init在使用之前,已经初始化了每个属性。因此,安全检查1似乎无关紧要,

但是然后可能会有另一种情况,有点复杂,

class Shape {
    var name: String
    var sides : Int
    init(sides:Int, named: String) {
        self.sides = sides
        self.name = named
        printShapeDescription()
    }
    func printShapeDescription() {
        print("Shape Name :\(self.name)")
        print("Sides :\(self.sides)")
    }
}

class Triangle: Shape {
    var hypotenuse: Int
    init(hypotenuse:Int) {
        self.hypotenuse = hypotenuse
        super.init(sides: 3, named: "Triangle")
    }

    override func printShapeDescription() {
        super.printShapeDescription()
        print("Hypotenuse :\(self.hypotenuse)")
    }
}

let triangle = Triangle(hypotenuse: 12)

输出:

Shape Name :Triangle
Sides :3
Hypotenuse :12

在这里,如果我们super.init在设置之前调用过hypotenuse,则该super.init调用将调用,printShapeDescription()并且由于已被覆盖,因此它将首先回退到的Triangle类实现printShapeDescription()printShapeDescription()Triangle类的of访问hypotenuse一个非可选属性,该属性尚未初始化。这是不允许的,因为两阶段初始化阻止了在初始化属性值之前对其进行访问

因此,请确保两阶段初始化已按定义完成,需要有一个特定的调用顺序super.init,也就是说,在初始化由self类引入的所有属性之后,因此我们需要进行安全检查1


1
很好的解释,为什么一定要添加到最上面的答案。
Guy Daher

因此,您基本上是在说,因为超类init 可能会调用一个(重写的)函数...,在该函数中,该函数将访问subclasses属性,因此为了避免未设置值,super必须在设置所有值之后进行对的调用。确定是有道理的。想知道Objective-C当时是如何做到的,以及为什么您必须先打电话super
亲爱的

本质上,您要指向的内容类似于printShapeDescription() self.sides = sides; self.name = named;之前放置会产生此错误:use of 'self' in method call 'printShapeDescription' before all stored properties are initialized。给出OP错误是为了减少运行时错误的“可能性”。
亲爱的

我专门使用了“可能性”一词,因为如果printShapeDescription一个函数没有被引用,self即像“ print(“ nothing”)之类的东西,那么就不会有问题。(尽管如此,编译器仍会抛出错误,因为它不是超级聪明)
Honey

好吧,objc并不安全。Swift是类型安全的,因此非可选对象确实需要为null!
Daij-Djan '17

36

在初始化所有实例变量之后,应调用“ super.init()”。

在Apple的“中级Swift”视频(您可以在Apple Developer视频资源页面https://developer.apple.com/videos/wwdc/2014/上找到)中,明确指出在28:40在初始化实例变量之后,必须调用父类。

在Objective-C中,情况恰恰相反。在Swift中,由于所有属性都需要在使用前进行初始化,因此我们需要先初始化属性。这是为了防止从超类的“ init()”方法调用重写的函数,而无需先初始化属性。

因此,“ Square”的实现应为:

class Square: Shape {
    var sideLength: Double

    init(sideLength:Double, name:String) {
        self.sideLength = sideLength
        numberOfSides = 4
        super.init(name:name) // Correct position for "super.init()"
    }
    func area () -> Double {
        return sideLength * sideLength
    }
}

1
永远不会猜到。用super初始化应该是我最初的想法。!! 嗯。快速变化。
mythicalcoder

为什么需要这样做?请提供技术原因
Masih

14

抱歉,格式化不正确。只需在声明后放置问号,一切都会好起来的。一个问题告诉编译器该值是可选的。

class Square: Shape {
    var sideLength: Double?   // <=== like this ..

    init(sideLength:Double, name:String) {
        super.init(name:name) // Error here
        self.sideLength = sideLength
        numberOfSides = 4
    }
    func area () -> Double {
        return sideLength * sideLength
    }
}

编辑1:

有一种更好的方法来跳过此错误。根据jmaschad的评论,由于需要使用可选参数,因此没有理由使用可选参数,因为可选参数使用起来不方便,并且在访问可选参数之前,您始终必须检查其是否为nil。因此,您要做的就是在声明后初始化成员:

class Square: Shape {
    var sideLength: Double=Double()   

    init(sideLength:Double, name:String) {
        super.init(name:name)
        self.sideLength = sideLength
        numberOfSides = 4
    }
    func area () -> Double {
        return sideLength * sideLength
    }
}

编辑2:

经过两个缺点后,我找到了更好的方法。如果要在构造函数中初始化类成员,则必须在构造函数内部以及在super.init()调用之前为其分配初始值。像这样:

class Square: Shape {
    var sideLength: Double  

    init(sideLength:Double, name:String) {
        self.sideLength = sideLength   // <= before super.init call..
        super.init(name:name)
        numberOfSides = 4
    }
    func area () -> Double {
        return sideLength * sideLength
    }
}

祝您学习Swift时好运。


只需切换super.init(name:name)self.sideLength = sideLength。声明sideLength为“可选”是一种误导,并在以后不得不强制打开包装时引入了其他麻烦。
Johannes Luong

是的,这是一个选择。谢谢
fnc12 2014年

您实际上可以拥有var sideLength: Double,而无需为其分配初始值
Jarsen 2014年

如果我真的有一个可选常数怎么办。我该怎么办?我必须在构造函数中初始化吗?我不明白您为什么必须这样做,但是编译器在Swift 1.2中抱怨
Van Du Tran 2015年

1
太好了!3个解决方案都可以使用“?”,“ String()”,但是对我来说,问题是我没有“分配”该属性中的一个,而当我这样做时它就起作用了!感谢队友
Naishta '16

9

swift会强制您初始化每个成员变量,然后再使用它。由于无法确定超级转弯时会发生什么,因此会出错:安全性胜过遗憾


1
IMO这样做没有意义,因为父类对子元素中声明的属性不具有可见性!
安迪·辛

1
它没有,但是您可以覆盖东西并“在超级制造之前”开始使用自我
Daij-Djan

您可以在这里看到它后面的评论吗?我想我是在说您的意思,即我们都在说编译器希望安全而不是后悔,我唯一的问题是,那么Objective-C如何解决这个问题?还是没有?如果不是,那为什么还需要您super.init在第一行写?
亲爱的

7

爱德华

您可以像下面这样修改示例中的代码:

var playerShip:PlayerShip!
var deltaPoint = CGPointZero

init(size: CGSize)
{
    super.init(size: size)
    playerLayerNode.addChild(playerShip)        
}

这使用了一个隐式解包的可选。

在文档中,我们可以阅读:

“与可选选项一样,如果在声明隐式解包的可选变量或属性时不提供初始值,则其值将自动默认为nil。”


我认为这是最干净的选择。在我的第一次Swift尝试中,我有一个AVCaptureDevice类型的成员变量,该成员变量无法直接实例化,因此需要init()代码。但是,ViewController需要多个初始化程序,您不能从init()调用通用的初始化方法,因此,此答案似乎是避免在每个初始化程序中复制/粘贴重复代码的唯一选择。
sunetos 2014年


6

在声明的末尾添加nil。


// Must be nil or swift complains
var someProtocol:SomeProtocol? = nil

// Init the view
override init(frame: CGRect)
    super.init(frame: frame)
    ...

这适用于我的情况,但可能不适用于您的情况


好人,同时将UIView与带有协议的UIViewController一起使用
Abdul sathar

1

您只是以错误的顺序进行初始化。

     class Shape2 {
        var numberOfSides = 0
        var name: String
        init(name:String) {
            self.name = name
        }
        func simpleDescription() -> String {
            return "A shape with \(numberOfSides) sides."
        }
    }

    class Square2: Shape2 {
        var sideLength: Double

        init(sideLength:Double, name:String) {

            self.sideLength = sideLength
            super.init(name:name) // It should be behind "self.sideLength = sideLength"
            numberOfSides = 4
        }
        func area () -> Double {
            return sideLength * sideLength
        }
    }

0

我可能会收到一些反对意见,但是老实说,这样生活会更轻松:

class CSListServerData<ListItem: CSJsonData>: CSServerData {
    var key: String!
    var type: ListItem.Type!
    var property: CSJsonDataList<ListItem>!

    func construct(_ key: String, _ type: ListItem.Type) -> Self {
        self.key = key
        self.type = type
        property = CSJsonDataList(self, type, key)
        return self
    }

    func construct(_ type: ListItem.Type) { construct("list", type) }

    var list: [ListItem] { property.list }
}

-4

这是一个非常愚蠢的设计。

考虑这样的事情:

.
.
.
var playerShip:PlayerShip
var deltaPoint = CGPointZero

init(size: CGSize)
{
    super.init(size: size)
    playerShip = PlayerShip(pos: CGPointMake(self.size.width / 2.0, 100))
    playerLayerNode.addChild(playerShip)        
}
.
.
.

如上所述,这是无效的。但是是这样的:

.
.
.
var playerShip:PlayerShip = PlayerShip(pos: CGPointMake(self.size.width / 2.0, 100))
var deltaPoint = CGPointZero

init(size: CGSize)
{
    super.init(size: size)
    playerLayerNode.addChild(playerShip)        
}
.
.
.

因为“自我”尚未初始化。

我衷心希望此错误能尽快修复。

(是的,我知道我可以创建一个空对象,然后设置大小,但这只是愚蠢的)。


2
这不是bug,它是OOP的新编程基础。
Hitendra Solanki 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.