Apa tujuan willSet dan didSet di Swift?

265

Swift memiliki sintaks deklarasi properti yang sangat mirip dengan C #:

var foo: Int {
    get { return getFoo() }
    set { setFoo(newValue) }
}

Namun, ia juga memiliki willSetdan didSetbertindak. Ini disebut masing-masing sebelum dan sesudah penyetel dipanggil. Apa tujuan mereka, mengingat Anda hanya bisa memiliki kode yang sama di dalam setter?

zneak
sumber
11
Saya pribadi tidak suka banyak jawaban di sini. Mereka terlalu banyak turun ke dalam sintaksis. Perbedaannya lebih lanjut tentang semantik dan kode readiblity. Properti Terkomputasi ( get& set) pada dasarnya memiliki properti yang dihitung berdasarkan properti lain, misalnya mengubah label textmenjadi setahun Int. didSet& willSetAdakah untuk mengatakan ... hei nilai ini telah ditetapkan, sekarang mari kita lakukan ini mis. Sumber data kami telah diperbarui ... jadi mari kita memuat kembali tableView sehingga akan termasuk baris baru. Sebagai contoh lain, lihat jawaban dfri tentang cara memanggil delegasi dididSet
Honey

Jawaban:

324

Intinya tampaknya bahwa kadang-kadang, Anda memerlukan properti yang memiliki penyimpanan otomatis dan beberapa perilaku, misalnya untuk memberi tahu objek lain bahwa properti baru saja diubah. Ketika semua yang Anda miliki adalah get/ set, Anda perlu bidang lain untuk menyimpan nilainya. Dengan willSetdan didSet, Anda dapat mengambil tindakan saat nilainya diubah tanpa perlu bidang lain. Misalnya, dalam contoh itu:

class Foo {
    var myProperty: Int = 0 {
        didSet {
            print("The value of myProperty changed from \(oldValue) to \(myProperty)")
        }
    }
}

myPropertymencetak nilai lama dan baru setiap kali dimodifikasi. Dengan hanya getter dan setter, saya akan membutuhkan ini sebagai gantinya:

class Foo {
    var myPropertyValue: Int = 0
    var myProperty: Int {
        get { return myPropertyValue }
        set {
            print("The value of myProperty changed from \(myPropertyValue) to \(newValue)")
            myPropertyValue = newValue
        }
    }
}

Jadi willSetdan didSetmewakili ekonomi beberapa baris, dan lebih sedikit noise dalam daftar bidang.

zneak
sumber
248
Perhatian: willSetdan didSettidak dipanggil saat Anda menyetel properti dari dalam metode init seperti yang dicatat Apple:willSet and didSet observers are not called when a property is first initialized. They are only called when the property’s value is set outside of an initialization context.
Klaas
4
Tetapi mereka tampaknya dipanggil pada properti array ketika melakukan ini: myArrayProperty.removeAtIndex(myIndex)... Tidak diharapkan.
Andreas
4
Anda bisa membungkus penugasan dalam pernyataan penangguhan {} di dalam initialiser yang menyebabkan metode willSet dan didSet dipanggil ketika ruang lingkup initialiser keluar. Saya tidak perlu merekomendasikannya, hanya mengatakan bahwa itu mungkin. Salah satu konsekuensinya adalah hanya berfungsi jika Anda mendeklarasikan properti opsional, karena tidak sepenuhnya diinisialisasi dari penginisialisasi.
Marmoy
Tolong jelaskan di bawah garis. Saya tidak mengerti, apakah metode ini atau variabel var propertyChangedListener: (Int, Int) -> Void = {println ("Nilai myProperty telah berubah dari ($ 0) menjadi ($ 1)")}
Vikash Rajput
Menginisialisasi properti di baris yang sama TIDAK didukung di Swift 3. Anda harus mengubah jawaban agar sesuai dengan swift 3.
Ramazan Polat
149

Pemahaman saya adalah bahwa set dan dapatkan adalah untuk properti yang dihitung (tidak ada dukungan dari properti yang disimpan )

jika Anda berasal dari Objective-C, ingatlah bahwa konvensi penamaan telah berubah. Dalam Swift, variabel iVar atau instance dinamai properti tersimpan

Contoh 1 (properti hanya baca) - dengan peringatan:

var test : Int {
    get {
        return test
    }
}

Ini akan menghasilkan peringatan karena ini menghasilkan panggilan fungsi rekursif (pengambil panggilan itu sendiri). Peringatan dalam kasus ini adalah "Mencoba untuk memodifikasi 'tes' di dalam pengambil sendiri".

Contoh 2. Bersyarat baca / tulis - dengan peringatan

var test : Int {
    get {
        return test
    }
    set (aNewValue) {
        //I've contrived some condition on which this property can be set
        //(prevents same value being set)
        if (aNewValue != test) {
            test = aNewValue
        }
    }
}

