Mengapa penginisialisasi Swift tidak dapat memanggil penginisialisasi kenyamanan pada superclass mereka?

88

Pertimbangkan dua kelas:

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

Saya tidak mengerti mengapa ini tidak diizinkan. Pada akhirnya, initializer ditunjuk masing-masing kelas ini disebut dengan nilai-nilai yang mereka butuhkan, jadi mengapa saya harus mengulang sendiri di B's initdengan menentukan nilai default untuk xlagi, ketika kenyamanan initdi Aakan baik-baik saja?

Robert
sumber
2
Saya mencari jawaban tetapi saya tidak dapat menemukan jawaban yang dapat memuaskan saya. Ini mungkin beberapa alasan implementasi. Mungkin mencari penginisialisasi yang ditunjuk di kelas lain jauh lebih mudah daripada mencari penginisialisasi kenyamanan ... atau sesuatu seperti itu.
Sulthan
@Robert, terima kasih atas komentar Anda di bawah. Saya rasa Anda dapat menambahkannya ke pertanyaan Anda, atau bahkan memposting jawaban dengan jawaban yang Anda terima: "Ini memang disengaja dan semua bug yang relevan telah disortir di area ini.". Jadi sepertinya mereka tidak bisa atau tidak mau menjelaskan alasannya.
Ferran Maylinch

Jawaban:

24

Ini adalah Aturan 1 dari aturan "Penginisialisasi Chaining" seperti yang ditentukan dalam Panduan Pemrograman Swift, yang berbunyi:

Aturan 1: Penginisialisasi yang ditunjuk harus memanggil penginisialisasi yang ditentukan dari superkelas langsungnya.

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

Penekanan milikku. Penginisialisasi yang ditunjuk tidak dapat memanggil penginisialisasi praktis.

Ada diagram yang sesuai dengan aturan untuk menunjukkan "petunjuk" penginisialisasi apa yang diizinkan:

Rangkaian Penginisialisasi

Craig Otis
sumber
80
Tapi kenapa dilakukan seperti ini? Dokumentasi hanya mengatakan bahwa itu menyederhanakan desain, tetapi saya tidak melihat bagaimana ini terjadi jika saya harus terus mengulangi diri saya sendiri dengan terus menentukan nilai default pada penginisialisasi yang ditunjuk Saya sebagian besar tidak peduli ketika yang default dalam kenyamanan penginisialisasi akan dilakukan?
Robert
5
Saya melaporkan bug 17266917 beberapa hari setelah pertanyaan ini, meminta untuk dapat memanggil penginisialisasi kenyamanan. Belum ada tanggapan, tapi saya juga belum punya untuk laporan bug Swift saya yang lain!
Robert
8
Mudah-mudahan mereka akan memungkinkan untuk memanggil penginisialisasi kenyamanan. Untuk beberapa kelas di SDK, tidak ada cara lain untuk mencapai perilaku tertentu selain memanggil penginisialisasi praktis. Lihat SCNGeometry: Anda dapat menambahkan SCNGeometryElementhanya dengan menggunakan penginisialisasi praktis, oleh karena itu seseorang tidak dapat mewarisinya.
Bicaralah
4
Ini adalah keputusan desain bahasa yang sangat buruk. Dari awal aplikasi baru saya, saya memutuskan untuk mengembangkan dengan cepat, saya perlu memanggil NSWindowController.init (windowNibName) dari subkelas saya, dan saya tidak bisa melakukannya :(
Uniqus
4
@Kaiserludi: Tidak ada yang berguna, itu ditutup dengan tanggapan berikut: "Ini sesuai desain dan semua bug yang relevan telah diselesaikan di area ini."
Robert
21

Mempertimbangkan

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()

Karena semua penginisialisasi kelas A yang ditunjuk diganti, kelas B akan mewarisi semua penginisialisasi praktis dari A. Jadi mengeksekusi ini akan menampilkan

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

Sekarang, jika penginisialisasi B.init yang ditunjuk (a: b :) akan diizinkan untuk memanggil penginisialisasi kenyamanan kelas dasar A.init (a :), ini akan menghasilkan panggilan rekursif ke B.init (a:, b: ).

beba
sumber
Itu mudah untuk dikerjakan. Segera setelah Anda memanggil penginisialisasi kenyamanan superclass dari dalam penginisialisasi yang ditunjuk, penginisialisasi kenyamanan superclass tidak akan lagi diwariskan.
fluidsonic
@fluidsonic tetapi itu akan menjadi gila karena struktur dan metode kelas Anda akan berubah tergantung pada bagaimana mereka digunakan. Bayangkan kesenangan debugging!
kdazzle
2
@kdazzle Baik struktur kelas maupun metode kelasnya tidak akan berubah. Mengapa harus demikian? - Satu-satunya masalah yang dapat saya pikirkan adalah bahwa penginisialisasi praktis harus secara dinamis mendelegasikan ke penginisialisasi yang ditunjuk subkelas ketika diwariskan dan secara statis ke penginisialisasi yang ditunjuk kelasnya sendiri ketika tidak diwariskan tetapi didelegasikan ke dari subkelas.
fluidsonic
Saya pikir Anda juga bisa mendapatkan masalah rekursi serupa dengan metode normal. Itu tergantung pada keputusan Anda saat memanggil metode. Misalnya, akan konyol jika suatu bahasa tidak mengizinkan panggilan rekursif karena Anda dapat masuk ke loop tak terbatas. Pemrogram harus memahami apa yang mereka lakukan. :)
Ferran Maylinch
13

