Lỗi trong lớp Swift: Thuộc tính không được khởi tạo tại cuộc gọi super.init


218

Tôi có hai lớp 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
    }
}

Với cách thực hiện ở trên, tôi nhận được lỗi:

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

Tại sao tôi phải đặt self.sideLengthtrước khi gọi super.init?


khá chắc chắn rằng điều này có liên quan đến thực tiễn lập trình tốt, không phải là một hạn chế kỹ thuật thực tế. Nếu Shape định gọi một hàm mà Square đã ghi đè, Square có thể muốn sử dụng sideL ngài, nhưng nó chưa được khởi tạo. Swift có lẽ chỉ hạn chế bạn làm điều này một cách tình cờ bằng cách buộc bạn phải khởi tạo các thể hiện của mình trước khi gọi lớp cơ sở.
cwharris

3
Các ví dụ trong cuốn sách là xấu. Bạn phải luôn gọi super.init () lần cuối để chắc chắn rằng tất cả các thuộc tính đã được khởi tạo, được giải thích bên dưới.
Pascal

Câu trả lời:


173

Trích dẫn từ Ngôn ngữ lập trình Swift, trả lời câu hỏi của bạn:

Trình biên dịch của Swift Swift thực hiện bốn kiểm tra an toàn hữu ích để đảm bảo rằng việc khởi tạo hai pha được hoàn thành mà không gặp lỗi:

Kiểm tra an toàn 1 Ban Một trình khởi tạo được chỉ định phải đảm bảo rằng tất cả các thuộc tính của Giới thiệu về lớp được giới thiệu đều được khởi tạo trước khi ủy quyền cho trình khởi tạo siêu lớp.

Trích từ: Apple Inc., Ngôn ngữ lập trình Swift. Sách điện tử. https://itunes.apple.com/us/book/swift-programming-lingu/id881256329?mt=11


47
Bây giờ, đó là một thay đổi đáng kinh ngạc so với C ++, C # hoặc Java.
MDJ

12
@MDJ Chắc chắn rồi. Tôi thực sự không thấy giá trị gia tăng của nó.
Ruben

20
Trên thực tế dường như có một. Nói trong C #, hàm tạo siêu lớp không nên gọi bất kỳ phương thức overridable (ảo) nào, bởi vì không ai biết chúng sẽ phản ứng như thế nào với lớp con không được khởi tạo đầy đủ. Trong Swift thì ổn, vì trạng thái lớp con thêm là tốt, khi hàm tạo của lớp bậc trên đang chạy. Hơn nữa, trong Swift tất cả nhưng các phương thức cuối cùng là quá mức.
MDJ

17
Tôi đặc biệt thấy khó chịu vì điều đó có nghĩa là, nếu tôi muốn tạo một lớp con UIView tạo ra các cuộc phỏng vấn của riêng mình, tôi phải khởi tạo các cuộc phỏng vấn đó mà không có khung để bắt đầu và thêm các khung sau vì tôi không thể tham khảo khung nhìn giới hạn cho đến khi SAU gọi super.init.
Tro

5
@Janos nếu bạn đặt thuộc tính tùy chọn, bạn không phải khởi tạo nó init.
JeremyP

105

Swift có một chuỗi các hoạt động cụ thể, rõ ràng được thực hiện trong các công cụ khởi tạo. Hãy bắt đầu với một số ví dụ cơ bản và tiến tới một trường hợp chung.

Chúng ta hãy lấy một đối tượng A. Chúng ta sẽ định nghĩa nó như sau.

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

Lưu ý rằng A không có siêu lớp, vì vậy nó không thể gọi hàm super.init () vì nó không tồn tại.

OK, vậy bây giờ hãy để lớp con A với một lớp mới có tên B.

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

Đây là một sự khởi hành từ Objective-C, nơi [super init]thường được gọi đầu tiên trước mọi thứ khác. Không như vậy trong Swift. Bạn chịu trách nhiệm đảm bảo rằng các biến đối tượng của bạn ở trạng thái nhất quán trước khi bạn làm bất cứ điều gì khác, bao gồm các phương thức gọi (bao gồm trình khởi tạo siêu lớp của bạn).


1
Điều này là vô cùng hữu ích, một ví dụ rõ ràng. cảm ơn bạn!
FullMetalFist

Điều gì xảy ra nếu tôi cần giá trị để tính y, ví dụ: init (y: Int) {self.y = y * self.x super.init ()}
6rod9

