Kelas tidak menerapkan anggota superkelas yang disyaratkan

155

Jadi saya memperbarui Xcode 6 beta 5 hari ini dan memperhatikan bahwa saya menerima kesalahan di hampir semua subkelas saya di kelas Apple.

Status kesalahan:

Kelas 'x' tidak menerapkan anggota superclass yang diperlukan

Inilah salah satu contoh yang saya pilih karena kelas ini saat ini cukup ringan sehingga akan mudah diposkan.

class InfoBar: SKSpriteNode  { //Error message here

    let team: Team
    let healthBar: SKSpriteNode

    init(team: Team, size: CGSize) {
        self.team = team
        if self.team == Team.TeamGood {
            healthBar = SKSpriteNode(color: UIColor.greenColor(), size:size)
        }
        else {
            healthBar = SKSpriteNode(color: UIColor.redColor(), size:size)
        }
        super.init(texture:nil, color: UIColor.darkGrayColor(), size: size)

        self.addChild(healthBar)

    }

}

Jadi pertanyaan saya adalah, mengapa saya menerima kesalahan ini, dan bagaimana saya bisa memperbaikinya? Apa yang tidak saya laksanakan? Saya memanggil penginisialisasi yang ditunjuk.

Epic Byte
sumber

Jawaban:

127

Dari seorang karyawan Apple di Forum Pengembang:

"Cara untuk mendeklarasikan ke kompiler dan program yang dibangun yang Anda benar-benar tidak inginkan agar kompatibel dengan NSCoding adalah dengan melakukan sesuatu seperti ini:"

required init(coder: NSCoder) {
  fatalError("NSCoding not supported")
}

Jika Anda tahu Anda tidak ingin mematuhi NSCoding, ini merupakan opsi. Saya telah mengambil pendekatan ini dengan banyak kode SpriteKit saya, karena saya tahu saya tidak akan memuatnya dari storyboard.


Opsi lain yang dapat Anda ambil yang berfungsi dengan baik adalah menerapkan metode ini sebagai kenyamanan, seperti:

convenience required init(coder: NSCoder) {
    self.init(stringParam: "", intParam: 5)
}

Perhatikan panggilan ke penginisialisasi dalam self. Ini memungkinkan Anda hanya perlu menggunakan nilai dummy untuk parameter, yang bertentangan dengan semua properti non-opsional, sambil menghindari melempar kesalahan fatal.


Opsi ketiga tentu saja adalah menerapkan metode sambil memanggil super, dan menginisialisasi semua properti non-opsional Anda. Anda harus mengambil pendekatan ini jika objeknya adalah tampilan yang dimuat dari storyboard:

required init(coder aDecoder: NSCoder!) {
    foo = "some string"
    bar = 9001

    super.init(coder: aDecoder)
}
Ben Kane
sumber
3
Opsi kedua tidak berguna dalam kebanyakan kasus kehidupan nyata. Ambil, misalnya, inisialisasi yang saya butuhkan init(collection:MPMediaItemCollection). Anda harus menyediakan koleksi item media nyata; itulah poin dari kelas ini. Kelas ini tidak bisa dipakai tanpa satu. Ini akan menganalisis koleksi dan menginisialisasi selusin variabel instan. Itulah inti dari ini menjadi satu-satunya inisialisasi yang ditunjuk! Dengan demikian, init(coder:)MPMediaItemCollection untuk memasok di sini tidak memiliki makna (atau bahkan tidak berarti); hanya fatalErrorpendekatannya yang benar.
matt
@ Mat Benar, satu atau opsi lain akan bekerja lebih baik dalam situasi yang berbeda.
Ben Kane
Benar, dan saya memang menemukan dan mempertimbangkan pilihan kedua secara mandiri, dan kadang-kadang itu masuk akal. Misalnya saya bisa menyatakan di saya init(collection:MPMediaItemCollection!). Itu akan memungkinkan init(coder:)untuk melewati nol. Tapi kemudian saya menyadari: tidak, sekarang Anda hanya menipu kompiler. Melewati nol tidak dapat diterima, jadi lemparkan fatalErrordan lanjutkan. :)
matt
1
Saya tahu pertanyaan ini dan jawabannya sudah lama sekarang, tetapi saya telah memposting jawaban baru yang membahas beberapa poin yang saya pikir sangat penting untuk benar-benar memahami kesalahan ini yang tidak ditangani oleh jawaban yang ada.
nhgrif
Jawaban yang bagus. Saya setuju dengan Anda bahwa memahami bahwa Swift tidak selalu mewarisi inisialisasi super sangat penting untuk memahami pola ini.
Ben Kane
71

