Praktik terbaik untuk menerapkan penginisialisasi yang dapat gagal di Swift

100

Dengan kode berikut saya mencoba untuk mendefinisikan kelas model sederhana dan penginisialisasi gagal itu, yang mengambil kamus (json-) sebagai parameter. Penginisialisasi harus kembali niljika nama pengguna tidak ditentukan dalam json asli.

1. Mengapa kode tidak dapat dikompilasi? Pesan kesalahan mengatakan:

Semua properti yang disimpan dari instance kelas harus diinisialisasi sebelum mengembalikan nol dari penginisialisasi.

Itu tidak masuk akal. Mengapa saya harus menginisialisasi properti tersebut ketika saya berencana untuk mengembalikannya nil?

2. Apakah pendekatan saya benar atau adakah ide atau pola umum lain untuk mencapai tujuan saya?

class User: NSObject {

    let userName: String
    let isSuperUser: Bool = false
    let someDetails: [String]?

    init?(dictionary: NSDictionary) {
        if let value: String = dictionary["user_name"] as? String {
            userName = value
        }
        else {
           return nil
        }

        if let value: Bool = dictionary["super_user"] as? Bool {
            isSuperUser = value
        }

        someDetails = dictionary["some_details"] as? Array

        super.init()
    }
}
Kai Huppmann
sumber
Saya memiliki masalah serupa, dengan milik saya saya menyimpulkan bahwa setiap nilai kamus harus diharapkan dan jadi saya memaksa membuka nilai-nilai itu. Jika properti tidak ada di sana, saya akan dapat menangkap bugnya. Selain itu, saya menambahkan canSetCalculablePropertiesparameter boolean yang memungkinkan inisialisasi saya menghitung properti yang dapat atau tidak dapat dibuat dengan cepat. Misalnya, jika ada dateCreatedkunci yang hilang dan saya dapat menyetel properti dengan cepat karena canSetCalculablePropertiesparameternya benar, maka saya cukup menyetelnya ke tanggal saat ini.
Adam Carter

Jawaban:

71

Pembaruan: Dari Swift 2.2 Change Log (dirilis 21 Maret 2016):

Penginisialisasi kelas yang ditunjuk dideklarasikan sebagai gagal atau melempar sekarang dapat mengembalikan nihil atau memunculkan kesalahan, masing-masing, sebelum objek diinisialisasi sepenuhnya.


Untuk Swift 2.1 dan sebelumnya:

Menurut dokumentasi Apple (dan kesalahan kompiler Anda), kelas harus menginisialisasi semua properti yang disimpannya sebelum kembali nildari penginisialisasi yang gagal:

Untuk kelas, bagaimanapun, penginisialisasi yang gagal dapat memicu kegagalan inisialisasi hanya setelah semua properti tersimpan yang diperkenalkan oleh kelas tersebut telah disetel ke nilai awal dan setiap pendelegasian penginisialisasi telah dilakukan.

Catatan: Ini sebenarnya berfungsi dengan baik untuk struktur dan enumerasi, hanya saja tidak untuk kelas.

Cara yang disarankan untuk menangani properti tersimpan yang tidak dapat diinisialisasi sebelum penginisialisasi gagal adalah dengan mendeklarasikannya sebagai opsional yang tidak terbungkus secara implisit.

Contoh dari dokumen:

class Product {
    let name: String!
    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = name
    }
}

Dalam contoh di atas, properti name dari kelas Produk didefinisikan sebagai memiliki tipe string opsional yang tidak terbungkus secara implisit (String!). Karena ini adalah tipe opsional, ini berarti bahwa properti name memiliki nilai default nil sebelum diberi nilai tertentu selama inisialisasi. Nilai default nil ini pada gilirannya berarti bahwa semua properti yang diperkenalkan oleh kelas Produk memiliki nilai awal yang valid. Akibatnya, penginisialisasi yang dapat gagal untuk Produk dapat memicu kegagalan inisialisasi di awal penginisialisasi jika diberikan string kosong, sebelum menetapkan nilai tertentu ke properti nama dalam penginisialisasi.

Dalam kasus Anda, bagaimanapun, hanya dengan mendefinisikan userNamesebagai String!tidak memperbaiki kesalahan kompilasi karena Anda masih perlu khawatir tentang menginisialisasi properti pada kelas dasar Anda NSObject,. Untungnya, dengan userNamedidefinisikan sebagai String!, Anda sebenarnya dapat memanggil super.init()sebelum Anda return nilyang akan menginisialisasi NSObjectkelas dasar Anda dan memperbaiki kesalahan kompilasi.

class User: NSObject {

    let userName: String!
    let isSuperUser: Bool = false
    let someDetails: [String]?