1
sử dụng cái gì đó như, init (y: Int, x: Int = 0) {self.y = y * x; tự.x = x; super.init (x: x)}, bạn cũng không thể gọi trực tiếp hàm tạo rỗng cho siêu lớp có tham chiếu đến ví dụ trên, vì tên siêu hạng A không có hàm tạo trống
Hitendra Solanki

43

Từ các tài liệu

Kiểm tra an toàn 1

Một trình khởi tạo được chỉ định phải đảm bảo rằng tất cả các thuộc tính được giới thiệu bởi lớp của nó được khởi tạo trước khi nó ủy quyền cho một trình khởi tạo siêu lớp.


Tại sao chúng ta cần kiểm tra an toàn như thế này?

Để trả lời điều này hãy đi qua mặc dù quá trình khởi tạo nhanh chóng.

Khởi tạo hai pha

Khởi tạo lớp trong Swift là một quá trình hai giai đoạn. Trong pha đầu tiên, mỗi thuộc tính được lưu trữ được gán một giá trị ban đầu bởi lớp đã giới thiệu nó. Khi trạng thái ban đầu cho mọi thuộc tính được lưu trữ đã được xác định, giai đoạn thứ hai bắt đầu và mỗi lớp sẽ có cơ hội tùy chỉnh các thuộc tính được lưu trữ của nó trước khi thể hiện mới được coi là sẵn sàng để sử dụng.

Việc sử dụng quy trình khởi tạo hai pha giúp khởi tạo an toàn, trong khi vẫn mang lại sự linh hoạt hoàn toàn cho mỗi lớp trong hệ thống phân cấp lớp. Khởi tạo hai pha ngăn không cho các giá trị thuộc tính được truy cập trước khi chúng được khởi tạo và ngăn các giá trị thuộc tính được đặt thành giá trị khác bởi một trình khởi tạo khác bất ngờ.

Vì vậy, để đảm bảo quá trình khởi tạo hai bước được thực hiện như được xác định ở trên, có bốn kiểm tra an toàn, một trong số đó là,

Kiểm tra an toàn 1

Một trình khởi tạo được chỉ định phải đảm bảo rằng tất cả các thuộc tính được giới thiệu bởi lớp của nó được khởi tạo trước khi nó ủy quyền cho một trình khởi tạo siêu lớp.

Bây giờ, khởi tạo hai pha không bao giờ nói về thứ tự, nhưng kiểm tra an toàn này, giới thiệu super.initsẽ được đặt hàng, sau khi khởi tạo tất cả các thuộc tính.

Kiểm tra an toàn 1 có vẻ không liên quan vì, Khởi tạo hai pha ngăn các giá trị thuộc tính không được truy cập trước khi chúng được khởi tạo có thể được thỏa mãn, mà không cần kiểm tra an toàn 1 này.

Giống như trong mẫu này

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đã khởi tạo, mọi tài sản trước khi được sử dụng. Vì vậy, kiểm tra an toàn 1 dường như không liên quan,

Nhưng sau đó có thể có một kịch bản khác, một chút phức tạp,

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)

Đầu ra:

Shape Name :Triangle
Sides :3
Hypotenuse :12

Ở đây nếu chúng ta đã gọi super.inittrước khi cài đặt hypotenuse, thì super.initcuộc gọi sẽ gọi printShapeDescription()và vì nó đã bị ghi đè, trước tiên nó sẽ dự phòng cho việc thực hiện lớp Tam giác printShapeDescription(). Lớp printShapeDescription()Tam giác truy cập vào hypotenusemột thuộc tính không tùy chọn vẫn chưa được khởi tạo. Và điều này không được phép vì khởi tạo hai pha ngăn giá trị thuộc tính được truy cập trước khi chúng được khởi tạo

Vì vậy, hãy đảm bảo việc khởi tạo Hai pha được thực hiện như được xác định, cần phải có một thứ tự gọi cụ thể super.initvà sau khi khởi tạo tất cả các thuộc tính được giới thiệu bởi selflớp, do đó chúng ta cần Kiểm tra an toàn 1


1
Giải thích tuyệt vời, tại sao chắc chắn nên thêm vào câu trả lời hàng đầu.
Guy Daher