Ada dua bagian informasi Swift khusus yang sangat krusial yang hilang dari jawaban yang ada yang menurut saya membantu menjernihkan hal ini sepenuhnya.

  1. Jika protokol menentukan penginisialisasi sebagai metode yang diperlukan, penginisialisasi itu harus ditandai menggunakan requiredkata kunci Swift .
  2. Swift memiliki seperangkat aturan pewarisan khusus mengenai initmetode.

The tl; dr adalah ini:

Jika Anda menerapkan inisialisasi apa pun, Anda tidak lagi mewarisi inisialisasi yang ditunjuk oleh superclass.

Inisialisasi hanya, jika ada, yang akan Anda warisi, adalah inisialisasi kenyamanan kelas super yang menunjuk ke inisialisasi yang ditunjuk yang kebetulan Anda timpa.

Jadi ... siap untuk versi panjang?


Swift memiliki seperangkat aturan pewarisan khusus mengenai initmetode.

Saya tahu ini adalah yang kedua dari dua poin yang saya buat, tetapi kami tidak dapat memahami poin pertama, atau mengapa requiredkata kunci itu ada sampai kami memahami poin ini. Setelah kita memahami hal ini, yang lain menjadi sangat jelas.

Semua informasi yang saya bahas di bagian jawaban ini berasal dari dokumentasi Apple yang ditemukan di sini .

Dari dokumen Apple:

Tidak seperti subclass di Objective-C, Swift subclass tidak mewarisi inisialisasi kelas supernya. Pendekatan Swift mencegah situasi di mana penginisialisasi sederhana dari superclass diwarisi oleh subclass yang lebih khusus dan digunakan untuk membuat instance baru dari subclass yang tidak sepenuhnya atau benar diinisialisasi.

Tekankan milikku.

Jadi, langsung dari dokumen Apple di sana, kita melihat bahwa subclass Swift tidak akan selalu (dan biasanya tidak) mewarisi initmetode superclass mereka .

Jadi, kapan mereka mewarisi dari superclass mereka?

Ada dua aturan yang menentukan kapan subkelas mewarisi initmetode dari induknya. Dari dokumen Apple:

Aturan 1

Jika subkelas Anda tidak menentukan inisialisasi yang ditunjuk, subisialisasi Anda secara otomatis akan mewarisi semua inisialisasi yang ditentukan oleh superclass.

Aturan 2

Jika subclass Anda memberikan implementasi dari semua inisialisasi yang ditentukan oleh superclass-baik dengan mewarisi mereka sesuai aturan 1, atau dengan memberikan implementasi kustom sebagai bagian dari definisi-maka secara otomatis mewarisi semua inisialisasi kenyamanan superclass.

Aturan 2 tidak terlalu relevan dengan percakapan ini karena SKSpriteNodeitu init(coder: NSCoder)tidak mungkin menjadi metode kenyamanan.

Jadi, InfoBarkelas Anda mewarisi requiredinitializer hingga titik yang Anda tambahkan init(team: Team, size: CGSize).

Jika Anda adalah untuk tidak disediakan ini initmetode dan bukannya membuat Anda InfoBar'sifat s menambahkan opsional atau memberikan mereka dengan nilai-nilai default, maka Anda akan masih telah mewarisi SKSpriteNode' s init(coder: NSCoder). Namun, ketika kami menambahkan inisialisasi kustom kami sendiri, kami berhenti mewarisi inisialisasi yang ditunjuk superclass kami (dan inisialisasi kenyamanan yang tidak menunjuk ke inisialisasi yang kami terapkan).

