Tại sao các trình khởi tạo Swift không thể gọi các trình khởi tạo tiện lợi trên lớp cha của chúng?


88

Hãy xem xét hai lớp:

class A {
    var x: Int

    init(x: Int) {
        self.x = x
    }

    convenience init() {
        self.init(x: 0)
    }
}

class B: A {
    init() {
        super.init() // Error: Must call a designated initializer of the superclass 'A'
    }
}

Tôi không hiểu tại sao điều này không được phép. Cuối cùng, khởi tạo được chỉ định của mỗi lớp được gọi với bất kỳ giá trị mà họ cần, vậy tại sao tôi cần phải lặp lại bản thân mình trong B's initbằng cách xác định một giá trị mặc định cho xmột lần nữa, khi sự tiện lợi inittrong Asẽ làm tốt?


2
Tôi đã tìm kiếm một câu trả lời nhưng không thể tìm thấy câu trả lời nào làm tôi hài lòng. Đó có thể là một số lý do thực hiện. Có thể việc tìm kiếm các trình khởi tạo được chỉ định trong một lớp khác dễ dàng hơn nhiều so với việc tìm kiếm các trình khởi tạo tiện lợi ... hoặc một cái gì đó tương tự.
Sulthan

@Robert, cảm ơn bạn đã bình luận bên dưới. Tôi nghĩ rằng bạn có thể thêm chúng vào câu hỏi của mình, hoặc thậm chí đăng câu trả lời với những gì bạn nhận được: "Đây là do thiết kế và mọi lỗi có liên quan đã được sắp xếp trong lĩnh vực này". Vì vậy, có vẻ như họ không thể hoặc không muốn giải thích lý do.
Ferran Maylinch

Câu trả lời:


24

Đây là Quy tắc 1 của quy tắc "Chuỗi khởi tạo" như được chỉ định trong Hướng dẫn lập trình Swift, có nội dung:

Quy tắc 1: Bộ khởi tạo được chỉ định phải gọi bộ khởi tạo được chỉ định từ lớp cha ngay lập tức của chúng.

https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Initialization.html

Nhấn mạnh của tôi. Các bộ khởi tạo được chỉ định không thể gọi các bộ khởi tạo tiện lợi.

Có một sơ đồ cùng với các quy tắc để chứng minh những "hướng dẫn" của trình khởi tạo nào được phép:

Chuỗi khởi tạo


80
Nhưng tại sao nó được thực hiện theo cách này? Tài liệu chỉ nói rằng nó đơn giản hóa thiết kế, nhưng tôi không hiểu trường hợp này xảy ra như thế nào nếu tôi phải tiếp tục tự lặp lại bằng cách liên tục chỉ định các giá trị mặc định trên các bộ khởi tạo được chỉ định mà tôi hầu như không quan tâm khi các bộ mặc định thuận tiện khởi tạo sẽ làm gì?
Robert

5
Tôi đã gửi lỗi 17266917 vài ngày sau câu hỏi này, yêu cầu có thể gọi các trình khởi tạo tiện lợi. Chưa có phản hồi nào, nhưng tôi cũng chưa có bất kỳ báo cáo lỗi Swift nào khác của mình!
Robert

8
Hy vọng rằng chúng sẽ cho phép gọi các khởi tạo tiện lợi. Đối với một số lớp trong SDK, không có cách nào khác để đạt được một hành vi nhất định ngoài việc gọi trình khởi tạo tiện lợi. Hãy xem SCNGeometry: bạn chỉ có thể thêm SCNGeometryElements bằng cách sử dụng trình khởi tạo tiện lợi, do đó người ta không thể kế thừa từ nó.
Spak