Vì vậy, về cơ bản bạn đang nói, bởi vì siêu lớp init có thể gọi hàm (overriden) ... trong đó hàm đó truy cập vào thuộc tính của lớp con, sau đó để tránh không đặt giá trị, lệnh gọi superphải xảy ra sau khi tất cả các giá trị được đặt. OK có ý nghĩa. Tự hỏi làm thế nào Objective-C đã làm điều đó sau đó và tại sao bạn phải gọi superđầu tiên?
Mật ong

Về cơ bản những gì bạn đang chỉ ra tương tự như : đặt printShapeDescription() trước self.sides = sides; self.name = named; đó sẽ tạo ra lỗi này : use of 'self' in method call 'printShapeDescription' before all stored properties are initialized. Lỗi của OP được đưa ra để giảm 'khả năng' lỗi thời gian chạy.
Mật ong

Tôi đặc biệt sử dụng từ 'khả năng', bởi vì nếu đó printShapeDescriptionlà một chức năng không đề cập đến, selfnghĩa là nó giống như 'print ("nothing") thì sẽ không có vấn đề gì. (Tuy nhiên, ngay cả đối với trình biên dịch sẽ ném ra một lỗi, bởi vì nó không phải là siêu thông minh)
Mật ong

Vâng objc không an toàn. Swift là loại an toàn vì vậy các đối tượng không phải là tùy chọn thực sự cần phải là nonnil!
Daij-Djan

36

"Super.init ()" nên được gọi sau khi bạn khởi tạo tất cả các biến thể hiện của mình.

Trong video "Trung cấp Swift" của Apple (bạn có thể tìm thấy nó trong trang tài nguyên video của Nhà phát triển Apple https://developer.apple.com/ideo/wwdc/2014/ ), vào khoảng 28:40, rõ ràng là tất cả các công cụ khởi tạo trong siêu lớp phải được gọi SAU KHI bạn khởi tạo các biến thể hiện của mình.

Trong Objective-C, nó là đảo ngược. Trong Swift, vì tất cả các thuộc tính cần được khởi tạo trước khi sử dụng, nên chúng ta cần khởi tạo các thuộc tính trước. Điều này có nghĩa là để ngăn chặn một lệnh gọi hàm được ghi đè từ phương thức "init ()" của siêu lớp, mà không khởi tạo các thuộc tính trước.

Vì vậy, việc thực hiện "Quảng trường" phải là:

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
Không bao giờ có thể đoán được. Khởi tạo với siêu nên là tuyên bố nắm tay là những gì tôi đã nghĩ. !! hm khá thay đổi với swift.
huyền thoại mã hóa

Tại sao điều này cần phải đến sau? Vui lòng cung cấp lý do kỹ thuật
Masih

14

Xin lỗi vì định dạng xấu. Chỉ cần đặt một câu hỏi nhân vật sau khi tuyên bố và mọi thứ sẽ ổn. Một câu hỏi cho trình biên dịch rằng giá trị là tùy chọn.

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
    }
}

Chỉnh sửa1:

Có một cách tốt hơn để bỏ qua lỗi này. Theo nhận xét của jmaschad, không có lý do gì để sử dụng tùy chọn trong trường hợp của bạn vì các tùy chọn không thoải mái khi sử dụng và Bạn luôn phải kiểm tra xem tùy chọn có không không trước khi truy cập. Vì vậy, tất cả những gì bạn phải làm là khởi tạo thành viên sau khi khai báo:

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
    }
}

Chỉnh sửa2:

Sau hai câu trả lời về câu trả lời này, tôi thấy cách tốt hơn nữa. Nếu bạn muốn thành viên lớp được khởi tạo trong hàm tạo của mình, bạn phải gán giá trị ban đầu cho nó bên trong bộ điều khiển và trước lệnh gọi super.init (). Như thế này:

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
    }
}

Chúc may mắn khi học Swift.


Chỉ cần chuyển đổi super.init(name:name)self.sideLength = sideLength. Tuyên bố sideLengthlà tùy chọn là sai lầm và đưa ra rắc rối bổ sung sau này khi bạn phải buộc mở khóa.
Julian Lương

Vâng, đây là một lựa chọn. Cảm ơn
fnc12

Bạn thực sự có thể có var sideLength: Double, không cần gán cho nó một giá trị ban đầu
Jarsen