    init?(dictionary: NSDictionary) {
        super.init()

        if let value = dictionary["user_name"] as? String {
            self.userName = value
        }
        else {
            return nil
        }

        if let value: Bool = dictionary["super_user"] as? Bool {
            self.isSuperUser = value
        }

        self.someDetails = dictionary["some_details"] as? Array
    }
}
Mike S.
sumber
1
Terima kasih banyak tidak hanya benar, tetapi juga dijelaskan dengan baik
Kai Huppmann
9
di swift1.2, Contoh dari dokumen membuat kesalahan "Semua properti yang disimpan dari instance kelas harus diinisialisasi sebelum mengembalikan nol dari penginisialisasi"
jeffrey
2
@jeffrey Benar, contoh dari dokumentasi ( Productkelas) tidak dapat memicu kegagalan inisialisasi sebelum menetapkan nilai tertentu, meskipun dokumen mengatakan dapat. Dokumen tidak sinkron dengan versi Swift terbaru. Disarankan untuk membuatnya varuntuk saat ini saja let. sumber: Chris Lattner .
Arjan
1
Dokumentasi memiliki potongan kode ini yang sedikit berbeda: Anda mengatur propertinya terlebih dahulu, lalu memeriksa apakah ada. Lihat "Penginisialisasi yang Gagal untuk Kelas", "Bahasa Pemrograman Swift." Produk kelas {biarkan nama: String! init? (name: String) {self.name = name if name.isEmpty {return nil}}} `` ``
Misha Karpenko
Saya membaca ini juga di dokumen Apple tetapi saya gagal untuk melihat mengapa ini diperlukan. Kegagalan akan berarti kembali nihil, lalu apa bedanya jika properti telah diinisialisasi?
Alper
132

Itu tidak masuk akal. Mengapa saya harus menginisialisasi properti tersebut ketika saya berencana mengembalikan nol?

Menurut Chris Lattner ini adalah bug. Inilah yang dia katakan:

Ini adalah batasan implementasi dalam kompilator swift 1.1, yang didokumentasikan dalam catatan rilis. Kompilator saat ini tidak dapat menghancurkan kelas yang diinisialisasi sebagian dalam semua kasus, jadi ia melarang pembentukan situasi yang harus dilakukan. Kami menganggap ini sebagai bug yang harus diperbaiki dalam rilis mendatang, bukan fitur.

Sumber

EDIT:

Jadi swift sekarang open source dan menurut changelog ini diperbaiki sekarang dalam snapshot swift 2.2

Penginisialisasi kelas yang ditunjuk dideklarasikan sebagai gagal atau melempar sekarang dapat mengembalikan nihil atau memunculkan kesalahan, masing-masing, sebelum objek diinisialisasi sepenuhnya.

mustafa
sumber
2
Terima kasih telah menyampaikan maksud saya bahwa gagasan untuk menginisialisasi properti yang tidak akan diperlukan lagi tampaknya tidak masuk akal. Dan +1 untuk berbagi sumber, yang membuktikan bahwa Chris Lattner merasa seperti saya;).
Kai Huppmann
22
FYI: "Memang. Ini masih sesuatu yang ingin kami tingkatkan, tetapi tidak berhasil untuk Swift 1.2". - Chris Lattner 10. Feb 2015
dreamlab
14
FYI: Di Swift 2.0 beta 2 ini masih menjadi masalah, dan juga masalah dengan penginisialisasi yang muncul.
aranasaurus
7

Saya menerima bahwa jawaban Mike S adalah rekomendasi Apple, tetapi menurut saya itu bukan praktik terbaik. Inti dari sistem tipe yang kuat adalah memindahkan kesalahan runtime ke waktu kompilasi. "Solusi" ini mengalahkan tujuan itu. IMHO, lebih baik melanjutkan dan menginisialisasi nama pengguna ""dan kemudian memeriksanya setelah super.init (). Jika userNames kosong diperbolehkan, maka setel bendera.

class User: NSObject {
    let userName: String = ""
    let isSuperUser: Bool = false
    let someDetails: [String]?

