Apakah mungkin untuk mengizinkan didSet dipanggil selama inisialisasi di Swift?

217

Pertanyaan

Dokumen Apple menetapkan bahwa:

pengamat willSet dan didSet tidak dipanggil saat properti pertama kali diinisialisasi. Mereka hanya dipanggil ketika nilai properti diatur di luar konteks inisialisasi.

Apakah mungkin untuk memaksa ini dipanggil saat inisialisasi?

Mengapa?

Katakanlah saya memiliki kelas ini

class SomeClass {
    var someProperty: AnyObject {
        didSet {
            doStuff()
        }
    }

    init(someProperty: AnyObject) {
        self.someProperty = someProperty
        doStuff()
    }

    func doStuff() {
        // do stuff now that someProperty is set
    }
}

Saya menciptakan metode doStuff, untuk membuat panggilan pemrosesan lebih ringkas, tetapi saya lebih suka hanya memproses properti dalam didSetfungsi. Apakah ada cara untuk memaksa panggilan ini saat inisialisasi?

Memperbarui

Saya memutuskan untuk hanya menghapus intializer kenyamanan untuk kelas saya dan memaksa Anda untuk mengatur properti setelah inisialisasi. Ini memungkinkan saya untuk tahu didSetakan selalu dipanggil. Saya belum memutuskan apakah ini secara keseluruhan lebih baik, tapi itu cocok dengan situasi saya dengan baik.