Jadi, sebagai contoh sederhana, saya menyajikan ini:

class Foo {
    var foo: String
    init(foo: String) {
        self.foo = foo
    }
}

class Bar: Foo {
    var bar: String
    init(foo: String, bar: String) {
        self.bar = bar
        super.init(foo: foo)
    }
}


let x = Bar(foo: "Foo")

Yang menyajikan kesalahan berikut:

Argumen untuk 'bar' parameter tidak ada dalam panggilan.

masukkan deskripsi gambar di sini

Jika ini Objective-C, tidak akan ada masalah mewarisi. Jika kita menginisialisasi a Bardengan initWithFoo:di Objective-C, self.barproperti akan menjadi nil. Ini mungkin tidak besar, tetapi itu adalah sempurna berlaku keadaan untuk objek yang akan di. Ini bukan keadaan sempurna berlaku untuk objek Swift berada di. self.barBukan opsional dan tidak bisa nil.

Sekali lagi, satu-satunya cara kami mewarisi inisialisasi adalah dengan tidak menyediakan inisialisasi kami. Jadi jika kita mencoba untuk mewarisi dengan menghapus Bar's init(foo: String, bar: String), seperti:

class Bar: Foo {
    var bar: String
}

Sekarang kita kembali ke pewarisan (semacam), tetapi ini tidak dapat dikompilasi ... dan pesan kesalahan menjelaskan mengapa kita tidak mewarisi initmetode superclass :

Masalah: Kelas 'Bar' tidak memiliki inisialisasi

Fix-It: 'bar' properti tersimpan tanpa inisialisasi mencegah inisialisasi disintesis

Jika kami telah menambahkan properti tersimpan di subkelas kami, tidak ada cara Swift yang memungkinkan untuk membuat instance yang valid dari subkelas kami dengan inisialisasi superclass yang tidak mungkin tahu tentang properti tersimpan subkelas kami.


Oke, well, mengapa saya harus menerapkan init(coder: NSCoder)sama sekali? Kenapa begitu required?

initMetode Swift dapat dimainkan oleh seperangkat aturan pewarisan khusus, tetapi kesesuaian protokol masih diwariskan ke bawah rantai. Jika kelas induk sesuai dengan protokol, subkelasnya harus sesuai dengan protokol itu.

Biasanya, ini bukan masalah, karena sebagian besar protokol hanya memerlukan metode yang tidak dimainkan oleh aturan pewarisan khusus di Swift, jadi jika Anda mewarisi dari kelas yang sesuai dengan protokol, Anda juga mewarisi semua metode atau properti yang memungkinkan kelas untuk memenuhi kepatuhan protokol.

Namun, ingat, initmetode Swift bermain dengan seperangkat aturan khusus dan tidak selalu diwariskan. Karena itu, kelas yang sesuai dengan protokol yang membutuhkan initmetode khusus (seperti NSCoding) mengharuskan kelas menandai initmetode tersebut required.

Pertimbangkan contoh ini:

protocol InitProtocol {
    init(foo: Int)
}

class ConformingClass: InitProtocol {
    var foo: Int
    init(foo: Int) {
        self.foo = foo
    }
}

Ini tidak dikompilasi. Ini menghasilkan peringatan berikut:

Masalah: Persyaratan inisialisasi 'init (foo :)' hanya dapat dipenuhi oleh inisialisasi 'wajib' di kelas non-final 'ConformingClass'

Fix-It: Masukkan wajib

Ia ingin saya membuat init(foo: Int)inisialisasi diperlukan. Saya juga bisa membuatnya bahagia dengan membuat kelas final(artinya kelas tidak dapat diwarisi dari).