Điều gì xảy ra nếu tôi thực sự có một hằng số tùy chọn. Tôi phải làm gì với nó? Tôi có phải khởi tạo trong hàm tạo không? Tôi không thấy lý do tại sao bạn phải làm điều đó, nhưng trình biên dịch đang phàn nàn trong Swift 1.2
Van Du Tran

1
Hoàn hảo ! cả 3 giải pháp đều hoạt động "?", "String ()", nhưng vấn đề đối với tôi là tôi đã không 'gán' một trong những tài sản và khi tôi làm điều đó thì nó đã hoạt động! Cảm ơn người bạn đời
Naishta

9

swift bắt buộc bạn phải khởi tạo mọi var thành viên trước khi nó được sử dụng. Vì không thể chắc chắn điều gì sẽ xảy ra khi đến lượt thay thế, nó báo lỗi: an toàn tốt hơn xin lỗi


1
Điều này không có ý nghĩa IMO vì lớp cha mẹ sẽ không có khả năng hiển thị các thuộc tính được khai báo trong đó là con!
Andy Hin

1
Không, nhưng bạn có thể ghi đè lên công cụ và bắt đầu sử dụng bản thân 'trước khi siêu thực hiện'
Daij-Djan

Bạn có thể xem ở đây và ý kiến ​​theo nó? Tôi nghĩ rằng tôi đang nói chính xác những gì bạn đang nói, tức là cả hai chúng tôi đều nói rằng trình biên dịch muốn an toàn hơn là xin lỗi, câu hỏi duy nhất của tôi là, vậy khách quan-c đã giải quyết vấn đề này như thế nào? Hay là không? Nếu không thì tại sao nó vẫn yêu cầu bạn viết super.initở dòng đầu tiên?
Mật ong

7

Edward,

Bạn có thể sửa đổi mã trong ví dụ của bạn như thế này:

var playerShip:PlayerShip!
var deltaPoint = CGPointZero

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

Điều này đang sử dụng một tùy chọn ngầm mở ra.

Trong tài liệu chúng ta có thể đọc:

"Giống như với các tùy chọn, nếu bạn không cung cấp giá trị ban đầu khi bạn khai báo một biến hoặc thuộc tính tùy chọn không được bao bọc hoàn toàn, thì giá trị đó sẽ tự động mặc định là không."


Tôi nghĩ rằng đây là lựa chọn sạch nhất. Trong lần thử Swift đầu tiên của mình, tôi đã có một biến thành viên loại AVCaptureDevice, không thể khởi tạo trực tiếp, do đó cần có mã init (). Tuy nhiên, ViewControll yêu cầu nhiều trình khởi tạo và bạn không thể gọi một phương thức khởi tạo chung từ init (), vì vậy câu trả lời này dường như là tùy chọn duy nhất tránh sao chép / dán mã trùng lặp trong mỗi trình khởi tạo.
sunetos

6

Swift sẽ không cho phép bạn khởi tạo siêu lớp mà không khởi tạo các thuộc tính, ngược lại với Obj C. Vì vậy, bạn phải khởi tạo tất cả các thuộc tính trước khi gọi "super.init".

Vui lòng truy cập http://blog.scottlogic.com/2014/11/20/swift-initialisation.html . Nó đưa ra một lời giải thích tốt đẹp cho vấn đề của bạn.


6

Thêm nil vào cuối khai báo.


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

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

Điều này làm việc cho trường hợp của tôi, nhưng có thể không làm việc cho trường hợp của bạn


Tốt, trong khi sử dụng với UIView với UIViewContoder với giao thức
abdul sathar

1

Bạn chỉ đang theo thứ tự sai.

     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

Tôi sẽ nhận được một số downvote có thể, nhưng thành thật mà nói, cuộc sống dễ dàng hơn theo cách này:

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

Đó là một thiết kế ngu ngốc đáng kinh ngạc.

Hãy xem xét một cái gì đó như thế này:

.
.
.
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)        
}
.
.
.

Điều này là không hợp lệ, như đã lưu ý ở trên. Nhưng cũng vậy:

.
.
.
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)        
}
.
.
.

Bởi vì 'bản thân' chưa được khởi tạo.

Tôi chân thành hy vọng lỗi này sẽ được khắc phục sớm.

(Có, tôi biết tôi có thể tạo một đối tượng trống và sau đó đặt kích thước nhưng điều đó thật ngu ngốc).


2
Đây không phải là lỗi, cơ bản lập trình mới của OOP.
Hitendra Solanki
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.