Jaki jest cel willSet i didSet w Swift?


265

Swift ma składnię deklaracji właściwości bardzo podobną do C #:

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

Ma jednak willSeti didSetdziałania. Są one wywoływane odpowiednio przed i po wywołaniu settera. Jaki jest ich cel, biorąc pod uwagę, że możesz mieć ten sam kod w seterie?


11
Osobiście nie lubię wielu odpowiedzi tutaj. Za bardzo idą w dół do składni. Różnice dotyczą bardziej semantyki i czytelności kodu. Obliczona właściwość ( get& set) polega zasadniczo na obliczeniu właściwości na podstawie innej właściwości, np. Konwersji etykiet textna rok Int. didSeti willSetczy możemy powiedzieć ... hej, ta wartość została ustawiona, teraz zróbmy to, np. Nasze źródło danych zostało zaktualizowane ... więc przeładujmy tableView, aby zawierał nowe wiersze. Aby zobaczyć inny przykład, zobacz odpowiedź dfri na temat dzwonienia do delegatówdidSet
Honey,

Odpowiedzi:


324

Chodzi o to, że czasami potrzebujesz właściwości, która ma automatyczne przechowywanie i pewne zachowanie, na przykład, aby powiadomić inne obiekty, że właściwość właśnie się zmieniła. Gdy wszystko, co masz, to get/ set, potrzebujesz innego pola do przechowywania wartości. Za pomocą willSeti didSetmożesz podejmować działania, gdy wartość jest modyfikowana bez potrzeby używania innego pola. Na przykład w tym przykładzie:

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

myPropertywypisuje swoją starą i nową wartość przy każdej modyfikacji. Z tylko getterami i setterami potrzebowałbym tego zamiast tego:

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

Więc willSeti didSetreprezentują ekonomię kilku liniach, i mniej hałasu w liście pól.


248
Uwaga: willSeti didSetnie są wywoływane, gdy ustawia się właściwość z metody init, jak zauważa 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
Ale wydaje się, że są one wywoływane we właściwościach tablic, gdy to robią: myArrayProperty.removeAtIndex(myIndex)... Nie oczekiwane.
Andreas

4
Możesz zawinąć przypisanie w instrukcję defer {} w inicjalizatorze, która powoduje, że metody willSet i didSet są wywoływane po wyjściu z zakresu inicjalizatora. Niekoniecznie polecam, mówiąc tylko, że to możliwe. Jedną z konsekwencji jest to, że działa tylko wtedy, gdy zadeklarujesz właściwość jako opcjonalną, ponieważ nie jest ona ściśle inicjowana z inicjalizatora.
Marmoy

Proszę wyjaśnić poniżej linii. Nie otrzymuję, czy ta metoda lub zmienna var propertyChangedListener: (Int, Int) -> Void = {println („Wartość myProperty zmieniła się z (0 $) na (1 $)”)}
Vikash Rajput

Inicjalizacja właściwości w tym samym wierszu NIE jest obsługiwana w Swift 3. Powinieneś zmienić odpowiedź, aby dostosować swift 3.
Ramazan Polat

149

Rozumiem, że set i get są dla obliczonych właściwości (brak kopii zapasowej z przechowywanych właściwości )

jeśli pochodzisz z Celu-C, pamiętaj, że zmieniły się konwencje nazewnictwa. W Swift zmienna iVar lub zmienna instancji nosi nazwę właściwości przechowywanej

Przykład 1 (właściwość tylko do odczytu) - z ostrzeżeniem:

var test : Int {
    get {
        return test
    }
}

Powoduje to ostrzeżenie, ponieważ powoduje rekurencyjne wywołanie funkcji (sam getter wywołuje sam). Ostrzeżenie w tym przypadku to „Próba zmodyfikowania„ testu ”we własnym getterze”.

Przykład 2. Warunkowy odczyt / zapis - z ostrzeżeniem

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

Podobny problem - nie możesz tego zrobić, ponieważ rekurencyjnie wywołuje setera. Pamiętaj też, że ten kod nie będzie narzekał na brak inicjatorów, ponieważ nie ma przechowywanej właściwości do inicjalizacji .

Przykład 3. odczyt / zapis obliczonej właściwości - z zapleczem

Oto wzorzec, który umożliwia warunkowe ustawienie rzeczywistej przechowywanej właściwości

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

Uwaga Rzeczywiste dane nazywa się _test (chociaż mogą to być dowolne dane lub kombinacja danych). Należy również zwrócić uwagę na potrzebę podania wartości początkowej (alternatywnie musisz użyć metody init), ponieważ _test jest w rzeczywistości zmienną instancji