Logan
sumber
itu jujur ​​terbaik hanya untuk "menerima ini cara swift bekerja". jika Anda memasukkan nilai inisialisasi inline pada pernyataan var, tentu saja itu tidak memanggil "didSet". itu sebabnya situasi "init ()" juga tidak boleh memanggil "didSet". itu semua masuk akal.
Fattie
@Logan Pertanyaan itu sendiri menjawab pertanyaan saya;) terima kasih!
AamirR
2
Jika Anda ingin menggunakan initializer kenyamanan, Anda dapat menggabungkannya dengan defer:convenience init(someProperty: AnyObject) { self.init() defer { self.someProperty = someProperty }
Darek Cieśla

Jawaban:

100

Buat set-Method sendiri dan gunakan dalam init-Method Anda:

class SomeClass {
    var someProperty: AnyObject! {
        didSet {
            //do some Stuff
        }
    }

    init(someProperty: AnyObject) {
        setSomeProperty(someProperty)
    }

    func setSomeProperty(newValue:AnyObject) {
        self.someProperty = newValue
    }
}

Dengan mendeklarasikan somePropertysebagai tipe: AnyObject!(sebuah opsi yang secara implisit tidak terbuka), Anda mengizinkan diri untuk menginisialisasi sepenuhnya tanpa somePropertydisetel. Saat Anda menelepon, setSomeProperty(someProperty)Anda menelepon yang setara dengan self.setSomeProperty(someProperty). Biasanya Anda tidak akan dapat melakukan ini karena diri belum sepenuhnya diinisialisasi. Karena somePropertytidak memerlukan inisialisasi dan Anda memanggil metode yang bergantung pada diri sendiri, Swift meninggalkan konteks inisialisasi dan didSet akan berjalan.

Oliver
sumber
3
Mengapa ini berbeda dari self.someProperty = newValue di init? Apakah Anda punya test case yang berfungsi?
mmmmmm
2
Tidak tahu mengapa itu berhasil - tetapi ternyata berhasil. Pengaturan langsung nilai baru di dalam init () - Metode tidak memanggil didSet () - tetapi menggunakan Metode yang diberikan setSomeProperty () tidak.
Oliver
50
Saya rasa saya tahu apa yang terjadi di sini. Dengan mendeklarasikan somePropertysebagai tipe: AnyObject!(suatu opsi yang secara implisit tidak terbuka), Anda mengizinkan selfuntuk menginisialisasi sepenuhnya tanpa somePropertydisetel. Saat Anda menelepon, setSomeProperty(someProperty)Anda menelepon yang setara dengan self.setSomeProperty(someProperty). Biasanya Anda tidak dapat melakukan ini karena selfbelum sepenuhnya diinisialisasi. Karena somePropertytidak memerlukan inisialisasi dan Anda memanggil metode yang bergantung self, Swift meninggalkan konteks inisialisasi dan didSetakan berjalan.
Logan
3
Sepertinya semua yang diatur di luar init aktual () TIDAK didetet dipanggil. Secara harfiah apa pun yang tidak diset init() { *HERE* }akan didetet dipanggil. Bagus untuk pertanyaan ini tetapi menggunakan fungsi sekunder untuk menetapkan beberapa nilai mengarah ke didSet dipanggil. Tidak bagus jika Anda ingin menggunakan kembali fungsi setter itu.
WCByrne
4
@Ark serius, mencoba menerapkan akal sehat atau logika untuk Swift tidak masuk akal. Tetapi Anda dapat mempercayai upvotes atau mencobanya sendiri. Saya baru saja melakukannya dan berhasil. Dan ya, sama sekali tidak masuk akal.
Dan Rosenstark
305

Jika Anda menggunakan deferdalam sebuah initializer , untuk memperbarui setiap sifat opsional atau memperbarui lanjut sifat non-opsional yang Anda sudah diinisialisasi dan setelah Anda disebut setiap super.init()metode, maka Anda willSet, didSet, dll akan dipanggil. Saya menemukan ini lebih nyaman daripada menerapkan metode terpisah yang Anda harus melacak panggilan di tempat yang tepat.

Sebagai contoh:

public class MyNewType: NSObject {

    public var myRequiredField:Int

    public var myOptionalField:Float? {
        willSet {
            if let newValue = newValue {
                print("I'm going to change to \(newValue)")
            }
        }
        didSet {
            if let myOptionalField = self.myOptionalField {
                print("Now I'm \(myOptionalField)")
            }
        }
    }

    override public init() {
        self.myRequiredField = 1

        super.init()

        // Non-defered
        self.myOptionalField = 6.28

        // Defered
        defer {
            self.myOptionalField = 3.14
        }
    }
}

Akan menghasilkan:

I'm going to change to 3.14
Now I'm 3.14
Brian Westphal
sumber
6
Trik kecil kurang ajar, aku suka ... keanehan aneh Swift.
Chris Hatton
4
Bagus. Anda menemukan kasus penggunaan lain yang bermanfaat defer. Terima kasih.
Ryan
1
Penggunaan defer! Seandainya aku bisa memberimu lebih dari satu upvote.
Christian Schnorr
3
sangat menarik .. saya belum pernah melihat defer digunakan seperti ini sebelumnya. Ini fasih dan bekerja.
aBikis
Tapi Anda menetapkan myOptionalField dua kali yang tidak bagus dari perspektif kinerja. Bagaimana jika perhitungan yang berat terjadi?
Yakiv Kovalskyi
77

Sebagai variasi jawaban Oliver, Anda bisa membungkus garis dengan penutupan. Misalnya:

class Classy {

    var foo: Int! { didSet { doStuff() } }

    init( foo: Int ) {
        // closure invokes didSet
        ({ self.foo = foo })()
    }

}

Sunting: Jawaban Brian Westphal lebih baik imho. Hal yang baik tentang miliknya adalah bahwa itu mengisyaratkan maksudnya.

original_username
sumber
2
Itu pintar dan tidak mencemari kelas dengan metode yang berlebihan.
pointum
Jawaban yang bagus, terima kasih! Perlu dicatat bahwa dalam Swift 2.2 Anda harus selalu membungkus tanda kurung.
Max
@ Max Cheers, contohnya termasuk parens sekarang
original_username
2
Jawaban cerdas! Satu catatan: jika kelas Anda adalah subclass dari sesuatu yang lain, Anda harus memanggil super.init () sebelum ({self.foo = foo}) ()
kcstricks
1
@ Hang, saya tidak merasa lebih mungkin untuk istirahat di masa depan daripada yang lain, tapi masih ada masalah bahwa tanpa mengomentari kode itu tidak begitu jelas apa fungsinya. Setidaknya jawaban Brian "menunda" memberi pembaca petunjuk bahwa kami ingin melakukan kode setelah inisialisasi.
original_username
10

Saya memiliki masalah yang sama dan ini bekerja untuk saya

class SomeClass {
    var someProperty: AnyObject {
        didSet {
            doStuff()
        }
    }

    init(someProperty: AnyObject) {
        defer { self.someProperty = someProperty }
    }

    func doStuff() {
        // do stuff now that someProperty is set
    }
}
Carmine Cuofano
sumber
dimana kamu mendapatkannya?
Vanya
3
Pendekatan ini tidak lagi berfungsi. Kompilator melempar dua kesalahan: - 'diri' yang digunakan dalam pemanggilan metode '$ defer' sebelum semua properti tersimpan diinisialisasi - Kembali dari inisialisasi tanpa menginisialisasi semua properti yang disimpan
Edward B
Anda benar tetapi ada solusinya. Anda hanya perlu mendeklarasikan someProperty sebagai tersirat atau opsional dan menyediakannya dengan nilai default dengan nil penggabungan untuk menghindari kegagalan. tldr; someProperty harus merupakan tipe opsional
Tandai
2

Ini berfungsi jika Anda melakukan ini dalam subkelas

class Base {

  var someProperty: AnyObject {
    didSet {
      doStuff()
    }
  }

  required init() {
    someProperty = "hello"
  }

  func doStuff() {
    print(someProperty)
  }
}

class SomeClass: Base {

  required init() {
    super.init()

    someProperty = "hello"
  }
}

let a = Base()
let b = SomeClass()

Dalam acontoh, didSettidak dipicu. Tetapi dalam bcontoh, didSetdipicu, karena itu di dalam subkelas. Itu harus melakukan sesuatu dengan apa yang initialization contextsebenarnya berarti, dalam hal ini superclassmemang peduli tentang itu

onmyway133
sumber
1

Meskipun ini bukan solusi, cara alternatif untuk melakukannya adalah menggunakan konstruktor kelas:

class SomeClass {
    var someProperty: AnyObject {
        didSet {
            // do stuff
        }
    }

    class func createInstance(someProperty: AnyObject) -> SomeClass {
        let instance = SomeClass() 
        instance.someProperty = someProperty
        return instance
    }  
}
Manusia salju
sumber
1
Solusi ini akan mengharuskan Anda untuk mengubah opsionalitas somePropertykarena tidak akan memberinya nilai selama inisialisasi.
Mick MacCallum
1

Dalam kasus tertentu di mana Anda ingin meminta willSetatau didSetdi dalam inituntuk properti yang tersedia di superclass Anda, Anda dapat langsung menetapkan properti super Anda secara langsung:

override init(frame: CGRect) {
    super.init(frame: frame)
    // this will call `willSet` and `didSet`
    someProperty = super.someProperty
}

Perhatikan bahwa solusi Charlesism dengan penutupan akan selalu berhasil juga dalam kasus itu. Jadi solusi saya hanyalah sebuah alternatif.

Cur
sumber
-1

Anda dapat menyelesaikannya dengan cara obj-с:

class SomeClass {
    private var _someProperty: AnyObject!
    var someProperty: AnyObject{
        get{
            return _someProperty
        }
        set{
            _someProperty = newValue
            doStuff()
        }
    }
    init(someProperty: AnyObject) {
        self.someProperty = someProperty
        doStuff()
    }

    func doStuff() {
        // do stuff now that someProperty is set
    }
}
yshilov
sumber
1
Halo @yshilov, saya tidak berpikir ini benar-benar memecahkan masalah yang saya harapkan sejak secara teknis, ketika Anda menelepon self.somePropertyAnda meninggalkan ruang inisialisasi. Saya percaya hal yang sama dapat dicapai dengan melakukan var someProperty: AnyObject! = nil { didSet { ... } }. Namun, beberapa orang mungkin menghargainya sebagai kerja keras, terima kasih untuk penambahannya
Logan
@Logan tidak, itu tidak akan berhasil. didSet akan sepenuhnya diabaikan di init ().
yshilov