Itu karena Anda bisa berakhir dengan rekursi tak terbatas. Mempertimbangkan:

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

dan lihat:

let a = SubClass()

yang akan memanggil SubClass.init()yang akan memanggil SuperClass.init(value:)yang akan memanggil SubClass.init().

Aturan init yang ditunjuk / praktis dirancang agar inisialisasi kelas akan selalu benar.

Julien
sumber
1
Sementara subclass mungkin tidak secara eksplisit memanggil penginisialisasi kenyamanan dari superclassnya, ia mungkin mewarisi mereka, mengingat bahwa subclass memasok implementasi dari semua penginisialisasi yang ditunjuk oleh superclassnya (lihat contoh ini ). Karenanya contoh Anda di atas memang benar, tetapi merupakan kasus khusus yang tidak secara eksplisit terkait dengan penginisialisasi kenyamanan superclass, melainkan fakta bahwa penginisialisasi yang ditunjuk mungkin tidak memanggil penginisialisasi praktis , karena ini akan menghasilkan skenario rekursif seperti yang di atas. .
dfrib
1

Saya menemukan solusi untuk ini. Ini tidak super cantik, tetapi memecahkan masalah tidak mengetahui nilai superclass atau ingin menyetel nilai default.

Yang harus Anda lakukan adalah membuat instance dari superclass, menggunakan kemudahan init, tepat di initsubclass. Kemudian Anda memanggil yang ditunjuk initdari super menggunakan contoh yang baru saja Anda buat.

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) 
    }
}
bigelerow
sumber
1

Pertimbangkan untuk mengekstrak kode inisialisasi dari kemudahan Anda init()ke fungsi pembantu baru foo(), panggil foo(...)untuk melakukan inisialisasi di sub-kelas Anda.

evanchin.dll
sumber
Saran yang bagus, tetapi sebenarnya tidak menjawab pertanyaan itu.
SwiftsNamesake
0

Lihat video WWDC "403 intermediate Swift" di 18:30 untuk penjelasan mendalam tentang penginisialisasi dan warisannya. Seperti yang saya pahami, pertimbangkan hal berikut:

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

Tapi sekarang pertimbangkan subkelas Wyrm: Wyrm adalah Naga tanpa kaki dan sayap. Jadi Penginisialisasi untuk Wyvern (2 kaki, 2 sayap) salah untuk itu! Kesalahan itu dapat dihindari jika kenyamanan Wyvern-Initializer tidak dapat dipanggil tetapi hanya Penginisialisasi lengkap yang ditunjuk:

class Wyrm: Dragon {
    init() {
        super.init(legs: 0, isFlying: false)
    }
}
Ralf
sumber
12
Itu bukan alasan sebenarnya. Bagaimana jika saya membuat subclass ketika initWyvernmasuk akal untuk dipanggil?
Sulthan
4
Ya, saya tidak yakin. Tidak ada yang bisa menghentikan Wyrmmenimpa jumlah kaki setelah memanggil penginisialisasi kenyamanan.
Robert
Itu adalah alasan yang diberikan dalam video WWDC (hanya dengan mobil -> mobil balap dan properti hasTurbo boolean). Jika subclass mengimplementasikan semua penginisialisasi yang ditunjuk, ia juga mewarisi penginisialisasi praktis. Aku agak mengerti maksudnya, juga sejujurnya aku tidak punya banyak masalah dengan cara kerja Objective-C. Lihat juga konvensi baru untuk memanggil super.init yang terakhir di init, bukan yang pertama seperti di Objective-C.
Ralf
IMO, konvensi untuk memanggil super.init "terakhir" bukanlah secara konseptual baru, itu sama dengan yang ada di objektif-c, hanya saja, semuanya diberi nilai awal (nil, 0, apa pun) secara otomatis. Hanya saja di Swift, kita harus melakukan sendiri fase inisialisasi os ini. Manfaat dari pendekatan ini adalah kami juga memiliki opsi untuk menetapkan nilai awal yang berbeda.
Roshan
-1

Mengapa Anda tidak hanya memiliki dua penginisialisasi - satu dengan nilai default?

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
Jason
sumber