Masalah serupa - Anda tidak dapat melakukan ini karena memanggil penyetel secara rekursif. Juga, perhatikan kode ini tidak akan mengeluh tentang tidak ada inisialisasi karena tidak ada properti yang disimpan untuk diinisialisasi .

Contoh 3. baca / tulis properti yang dihitung - dengan backing store

Berikut adalah pola yang memungkinkan pengaturan bersyarat dari properti tersimpan aktual

//True model data
var _test : Int = 0

var test : Int {
    get {
        return _test
    }
    set (aNewValue) {
        //I've contrived some condition on which this property can be set
        if (aNewValue != test) {
            _test = aNewValue
        }
    }
}

Catatan Data aktual disebut _test (walaupun bisa berupa data atau kombinasi data)

Contoh 4. Menggunakan kemauan dan memang diatur

//True model data
var _test : Int = 0 {

    //First this
    willSet {
        println("Old value is \(_test), new value is \(newValue)")
    }

    //value is set

    //Finaly this
    didSet {
        println("Old value is \(oldValue), new value is \(_test)")
    }
}

var test : Int {
    get {
        return _test
    }
    set (aNewValue) {
        //I've contrived some condition on which this property can be set
        if (aNewValue != test) {
            _test = aNewValue
        }
    }
}

Di sini kita melihat willSet dan didSet mencegat perubahan pada properti tersimpan aktual. Ini berguna untuk mengirim pemberitahuan, sinkronisasi dll ... (lihat contoh di bawah)

Contoh 5. Contoh Beton - Kontainer ViewController

//Underlying instance variable (would ideally be private)
var _childVC : UIViewController? {
    willSet {
        //REMOVE OLD VC
        println("Property will set")
        if (_childVC != nil) {
            _childVC!.willMoveToParentViewController(nil)
            self.setOverrideTraitCollection(nil, forChildViewController: _childVC)
            _childVC!.view.removeFromSuperview()
            _childVC!.removeFromParentViewController()
        }
        if (newValue) {
            self.addChildViewController(newValue)
        }

    }

    //I can't see a way to 'stop' the value being set to the same controller - hence the computed property

    didSet {
        //ADD NEW VC
        println("Property did set")
        if (_childVC) {
//                var views  = NSDictionaryOfVariableBindings(self.view)    .. NOT YET SUPPORTED (NSDictionary bridging not yet available)

            //Add subviews + constraints
            _childVC!.view.setTranslatesAutoresizingMaskIntoConstraints(false)       //For now - until I add my own constraints
            self.view.addSubview(_childVC!.view)
            let views = ["view" : _childVC!.view] as NSMutableDictionary
            let layoutOpts = NSLayoutFormatOptions(0)
            let lc1 : AnyObject[] = NSLayoutConstraint.constraintsWithVisualFormat("|[view]|",  options: layoutOpts, metrics: NSDictionary(), views: views)
            let lc2 : AnyObject[] = NSLayoutConstraint.constraintsWithVisualFormat("V:|[view]|", options: layoutOpts, metrics: NSDictionary(), views: views)
            self.view.addConstraints(lc1)
            self.view.addConstraints(lc2)

            //Forward messages to child
            _childVC!.didMoveToParentViewController(self)
        }
    }
}


//Computed property - this is the property that must be used to prevent setting the same value twice
//unless there is another way of doing this?
var childVC : UIViewController? {
    get {
        return _childVC
    }
    set(suggestedVC) {
        if (suggestedVC != _childVC) {
            _childVC = suggestedVC
        }
    }
}

Perhatikan penggunaan KEDUA properti yang dihitung dan disimpan. Saya telah menggunakan properti yang dihitung untuk mencegah pengaturan nilai yang sama dua kali (untuk menghindari hal-hal buruk terjadi!); Saya telah menggunakan willSet dan didSet untuk meneruskan pemberitahuan ke viewControllers (lihat dokumentasi UIViewController dan info tentang wadah viewController)

Saya harap ini membantu, dan tolong seseorang berteriak jika saya membuat kesalahan di mana saja di sini!

pengguna3675131
sumber
3
Mengapa saya tidak bisa menggunakan saya menggunakan didSet bersama dengan get and set ..?
Ben Sinclair
//I can't see a way to 'stop' the value being set to the same controller - hence the computed property peringatan menghilang setelah saya menggunakan if let newViewController = _childVC { bukan if (_childVC) {
evfemist
5
dapatkan dan atur digunakan untuk membuat properti yang dihitung. Ini adalah metode murni, dan tidak ada penyimpanan dukungan (variabel contoh). willSet dan didSet adalah untuk mengamati perubahan pada properti variabel yang disimpan. Di bawah tenda, ini didukung oleh penyimpanan, tetapi di Swift semuanya menyatu menjadi satu.
user3675131
Dalam contoh Anda 5, di get, saya pikir Anda perlu menambahkan if _childVC == nil { _childVC = something }lalu return _childVC.
JW.ZG
18