Przykład 4. Korzystanie z will i did set

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

Widzimy tutaj willSet i didSet przechwytujące zmianę w rzeczywistej przechowywanej właściwości. Jest to przydatne do wysyłania powiadomień, synchronizacji itp. (Patrz przykład poniżej)

Przykład 5. Konkretny przykład - Kontener 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
        }
    }
}

Zwróć uwagę na użycie OBA obliczonych i przechowywanych właściwości. Użyłem obliczonej właściwości, aby zapobiec dwukrotnemu ustawianiu tej samej wartości (aby uniknąć złych rzeczy!); Użyłem willSet i didSet do przekazywania powiadomień do viewControllers (zobacz dokumentację UIViewController i informacje o kontenerach viewController)

Mam nadzieję, że to pomoże, i proszę, niech ktoś krzyknie, jeśli popełniłem błąd tutaj!


3
Dlaczego nie mogę używać Używam didSet razem z get i set ..?
Ben Sinclair

//I can't see a way to 'stop' the value being set to the same controller - hence the computed property ostrzeżenie zniknie po tym, jak użyłem if let newViewController = _childVC { zamiast if (_childVC) {
evfemist

5
get i set służą do tworzenia obliczonej właściwości. Są to wyłącznie metody i nie ma pamięci zapasowej (zmienna instancji). willSet i didSet służą do obserwowania zmian w przechowywanych właściwościach zmiennych. Pod maską są one zabezpieczone przechowywaniem, ale w Swift wszystko jest połączone.
user3675131

W twoim przykładzie 5 getmyślę, że musisz dodać, if _childVC == nil { _childVC = something }a następnie return _childVC.
JW.ZG

18

Są to tak zwani obserwatorzy właściwości :

Obserwatorzy nieruchomości obserwują zmiany wartości nieruchomości i reagują na nie. Obserwatorzy właściwości są wywoływani za każdym razem, gdy ustawiana jest wartość właściwości, nawet jeśli nowa wartość jest taka sama jak bieżąca wartość właściwości.

Fragment: Apple Inc. „Swift Programming Language”. iBooks. https://itun.es/ca/jEUH0.l

Podejrzewam, że pozwala to na rzeczy, które tradycyjnie robimy z KVO, takie jak wiązanie danych z elementami interfejsu użytkownika lub wywoływanie skutków ubocznych zmiany właściwości, uruchamiania procesu synchronizacji, przetwarzania w tle itp.


16

UWAGA

willSeta didSetobserwatorzy nie są wywoływani, gdy właściwość jest ustawiona w inicjalizatorze przed delegowaniem


16

Możesz także użyć, didSetaby ustawić zmienną na inną wartość. Nie powoduje to ponownego wezwania obserwatora, jak podano w Przewodniku po właściwościach . Na przykład przydaje się, gdy chcesz ograniczyć wartość, jak poniżej:

let minValue = 1

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

value = -10 // value is minValue now.

10

Wiele dobrze napisanych istniejących odpowiedzi dobrze obejmuje pytanie, ale wspomnę bardziej szczegółowo o dodatku, który moim zdaniem jest wart omówienia.


Te willSeti didSetwłasności obserwatorzy mogą być używane do wywołania delegatów, na przykład, dla właściwości klasy, które są zawsze tylko zaktualizowane przez interakcji użytkownika, ale gdzie chcesz uniknąć wywołanie delegata przy inicjalizacji obiektu.

Przytoczę podniesiony komentarz Klaasa do zaakceptowanej odpowiedzi:

Obserwatory willSet i didSet nie są wywoływane przy pierwszej inicjalizacji właściwości. Są wywoływane tylko wtedy, gdy wartość właściwości jest ustawiona poza kontekstem inicjalizacji.

Jest to dość schludne, ponieważ oznacza, że ​​np. didSetWłaściwość jest dobrym wyborem punktu uruchamiania dla wywołań zwrotnych i funkcji delegowanych dla własnych klas niestandardowych.

Jako przykład rozważmy niestandardowy obiekt kontroli użytkownika z pewną kluczową właściwością value(np. Pozycja w kontroli oceny), zaimplementowany jako podklasa 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.
}

Po czym funkcje delegowane mogą być używane, powiedzmy, w niektórych kontrolerach widoków do obserwowania kluczowych zmian w modelu CustomViewController, podobnie jak w przypadku korzystania z nieodłącznych funkcji delegowania obiektów UITextFieldDelegatefor UITextField(np textFieldDidEndEditing(...).).

W tym prostym przykładzie użyj wywołania zwrotnego delegowanego z didSetwłaściwości class, valueaby poinformować kontroler widoku, że jedno z jego gniazd ma powiązaną aktualizację modelu:

// 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 ...
    //}
}

Tutaj valuewłaściwość została enkapsulowana, ale ogólnie: w takich sytuacjach uważaj, aby nie aktualizować valuewłaściwości customUserControlobiektu w zakresie powiązanej funkcji delegowania (tutaj didChangeValue():) w kontrolerze widoku, w przeciwnym razie skończysz na nieskończona rekurencja.


4

Obserwatorzy willSet i didSet dla właściwości za każdym razem, gdy właściwość ma przypisaną nową wartość. Jest to prawdą, nawet jeśli nowa wartość jest taka sama jak bieżąca wartość.

Zauważ, że willSetpotrzebna jest nazwa parametru do obejścia, z drugiej strony didSetnie.

Obserwator didSet jest wywoływany po zaktualizowaniu wartości właściwości. Porównuje się ze starą wartością. Jeśli całkowita liczba kroków wzrosła, drukowany jest komunikat wskazujący, ile nowych kroków zostało wykonanych. Obserwator didSet nie podaje niestandardowej nazwy parametru dla starej wartości, a zamiast tego używana jest domyślna nazwa oldValue.


2

Getter i setter są czasami zbyt ciężkie, aby je wdrożyć, aby zaobserwować właściwe zmiany wartości. Zwykle wymaga to dodatkowej obsługi zmiennych tymczasowych i dodatkowych kontroli, a ty będziesz unikać nawet tych drobnych robocizny, jeśli napiszesz setki pobierających i ustawiających. Te rzeczy są odpowiednie do sytuacji.


1
Czy twierdzisz, że używanie i równoważny kod ustawiający ma przewagę wydajności ? To wydaje się śmiałym twierdzeniem. willSetdidSet
zneak

1
@zneak Użyłem złego słowa. Żądam wysiłku programisty, a nie kosztów przetwarzania.
Eonil

1

We własnej (podstawowej) klasie willSeti didSetsą one dość redundantne , ponieważ zamiast tego można zdefiniować obliczoną właściwość (tj. Metody get- i set-), która uzyskuje dostęp _propertyVariabledo żądanej operacji poprzedzającej i postprocesowej .

Jeżeli jednak , by zastąpić klasę, w którym nieruchomość jest już zdefiniowany , następniewillSet i didSetużyteczne i nie zbędny!


1

Jedną z rzeczy, która didSetjest naprawdę przydatna, jest użycie gniazdek w celu dodania dodatkowej konfiguracji.

@IBOutlet weak var loginOrSignupButton: UIButton! {
  didSet {
        let title = NSLocalizedString("signup_required_button")
        loginOrSignupButton.setTitle(title, for: .normal)
        loginOrSignupButton.setTitle(title, for: .highlighted)
  }

lub użycie willSet ma sens, aby wpływać na metody tych punktów sprzedaży, prawda?
elia

-5

Nie znam C #, ale przy odrobinie zgadywania myślę, że rozumiem co

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

robi. Wygląda bardzo podobnie do tego, co masz w Swift, ale to nie to samo: w Swift nie masz getFooi setFoo. To nie jest mała różnica: oznacza, że ​​nie masz żadnego bazowego miejsca na swoją wartość.

Swift ma zapisane i obliczone właściwości.

Obliczona właściwość ma geti może mieć set(jeśli jest zapisywalna). Ale kod w module pobierającym i ustawiającym, jeśli muszą faktycznie przechowywać niektóre dane, muszą to zrobić w innych właściwościach. Nie ma miejsca na kopię zapasową.

Z drugiej strony przechowywana właściwość ma pamięć zapasową. Ale nie ma geti set. Zamiast tego ma willSeti za pomocą didSetktórego można obserwować zmiany zmiennych i ostatecznie wywoływać skutki uboczne i / lub modyfikować zapisaną wartość. Nie masz willSeti didSetdla obliczonych właściwości i nie potrzebujesz ich, ponieważ dla obliczonych właściwości możesz użyć kodu setdo kontrolowania zmian.


To jest przykład Swift. getFooi setFoosą prostymi symbolami zastępczymi dla wszystkiego, co chcesz, aby robiły i ustawiające. C # też ich nie potrzebuje. (Zrobiłem przegap kilka składniowe subtelności jak poprosiłem wcześniej miałem dostęp do kompilatora.)
zneak

1
No dobrze Ale ważne jest to, że obliczona właściwość NIE ma podstawowej pamięci. Zobacz także moją inną odpowiedź: stackoverflow.com/a/24052566/574590
Plik analogowy
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.