    init?(dictionary: [String: AnyObject]) {
        if let user_name = dictionary["user_name"] as? String {
            userName = user_name
        }

        if let value: Bool = dictionary["super_user"] as? Bool {
            isSuperUser = value
        }

        someDetails = dictionary["some_details"] as? Array

        super.init()

        if userName.isEmpty {
            return nil
        }
    }
}
Daniel T.
sumber
Terima kasih, tetapi saya tidak melihat bagaimana ide-ide sistem tipe kuat dirusak oleh jawaban Mike. Semua dalam semua Anda menyajikan solusi yang sama dengan perbedaan bahwa nilai awal disetel ke "", bukan nihil. Selain itu, kode Anda menghilangkan penggunaan "" sebagai nama pengguna (yang mungkin tampak cukup akademis, tetapi setidaknya berbeda dari yang tidak disetel dalam json / kamus)
Kai Huppmann
2
Setelah ditinjau, saya melihat bahwa Anda benar, tetapi hanya karena userName adalah sebuah konstanta. Jika itu adalah variabel, maka jawaban yang diterima akan lebih buruk daripada milik saya karena userName nanti bisa disetel ke nihil.
Daniel T.
Saya suka jawaban ini. @KaiHuppmann, jika Anda ingin mengizinkan nama pengguna kosong, Anda juga dapat memiliki Bool needsRetNil sederhana. Jika nilainya tidak ada dalam kamus, setel needsReturnNil ke true dan setel userName ke apapun. Setelah super.init (), periksa needsReturnNil dan kembalikan nil jika perlu.
Richard Venable
6

Cara lain untuk menghindari batasan ini adalah bekerja dengan fungsi-kelas untuk melakukan inisialisasi. Anda bahkan mungkin ingin memindahkan fungsi itu ke ekstensi:

class User: NSObject {

    let username: String
    let isSuperUser: Bool
    let someDetails: [String]?

    init(userName: String, isSuperUser: Bool, someDetails: [String]?) {

         self.userName = userName
         self.isSuperUser = isSuperUser
         self.someDetails = someDetails

         super.init()
    }
}

extension User {

    class func fromDictionary(dictionary: NSDictionary) -> User? {

        if let username: String = dictionary["user_name"] as? String {

            let isSuperUser = (dictionary["super_user"] as? Bool) ?? false
            let someDetails = dictionary["some_details"] as? [String]

            return User(username: username, isSuperUser: isSuperUser, someDetails: someDetails)
        }

        return nil
    }
}

Menggunakannya akan menjadi:

if let user = User.fromDictionary(someDict) {

     // Party hard
}
Kevin R
sumber
1
Saya suka ini; Saya lebih suka konstruktor transparan tentang apa yang mereka inginkan, dan menyampaikan kamus sangat tidak jelas.
Ben Leggiero
3

Meskipun Swift 2.2 telah dirilis dan Anda tidak lagi harus menginisialisasi objek sepenuhnya sebelum gagal dalam penginisialisasi, Anda perlu menahan kudanya hingga https://bugs.swift.org/browse/SR-704 diperbaiki.

sssilver.dll
sumber
1

Saya menemukan kaleng ini dilakukan di Swift 1.2

Ada beberapa syarat:

  • Properti yang diperlukan harus dideklarasikan sebagai opsional yang tidak terbungkus secara implisit
  • Tetapkan nilai ke properti wajib Anda tepat satu kali. Nilai ini mungkin nihil.
  • Kemudian panggil super.init () jika kelas Anda mewarisi dari kelas lain.
  • Setelah semua properti wajib Anda diberi nilai, periksa apakah nilainya sesuai harapan. Jika tidak, kembalikan nihil.

Contoh:

class ClassName: NSObject {

    let property: String!

    init?(propertyValue: String?) {

        self.property = propertyValue

        super.init()

        if self.property == nil {
            return nil
        }
    }
}
Pim
sumber
0

Penginisialisasi yang dapat gagal untuk jenis nilai (yaitu, struktur atau enumerasi) dapat memicu kegagalan inisialisasi pada titik mana pun dalam penerapan penginisialisasi

Untuk kelas, bagaimanapun, penginisialisasi yang gagal dapat memicu kegagalan inisialisasi hanya setelah semua properti tersimpan yang diperkenalkan oleh kelas tersebut telah disetel ke nilai awal dan setiap pendelegasian penginisialisasi telah dilakukan.

Kutipan Dari: Apple Inc. “ Bahasa Pemrograman Cepat. IBooks. https://itun.es/sg/jEUH0.l

pengguna1046037
sumber
0

Anda dapat menggunakan kemudahan init :

class User: NSObject {
    let userName: String
    let isSuperUser: Bool = false
    let someDetails: [String]?

    init(userName: String, isSuperUser: Bool, someDetails: [String]?) {
        self.userName = userName
        self.isSuperUser = isSuperUser
        self.someDetails = someDetails
    }     

    convenience init? (dict: NSDictionary) {            
       guard let userName = dictionary["user_name"] as? String else { return nil }
       guard let isSuperUser = dictionary["super_user"] as? Bool else { return nil }
       guard let someDetails = dictionary["some_details"] as? [String] else { return nil }

       self.init(userName: userName, isSuperUser: isSuperUser, someDetails: someDetails)
    } 
}
Максим Петров
sumber