Ini disebut Pengamat Properti :

Pengamat properti mengamati dan merespons perubahan nilai properti. Pengamat properti dipanggil setiap kali nilai properti ditetapkan, bahkan jika nilai baru sama dengan nilai properti saat ini.

Kutipan dari: Apple Inc. “Bahasa Pemrograman Swift.” iBooks. https://itun.es/ca/jEUH0.l

Saya menduga itu untuk memungkinkan hal-hal yang secara tradisional akan kita lakukan dengan KVO seperti pengikatan data dengan elemen UI, atau memicu efek samping dari mengubah properti, memicu proses sinkronisasi, pemrosesan latar belakang, dll, dll.

Sebastien Martin
sumber
16

CATATAN

willSetdan didSetpengamat tidak dipanggil saat properti diatur dalam inisialisasi sebelum delegasi berlangsung

Bartłomiej Semańczyk
sumber
16

Anda juga dapat menggunakan didSetuntuk mengatur variabel ke nilai yang berbeda. Ini tidak menyebabkan pengamat dipanggil lagi seperti yang dinyatakan dalam panduan Properti . Misalnya, berguna ketika Anda ingin membatasi nilai seperti di bawah ini:

let minValue = 1

var value = 1 {
    didSet {
        if value < minValue {
            value = minValue
        }
    }
}

value = -10 // value is minValue now.
tahu
sumber
10

Banyak jawaban yang sudah ditulis dengan baik mencakup pertanyaan dengan baik, tetapi saya akan menyebutkan, secara rinci, tambahan yang saya yakin layak untuk dibahas.


The willSetdan didSetproperti pengamat dapat digunakan untuk menelepon delegasi, misalnya, untuk properti kelas yang hanya pernah diperbarui oleh interaksi pengguna, tetapi di mana Anda ingin menghindari memanggil delegasi di objek inisialisasi.

Saya akan mengutip komentar terpilih Klaas untuk jawaban yang diterima:

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

Ini cukup rapi karena artinya, misalnya, didSetproperti merupakan pilihan awal yang baik untuk callback & fungsi delegasi, untuk kelas khusus Anda sendiri.

Sebagai contoh, pertimbangkan beberapa objek kontrol pengguna kustom, dengan beberapa properti utama value(misalnya posisi dalam kontrol peringkat), diimplementasikan sebagai subkelas dari UIView:

// CustomUserControl.swift
protocol CustomUserControlDelegate {
    func didChangeValue(value: Int)
    // func didChangeValue(newValue: Int, oldValue: Int)
    // func didChangeValue(customUserControl: CustomUserControl)
    // ... other more sophisticated delegate functions
}

class CustomUserControl: UIView {

    // Properties
    // ...
    private var value = 0 {
        didSet {
            // Possibly do something ...

            // Call delegate.
            delegate?.didChangeValue(value)
            // delegate?.didChangeValue(value, oldValue: oldValue)
            // delegate?.didChangeValue(self)
        }
    }

    var delegate: CustomUserControlDelegate?

    // Initialization
    required init?(...) { 
        // Initialise something ...

        // E.g. 'value = 1' would not call didSet at this point
    }

    // ... some methods/actions associated with your user control.
}

Setelah fungsi delegasi Anda dapat digunakan di, katakanlah, beberapa view controller untuk mengamati perubahan kunci dalam model untuk CustomViewController, seperti halnya Anda akan menggunakan fungsi delegasi inheren objek UITextFieldDelegatefor UITextField(misalnya textFieldDidEndEditing(...)).

Untuk contoh sederhana ini, gunakan panggilan balik delegasi dari didSetproperti kelas valueuntuk memberi tahu pengontrol tampilan bahwa salah satu outletnya memiliki pembaruan model yang terkait:

// ViewController.swift
Import UIKit
// ...

class ViewController: UIViewController, CustomUserControlDelegate {

    // Properties
    // ...
    @IBOutlet weak var customUserControl: CustomUserControl!

    override func viewDidLoad() {
        super.viewDidLoad()
        // ...

        // Custom user control, handle through delegate callbacks.
        customUserControl = self
    }

    // ...

    // CustomUserControlDelegate
    func didChangeValue(value: Int) {
        // do some stuff with 'value' ...
    }

    // func didChangeValue(newValue: Int, oldValue: Int) {
        // do some stuff with new as well as old 'value' ...
        // custom transitions? :)
    //}

    //func didChangeValue(customUserControl: CustomUserControl) {
    //    // Do more advanced stuff ...
    //}
}