Jadi, apa yang terjadi jika saya subkelas? Dari titik ini, jika saya subkelas, saya baik-baik saja. Jika saya menambahkan inisialisasi, saya tiba-tiba tidak lagi mewarisi init(foo:). Ini bermasalah karena sekarang saya tidak lagi sesuai dengan InitProtocol. Saya tidak bisa subkelas dari kelas yang sesuai dengan protokol dan kemudian tiba-tiba memutuskan saya tidak ingin lagi sesuai dengan protokol itu. Saya mewarisi kesesuaian protokol, tetapi karena cara Swift bekerja dengan initpewarisan metode, saya tidak mewarisi bagian dari apa yang diperlukan untuk menyesuaikan dengan protokol itu dan saya harus mengimplementasikannya.


Oke, ini semua masuk akal. Tetapi mengapa saya tidak bisa mendapatkan pesan kesalahan yang lebih bermanfaat?

Dapat diperdebatkan, pesan kesalahan mungkin lebih jelas atau lebih baik jika ditentukan bahwa kelas Anda tidak lagi sesuai dengan NSCodingprotokol yang diwarisi dan bahwa untuk memperbaikinya Anda perlu menerapkan init(coder: NSCoder). Tentu.

Tapi Xcode tidak bisa menghasilkan pesan itu karena itu sebenarnya tidak akan selalu menjadi masalah aktual dengan tidak menerapkan atau mewarisi metode yang diperlukan. Setidaknya ada satu alasan lain untuk membuat initmetode requiredselain kepatuhan protokol, dan itulah metode pabrik.