4
Đây là một quyết định thiết kế ngôn ngữ rất kém. Từ khi bắt đầu của ứng dụng mới của tôi, tôi quyết định phát triển với nhanh chóng tôi cần phải gọi NSWindowController.init (windowNibName) từ lớp con tôi, và tôi không thể làm điều đó :(
Uniqus

4
@Kaiserludi: Không có gì hữu ích, nó đã bị đóng lại với phản hồi sau: "Đây là do thiết kế và mọi lỗi liên quan đã được sắp xếp trong lĩnh vực này."
Robert

21

Xem xét

class A
{
    var a: Int
    var b: Int

    init (a: Int, b: Int) {
        print("Entering A.init(a,b)")
        self.a = a; self.b = b
    }

    convenience init(a: Int) {
        print("Entering A.init(a)")
        self.init(a: a, b: 0)
    }

    convenience init() {
        print("Entering A.init()")
        self.init(a:0)
    }
}


class B : A
{
    var c: Int

    override init(a: Int, b: Int)
    {
        print("Entering B.init(a,b)")
        self.c = 0; super.init(a: a, b: b)
    }
}

var b = B()

Vì tất cả các trình khởi tạo được chỉ định của lớp A đều bị ghi đè, nên lớp B sẽ kế thừa tất cả các trình khởi tạo tiện lợi của A. Vì vậy, việc thực thi điều này sẽ xuất

Entering A.init()
Entering A.init(a:)
Entering B.init(a:,b:)
Entering A.init(a:,b:)

Bây giờ, nếu bộ khởi tạo được chỉ định B.init (a: b :) được phép gọi bộ khởi tạo tiện lợi lớp cơ sở A.init (a :), điều này sẽ dẫn đến một cuộc gọi đệ quy tới B.init (a:, b: ).


Thật dễ dàng để làm việc xung quanh. Ngay sau khi bạn gọi một trình khởi tạo tiện lợi của lớp cha từ bên trong một trình khởi tạo được chỉ định, thì các trình khởi tạo tiện lợi của lớp cha sẽ không còn được kế thừa nữa.
siêu âm

@fluidsonic nhưng điều đó sẽ thật điên rồ vì cấu trúc và phương thức lớp của bạn sẽ thay đổi tùy thuộc vào cách chúng được sử dụng. Hãy tưởng tượng niềm vui gỡ lỗi!
kdazzle

2
@kdazzle Cả cấu trúc của lớp cũng như các phương thức lớp của nó sẽ không thay đổi. Tại sao họ phải? - Vấn đề duy nhất tôi có thể nghĩ đến là các trình khởi tạo tiện lợi phải ủy quyền động cho bộ khởi tạo được chỉ định của lớp con khi được kế thừa và tĩnh cho bộ khởi tạo được chỉ định của lớp đó khi không được kế thừa nhưng được ủy quyền từ một lớp con.
Fluidsonic,

Tôi nghĩ rằng bạn cũng có thể gặp vấn đề đệ quy tương tự với các phương pháp thông thường. Điều đó phụ thuộc vào quyết định của bạn khi gọi các phương thức. Ví dụ, sẽ thật ngớ ngẩn nếu một ngôn ngữ không cho phép gọi đệ quy vì bạn có thể mắc vào một vòng lặp vô hạn. Lập trình viên nên hiểu những gì họ đang làm. :)
Ferran Maylinch

13

Đó là bởi vì bạn có thể kết thúc với một đệ quy vô hạn. Xem xét:

class SuperClass {
    init() {
    }

    convenience init(value: Int) {
        // calls init() of the current class
        // so init() for SubClass if the instance
        // is a SubClass
        self.init()
    }
}

class SubClass : SuperClass {
    override init() {
        super.init(value: 10)
    }
}

và nhìn vào:

let a = SubClass()

cái nào sẽ gọi SubClass.init()cái nào sẽ gọi SuperClass.init(value:)cái nào sẽ gọi SubClass.init().

Các quy tắc init được chỉ định / tiện lợi được thiết kế để khởi tạo lớp sẽ luôn đúng.


1
Mặc dù một lớp con có thể không gọi rõ ràng các bộ khởi tạo tiện lợi từ lớp cha của nó, nhưng nó có thể kế thừa chúng, cho rằng lớp con cung cấp việc triển khai tất cả các bộ khởi tạo được chỉ định của lớp cha ( ví dụ như ví dụ này ). Do đó, ví dụ của bạn ở trên thực sự đúng, nhưng là một trường hợp đặc biệt không liên quan rõ ràng đến trình khởi tạo tiện lợi của lớp siêu, mà là thực tế là trình khởi tạo được chỉ định có thể không gọi là tiện lợi , vì điều này sẽ dẫn đến các trường hợp đệ quy như trường hợp trên .
dfrib