Di sini, valueproperti telah dienkapsulasi, tetapi secara umum: dalam situasi seperti ini, berhati-hatilah untuk tidak memperbarui valueproperti customUserControlobjek dalam lingkup fungsi delegasi terkait (di sini didChangeValue():) di pengontrol tampilan, atau Anda akan berakhir dengan rekursi tak terbatas.

dfri
sumber
4

Pengamat willSet dan didSet untuk properti setiap kali properti diberi nilai baru. Ini benar bahkan jika nilai baru sama dengan nilai saat ini.

Dan perhatikan bahwa willSetperlu nama parameter untuk bekerja, di sisi lain, didSettidak.

Pengamat didSet dipanggil setelah nilai properti diperbarui. Ini dibandingkan dengan nilai lama. Jika jumlah langkah telah meningkat, sebuah pesan dicetak untuk menunjukkan berapa banyak langkah baru telah diambil. Pengamat didSet tidak memberikan nama parameter khusus untuk nilai lama, dan sebagai gantinya nama default oldValue digunakan.

Zigii Wong
sumber
2

Getter dan setter terkadang terlalu berat untuk diterapkan hanya untuk mengamati perubahan nilai yang tepat. Biasanya ini membutuhkan penanganan variabel sementara tambahan dan pemeriksaan tambahan, dan Anda akan ingin menghindari bahkan tenaga kerja kecil jika Anda menulis ratusan getter dan setter. Barang-barang ini untuk situasi ini.

Eonil
sumber
1
Apakah Anda mengatakan bahwa ada keuntungan kinerja untuk menggunakan willSetdan didSetdibandingkan dengan kode setter yang setara? Ini sepertinya klaim yang berani.
zneak
1
@ zneak saya menggunakan kata yang salah. Saya mengklaim upaya programmer, bukan biaya pemrosesan.
Eonil
1

Di kelas (basis) Anda sendiri, willSetdan didSetcukup redundan , karena Anda dapat mendefinisikan properti yang dihitung (yaitu metode get dan set) yang mengakses a _propertyVariabledan melakukan pra-dan pasca-pemrosesan yang diinginkan .

Jika, bagaimanapun , Anda menimpa kelas di mana properti tersebut sudah ditetapkan , maka para willSetdan didSetyang berguna dan tidak berlebihan!

ragnarius
sumber
1

Satu hal didSetyang sangat berguna adalah ketika Anda menggunakan outlet untuk menambah konfigurasi tambahan.

@IBOutlet weak var loginOrSignupButton: UIButton! {
  didSet {
        let title = NSLocalizedString("signup_required_button")
        loginOrSignupButton.setTitle(title, for: .normal)
        loginOrSignupButton.setTitle(title, for: .highlighted)
  }
orkoden
sumber
atau menggunakan willSet membuat beberapa efek pada metode outlet ini, bukan?
Elia
-5

Saya tidak tahu C #, tetapi dengan sedikit tebakan saya pikir saya mengerti apa

foo : int {
    get { return getFoo(); }
    set { setFoo(newValue); }
}

tidak. Itu terlihat sangat mirip dengan apa yang Anda miliki di Swift, tetapi tidak sama: di Swift Anda tidak memiliki getFoodan setFoo. Itu tidak sedikit perbedaan: itu berarti Anda tidak memiliki penyimpanan yang mendasari untuk nilai Anda.

Swift telah menyimpan dan menghitung properti.

Properti yang dikomputasi telah getdan mungkin telah set(jika dapat ditulisi). Tetapi kode dalam pengambil dan penyetel, jika mereka perlu benar-benar menyimpan beberapa data, harus melakukannya di yang lain properti . Tidak ada penyimpanan dukungan.

Properti yang disimpan, di sisi lain, memang memiliki penyimpanan dukungan. Tetapi tidak memiliki getdan set. Sebaliknya ia memiliki willSetdan didSetyang dapat Anda gunakan untuk mengamati perubahan variabel dan, pada akhirnya, memicu efek samping dan / atau memodifikasi nilai yang disimpan. Anda tidak memiliki willSetdan didSetuntuk properti yang dihitung, dan Anda tidak membutuhkannya karena untuk properti yang dihitung Anda dapat menggunakan kode setuntuk mengontrol perubahan.

File Analog
sumber
Ini adalah contoh Swift. getFoodan setFooadalah penampung sederhana untuk apa pun yang Anda inginkan untuk dilakukan oleh pengambil dan setter. C # tidak membutuhkannya juga. (Saya memang melewatkan beberapa seluk-beluk sintaksis seperti yang saya minta sebelum saya memiliki akses ke kompiler.)
zneak
1
Oh oke. Tetapi poin penting adalah bahwa properti yang dihitung TIDAK memiliki penyimpanan yang mendasarinya. Lihat juga jawaban saya yang lain: stackoverflow.com/a/24052566/574590
File Analog