Jika saya ingin menulis metode pabrik yang tepat, saya perlu menentukan tipe pengembalian menjadi Self(Swift's setara dengan Objective-C instanceType). Tetapi untuk melakukan ini, saya benar-benar perlu menggunakan requiredmetode penginisialisasi.

class Box {
    var size: CGSize
    init(size: CGSize) {
        self.size = size
    }

    class func factory() -> Self {
        return self.init(size: CGSizeZero)
    }
}

Ini menghasilkan kesalahan:

Membangun objek tipe kelas 'Mandiri' dengan nilai metatype harus menggunakan penginisialisasi 'wajib'

masukkan deskripsi gambar di sini

Ini pada dasarnya masalah yang sama. Jika kita subkelas Box, subkelas kita akan mewarisi metode kelas factory. Jadi kita bisa menelepon SubclassedBox.factory(). Namun, tanpa requiredkata kunci pada init(size:)metode, Box's subclass tidak dijamin untuk mewarisi self.init(size:)yang factorymemanggil.

Jadi kita harus membuat metode itu requiredjika kita ingin metode pabrik seperti ini, dan itu berarti jika kelas kita mengimplementasikan metode seperti ini, kita akan memiliki requiredmetode penginisialisasi dan kita akan mengalami masalah yang sama persis dengan yang Anda temui di sini dengan NSCodingprotokol.


Pada akhirnya, itu semua bermuara pada pemahaman dasar bahwa inisialisasi Swift bermain dengan seperangkat aturan pewarisan yang sedikit berbeda yang berarti Anda tidak dijamin mewarisi inisialisasi dari superclass Anda. Ini terjadi karena inisialisasi superclass tidak dapat mengetahui tentang properti tersimpan baru Anda dan mereka tidak dapat membuat objek Anda menjadi keadaan yang valid. Tetapi, karena berbagai alasan, sebuah superclass mungkin menandai inisialisasi sebagai required. Ketika hal itu terjadi, kita dapat menggunakan salah satu skenario yang sangat spesifik yang dengannya kita benar-benar mewarisi requiredmetode ini, atau kita harus mengimplementasikannya sendiri.

Poin utama di sini adalah bahwa jika kita mendapatkan kesalahan yang Anda lihat di sini, itu berarti bahwa kelas Anda sebenarnya tidak menerapkan metode sama sekali.

Sebagai mungkin satu contoh terakhir untuk menelusuri fakta bahwa subclass Swift tidak selalu mewarisi initmetode orang tua mereka (yang saya pikir sangat penting untuk memahami sepenuhnya masalah ini), pertimbangkan contoh ini:

class Foo {
    init(a: Int, b: Int, c: Int) {
        // do nothing
    }
}

class Bar: Foo {
    init(string: String) {
        super.init(a: 0, b: 1, c: 2)
        // do more nothing
    }
}

let f = Foo(a: 0, b: 1, c: 2)
let b = Bar(a: 0, b: 1, c: 2)

Ini gagal dikompilasi.

masukkan deskripsi gambar di sini

Pesan kesalahan yang diberikannya sedikit menyesatkan:

Argumen ekstra 'b' dalam panggilan

Tapi intinya adalah, Bartidak mewarisi apapun Foo's initmetode karena belum puas salah satu dari dua kasus khusus untuk mewarisi initmetode dari kelas induknya.

Jika ini Objective-C, kami akan mewarisinya inittanpa masalah, karena Objective-C sangat senang tidak menginisialisasi properti objek (meskipun sebagai pengembang, Anda seharusnya tidak senang dengan ini). Di Swift, ini tidak akan berhasil. Anda tidak dapat memiliki status yang tidak valid, dan mewarisi inisialisasi kelas super hanya dapat menyebabkan status objek yang tidak valid.

nhgrif
sumber
Bisakah Anda jelaskan apa arti kalimat ini atau memberi contoh? "(dan inisialisasi kenyamanan yang tidak menunjukkan inisialisasi yang kami laksanakan)"
Abbey Jackson
Jawaban yang brilian! Saya berharap lebih banyak posting SO tentang mengapa , seperti ini, bukan hanya bagaimana .
Alexander Vasenin
56

Mengapa masalah ini muncul? Yah, fakta yang jelas adalah bahwa itu selalu penting (yaitu di Objective-C, sejak hari saya mulai memprogram Cocoa kembali di Mac OS X 10.0) untuk berurusan dengan inisialisasi yang tidak siap ditangani oleh kelas Anda. Dokumen selalu cukup jelas tentang tanggung jawab Anda dalam hal ini. Tetapi berapa banyak dari kita yang bersusah payah untuk memenuhi mereka, sepenuhnya dan untuk surat itu? Mungkin tidak ada dari kita! Dan kompiler tidak menegakkan mereka; itu semua murni konvensional.

Misalnya, dalam subkelas pengontrol tampilan Objective-C saya dengan penginisialisasi yang ditunjuk ini:

- (instancetype) initWithCollection: (MPMediaItemCollection*) coll;

... sangat penting bagi kita untuk melewati koleksi item media yang sebenarnya: instance tidak dapat muncul tanpa satu. Tetapi saya tidak menulis "stopper" untuk mencegah seseorang menginisialisasi saya dengan tulang kosong init. Saya seharusnya menulis satu (sebenarnya, dengan benar, saya seharusnya menulis implementasi dari initWithNibName:bundle:, initializer yang diwarisi ditunjuk); tapi aku terlalu malas untuk repot, karena aku "tahu" aku tidak akan salah menginisialisasi kelasku dengan cara itu. Ini meninggalkan lubang menganga. Di Objective-C, seseorang dapat memanggil tulang-telanjang init, membiarkan ivar saya tidak diinisialisasi, dan kami sampai di sungai tanpa dayung.

Swift, luar biasa, menyelamatkan saya dari diri saya sendiri dalam banyak kasus. Segera setelah saya menerjemahkan aplikasi ini ke Swift, seluruh masalah hilang. Swift secara efektif menciptakan penghenti bagi saya! Jika init(collection:MPMediaItemCollection)satu-satunya inisialisasi yang dideklarasikan dinyatakan di kelas saya, saya tidak dapat diinisialisasi dengan memanggil bare-bone init(). Ini keajaiban!

Apa yang terjadi pada seed 5 hanyalah bahwa kompiler telah menyadari bahwa mukjizat tidak bekerja dalam kasus init(coder:), karena secara teori contoh kelas ini dapat berasal dari nib, dan kompiler tidak dapat mencegah itu - dan ketika nib memuat, init(coder:)akan dipanggil. Jadi kompiler membuat Anda menulis penghenti secara eksplisit. Dan benar juga.

matt
sumber
Terima kasih atas jawaban terinci seperti itu. Ini benar-benar membawa cahaya ke dalam masalah ini.
Julian Osorio
Suara positif untuk pasta12 karena memberi tahu saya bagaimana membuat kompiler tutup mulut, tetapi juga suara positif bagi Anda karena memberi saya petunjuk tentang apa yang dikeluhkannya.
Garrett Albright
2
Menganga lubang atau tidak, saya tidak akan pernah memanggil init ini, jadi itu benar-benar resmi untuk memaksa saya untuk memasukkannya. Kode yang membengkak adalah overhead yang tidak kita perlukan. Sekarang juga memaksa Anda untuk menginisialisasi properti Anda di kedua init juga. Tak berarti!
Dan Greenfield
5
@DanGreenfield Tidak, itu tidak memaksa Anda untuk menginisialisasi apa pun, karena jika Anda tidak akan pernah menyebutnya, Anda cukup memasukkan fatalErrorpenghenti yang dijelaskan dalam stackoverflow.com/a/25128815/341994 . Buat saja Cuplikan Kode pengguna dan mulai sekarang Anda bisa memasukkannya di tempat yang diperlukan. Butuh setengah detik.
matt
1
@ nhgrif Baiklah, agar adil, pertanyaannya tidak menanyakan cerita lengkapnya. Itu hanya tentang bagaimana keluar dari kemacetan ini dan melanjutkan. Kisah lengkapnya diberikan dalam buku saya: apeth.com/swiftBook/ch04.html#_class_initializers
matt
33

Menambahkan

required init(coder aDecoder: NSCoder!) {
  super.init(coder: aDecoder)
}
Gagan Singh
sumber
3
Ini berhasil, tapi saya rasa ini bukan bug. inisialisasi tidak diwariskan dengan cepat (saat inisialisasi Anda dideklarasikan) dan ini ditandai dengan kata kunci yang diperlukan. Satu-satunya masalah adalah bahwa sekarang saya perlu menginisialisasi SEMUA properti saya dalam metode ini untuk masing-masing kelas saya yang akan banyak kode terbuang karena saya tidak menggunakan ini sama sekali. Atau saya harus mendeklarasikan semua properti saya sebagai tipe opsional yang terbuka dan tidak terbuka untuk memotong inisialisasi yang juga tidak ingin saya lakukan.
Epic Byte
1
Ya! Saya menyadari segera setelah mengatakan itu mungkin bug, bahwa itu benar-benar masuk akal. Saya setuju itu akan banyak kode terbuang, karena seperti Anda, saya tidak akan pernah menggunakan metode init ini. Belum yakin tentang solusi elegan
Gagan Singh
2
Saya memiliki masalah yang sama. Masuk akal dengan "wajib init", tetapi cepat bukan bahasa "mudah" yang saya harapkan. Semua ini "opsional" membuat bahasa lebih kompleks daripada yang dibutuhkan. Dan tidak ada dukungan untuk DSL dan AOP. Saya semakin kecewa.
user810395
2
Ya saya sepenuhnya setuju. Banyak properti saya sekarang dinyatakan sebagai opsional karena saya terpaksa melakukannya, padahal sebenarnya tidak boleh nol. Beberapa opsional karena mereka secara sah harus opsional (artinya nil adalah nilai yang valid). Dan kemudian di kelas-kelas di mana saya tidak mensubklasifikasikan, saya tidak perlu menggunakan opsional, jadi semuanya menjadi sangat kompleks, dan saya tidak bisa menemukan gaya pengkodean yang tepat. Semoga Apple menemukan sesuatu.
Epic Byte
5
Saya pikir itu berarti bahwa Anda dapat memenuhi inisialisasi yang diperlukan dengan tidak mendeklarasikan inisialisasi Anda sendiri, yang akan mengakibatkan semua inisialisasi diinisiasi.
Epic Byte