1

Tôi đã tìm thấy một công việc xung quanh cho điều này. Nó không quá đẹp, nhưng nó giải quyết vấn đề không biết giá trị của lớp cha hoặc muốn đặt giá trị mặc định.

Tất cả những gì bạn phải làm là tạo một thể hiện của lớp cha, sử dụng sự tiện lợi init, ngay trong initlớp con. Sau đó, bạn gọi chỉ định initcủa siêu bằng cách sử dụng cá thể bạn vừa tạo.

class A {
    var x: Int

    init(x: Int) {
        self.x = x
    }

    convenience init() {
        self.init(x: 0)
    }
}

class B: A {
    init() {
        // calls A's convenience init, gets instance of A with default x value
        let intermediate = A() 

        super.init(x: intermediate.x) 
    }
}

1

Xem xét trích xuất mã khởi tạo từ init()chức năng trợ giúp thuận tiện của bạn sang một chức năng trợ giúp mới foo(), gọi foo(...)để thực hiện việc khởi tạo trong lớp con của bạn.


Đề xuất tốt, nhưng nó không thực sự trả lời câu hỏi.
SwiftsNamesake

0

Xem WWDC-video "403 Swift trung gian" lúc 18:30 để có giải thích sâu về các trình khởi tạo và kế thừa của chúng. Như tôi đã hiểu, hãy xem xét những điều sau:

class Dragon {
    var legs: Int
    var isFlying: Bool

    init(legs: Int, isFlying: Bool) {
        self.legs = legs
        self.isFlying = isFlying
    }

    convenience initWyvern() { 
        self.init(legs: 2, isFlying: true)
    }
}

Nhưng bây giờ hãy xem xét một lớp con Wyrm: Wyrm là một con Rồng không có chân và không có cánh. Vì vậy, khởi tạo cho Wyvern (2 chân, 2 cánh) là sai đối với nó! Lỗi đó có thể tránh được nếu không thể gọi Wyvern-Initializer tiện lợi mà chỉ gọi được Trình khởi tạo được chỉ định đầy đủ:

class Wyrm: Dragon {
    init() {
        super.init(legs: 0, isFlying: false)
    }
}

12
Đó không thực sự là một lý do. Điều gì sẽ xảy ra nếu tôi tạo một lớp con khi initWyvernđược gọi là hợp lý?
Sulthan

4
Vâng, tôi không bị thuyết phục. Không có gì ngăn cản việc ghi Wyrmđè số lượng chân sau khi nó gọi một bộ khởi tạo tiện lợi.
Robert

Đó là lý do được đưa ra trong video WWDC (chỉ với ô tô -> xe đua và hasTurbo-property kiểu boolean). Nếu lớp con triển khai tất cả các bộ khởi tạo được chỉ định, thì nó cũng kế thừa các bộ khởi tạo tiện lợi. Tôi gần như thấy ý nghĩa của nó, cũng thành thật mà nói, tôi không gặp nhiều khó khăn với cách Objective-C hoạt động. Xem thêm quy ước mới để gọi super.init cuối cùng trong init, không phải đầu tiên như trong Objective-C.
Ralf

IMO, quy ước gọi super.init "cuối cùng" không phải là mới về mặt khái niệm, nó giống như trong mục tiêu-c, chỉ là ở đó, mọi thứ đã được gán giá trị ban đầu (nil, 0, bất kỳ) tự động. Chỉ là trong Swift, chúng ta phải tự khởi tạo hệ điều hành giai đoạn này. Một lợi ích của cách tiếp cận này là chúng tôi cũng có một tùy chọn để chỉ định một giá trị ban đầu khác.
Roshan

-1

Tại sao bạn không chỉ có hai bộ khởi tạo - một bộ có giá trị mặc định?

class A {
  var x: Int

  init(x: Int) {
    self.x = x
  }

  init() {
    self.x = 0
  }
}

class B: A {
  override init() {
    super.init()

    // Do something else
  }
}

let s = B()
s.x // 0
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.