Dlaczego słowo kluczowe wygodne jest w ogóle potrzebne w Swift?

132

Ponieważ Swift obsługuje przeciążanie metod i inicjatorów, możesz umieścić wiele initobok siebie i użyć tego, co uznasz za wygodne:

class Person {
    var name:String

    init(name: String) {
        self.name = name
    }

    init() {
        self.name = "John"
    }
}

Dlaczego więc conveniencesłowo kluczowe w ogóle istniało? Co sprawia, że ​​poniższe elementy są znacznie lepsze?

class Person {
    var name:String

    init(name: String) {
        self.name = name
    }

    convenience init() {
        self.init(name: "John")
    }
}
Desmond Hume
źródło
13
Właśnie przeczytałem to w dokumentacji i też się z tym pogubiłem. : /
boidkan

Odpowiedzi:

236

Istniejące odpowiedzi mówią tylko połowę conveniencehistorii. Druga połowa historii, połowa, której żadna z istniejących odpowiedzi nie obejmuje, odpowiada na pytanie, które Desmond zamieścił w komentarzach:

Dlaczego Swift miałby zmuszać mnie do umieszczenia convenienceprzed moim inicjatorem tylko dlatego, że muszę self.initz niego zadzwonić ? `

Poruszyłem go nieco w tej odpowiedzi , w której szczegółowo omawiam kilka reguł inicjowania języka Swift, ale główny nacisk położono na requiredsłowo. Ale ta odpowiedź wciąż odnosiła się do czegoś, co jest istotne dla tego pytania i tej odpowiedzi. Musimy zrozumieć, jak działa dziedziczenie inicjalizatora Swift.

Ponieważ Swift nie zezwala na niezainicjowane zmienne, nie ma gwarancji, że odziedziczysz wszystkie (lub jakiekolwiek) inicjatory z klasy, z której dziedziczysz. Jeśli utworzymy podklasę i dodamy jakiekolwiek niezainicjowane zmienne instancji do naszej podklasy, przestaliśmy dziedziczyć inicjatory. Dopóki nie dodamy własnych inicjatorów, kompilator będzie na nas krzyczał.

Dla jasności, niezainicjowana zmienna instancji to dowolna zmienna instancji, której nie nadano wartości domyślnej (należy pamiętać, że opcje i niejawnie rozpakowane opcje opcjonalne automatycznie przyjmują wartość domyślną nil).

Więc w tym przypadku:

class Foo {
    var a: Int
}

ajest niezainicjowaną zmienną instancji. To się nie skompiluje, chyba że podamy awartość domyślną:

class Foo {
    var a: Int = 0
}

lub zainicjuj aw metodzie inicjalizującej:

class Foo {
    var a: Int

    init(a: Int) {
        self.a = a
    }
}

Zobaczmy teraz, co się stanie, jeśli podklasujemy Foo, dobrze?

class Bar: Foo {
    var b: Int

    init(a: Int, b: Int) {
        self.b = b
        super.init(a: a)
    }
}

Dobrze? Dodaliśmy zmienną i dodaliśmy inicjalizator, aby ustawić wartość, aby bmogła się skompilować. W zależności od tego, co język idziesz z, można spodziewać się, że Barodziedziczył Foo„s inicjator, init(a: Int). Ale tak nie jest. A jak to możliwe? Jak to Foojest init(a: Int)wiedza, jak przypisać wartość do bzmiennej, która Bardodana? Tak nie jest. Dlatego nie możemy zainicjować Barinstancji za pomocą inicjatora, który nie może zainicjować wszystkich naszych wartości.

Co to wszystko ma wspólnego convenience?

Przyjrzyjmy się regułom dziedziczenia inicjatorów :

Zasada nr 1

Jeśli twoja podklasa nie definiuje żadnych wyznaczonych inicjatorów, automatycznie dziedziczy wszystkie wyznaczone inicjatory nadklasy.

Zasada 2

Jeśli Twoja podklasa zapewnia implementację wszystkich wyznaczonych przez nią inicjatorów nadklasy - albo przez dziedziczenie ich zgodnie z regułą 1, albo przez dostarczenie niestandardowej implementacji jako części swojej definicji - wówczas automatycznie dziedziczy wszystkie inicjatory wygody nadklasy.

Zwróć uwagę na regułę 2, która wspomina o wygodnych inicjatorach.

Więc co convenienceHasło nie zrobić, to wskazać nam inicjalizatory które mogą być dziedziczone przez podklasy, że zmienne instancji dodatek bez wartości domyślnych.

Weźmy przykładową Baseklasę:

class Base {
    let a: Int
    let b: Int

    init(a: Int, b: Int) {
        self.a = a
        self.b = b
    }

    convenience init() {
        self.init(a: 0, b: 0)
    }

    convenience init(a: Int) {
        self.init(a: a, b: 0)
    }

    convenience init(b: Int) {
        self.init(a: 0, b: b)
    }
}

Zauważ, że mamy conveniencetutaj trzy inicjatory. Oznacza to, że mamy trzy inicjatory, które mogą być dziedziczone. Mamy jeden wyznaczony inicjator (wyznaczony inicjator to po prostu dowolny inicjalizator, który nie jest wygodnym inicjatorem).

Instancje klasy bazowej możemy utworzyć na cztery różne sposoby:

wprowadź opis obrazu tutaj

Stwórzmy więc podklasę.

class NonInheritor: Base {
    let c: Int

    init(a: Int, b: Int, c: Int) {
        self.c = c
        super.init(a: a, b: b)
    }
}

Dziedziczymy z Base. Dodaliśmy własną zmienną instancji i nie nadaliśmy jej wartości domyślnej, więc musimy dodać własne inicjatory. Dodaliśmy jeden, init(a: Int, b: Int, c: Int)ale nie pasuje podpis Baseklasy jest wyznaczony Inicjator: init(a: Int, b: Int). Oznacza to, że nie dziedziczymy żadnych inicjatorów z Base:

wprowadź opis obrazu tutaj

Więc co by się stało, gdybyśmy odziedziczyli po Base, ale poszliśmy dalej i zaimplementowaliśmy inicjator, który pasuje do wyznaczonego inicjatora z Base?

class Inheritor: Base {
    let c: Int

    init(a: Int, b: Int, c: Int) {
        self.c = c
        super.init(a: a, b: b)
    }

    convenience override init(a: Int, b: Int) {
        self.init(a: a, b: b, c: 0)
    }
}

Teraz, oprócz dwóch inicjatorów, które zaimplementowaliśmy bezpośrednio w tej klasie, ponieważ zaimplementowaliśmy inicjator pasujący Basedo wyznaczonego inicjatora klasy, możemy dziedziczyć wszystkie inicjatory Baseklasy convenience:

wprowadź opis obrazu tutaj

Fakt, że inicjator z pasującym podpisem jest oznaczony jako, conveniencenie ma tutaj znaczenia. Oznacza to tylko, że Inheritorma tylko jeden wyznaczony inicjator. Więc gdybyśmy dziedziczyli z Inheritor, musielibyśmy po prostu zaimplementować ten jeden wyznaczony inicjator, a następnie odziedziczylibyśmy Inheritorwygodny inicjator, co z kolei oznacza, że ​​zaimplementowaliśmy wszystkie Basewyznaczone inicjatory i możemy dziedziczyć jego convenienceinicjatory.

nhgrif
źródło
16
Jedyna odpowiedź, która faktycznie odpowiada na pytanie i podąża za dokumentami. Przyjąłbym to, gdybym był OP.
FreeNickname
12
Powinieneś napisać książkę;)
coolbeet
1
@SLN Ta odpowiedź zawiera wiele informacji o tym, jak działa dziedziczenie inicjalizatora Swift.
nhgrif
1
@SLN Ponieważ utworzenie paska z init(a: Int)pozostawiłoby bniezainicjalizowany.
Ian Warburton,
2
@IanWarburton Nie znam odpowiedzi na to „dlaczego”. Twoja logika w drugiej części twojego komentarza wydaje mi się rozsądna, ale dokumentacja jasno stwierdza, że ​​tak to działa, a przytoczenie przykładu tego, o co pytasz na placu zabaw, potwierdza, że ​​zachowanie jest zgodne z tym, co jest udokumentowane.
nhgrif
9

Głównie przejrzystość. Z twojego drugiego przykładu,

init(name: String) {
    self.name = name
}

jest wymagane lub wyznaczone . Musi zainicjować wszystkie stałe i zmienne. Wygodne inicjatory są opcjonalne i zwykle mogą być używane w celu ułatwienia inicjalizacji. Załóżmy na przykład, że Twoja klasa Person ma opcjonalną zmienną płeć:

var gender: Gender?

gdzie Płeć jest wyliczeniem

enum Gender {
  case Male, Female
}

możesz mieć takie wygodne inicjatory

convenience init(maleWithName: String) {
   self.init(name: name)
   gender = .Male
}

convenience init(femaleWithName: String) {
   self.init(name: name)
   gender = .Female
}

Wygodne inicjatory muszą wywoływać wyznaczone lub wymagane inicjatory w nich. Jeśli twoja klasa jest podklasą, musi wywołać super.init() w ramach swojej inicjalizacji.

Nate Mann
źródło
2
Dla kompilatora byłoby więc całkowicie oczywiste, co próbuję zrobić z wieloma inicjalizatorami, nawet bez conveniencesłowa kluczowego, ale Swift nadal by się o to martwił. To nie jest taka prostota, jakiej oczekiwałem od Apple =)
Desmond Hume
2
Ta odpowiedź na nic nie odpowiada. Powiedziałeś „przejrzystość”, ale nie wyjaśniłeś, jak to czyni cokolwiek jaśniejszym.
Robo Robok
7

Cóż, pierwszą rzeczą, która przychodzi mi do głowy, jest to, że jest używany w dziedziczeniu klas do organizacji kodu i czytelności. Kontynuując Personnaukę, pomyśl o takim scenariuszu

class Person{
    var name: String
    init(name: String){
        self.name = name
    }

    convenience init(){
        self.init(name: "Unknown")
    }
}


class Employee: Person{
    var salary: Double
    init(name:String, salary:Double){
        self.salary = salary
        super.init(name: name)
    }

    override convenience init(name: String) {
        self.init(name:name, salary: 0)
    }
}

let employee1 = Employee() // {{name "Unknown"} salary 0}
let john = Employee(name: "John") // {{name "John"} salary 0}
let jane = Employee(name: "Jane", salary: 700) // {{name "Jane"} salary 700}

Dzięki wygodnemu inicjalizatorowi jestem w stanie stworzyć Employee()obiekt bez wartości, stąd słowoconvenience

u54r
źródło
2
Po convenienceusunięciu słów kluczowych, czy Swift nie uzyska wystarczającej ilości informacji, aby zachowywać się dokładnie w ten sam sposób?
Desmond Hume
Nie, jeśli usuniesz conveniencesłowo kluczowe, nie możesz zainicjować Employeeobiektu bez żadnego argumentu.
u54r
W szczególności wywołanie Employee()wywołuje convenienceinicjator (inherited, due ) init(), który wywołuje self.init(name: "Unknown"). init(name: String), również wygodny inicjator dla Employee, wywołuje wyznaczony inicjator.
BallpointBen
1

Oprócz punktów, które wyjaśnili tutaj inni użytkownicy, jest moja odrobina zrozumienia.

Mocno czuję związek między wygodnym inicjatorem a rozszerzeniami. Jak dla mnie wygodne inicjatory są najbardziej przydatne, gdy chcę zmodyfikować (w większości przypadków uczynić to krótkim lub łatwym) inicjalizację istniejącej klasy.

Na przykład jakaś klasa innej firmy, której używasz, ma initcztery parametry, ale w twojej aplikacji ostatnie dwa mają tę samą wartość. Aby uniknąć więcej wpisywania i sprawić, by kod był czysty, możesz zdefiniować a convenience initz tylko dwoma parametrami i wewnątrz niego wywołać self.initparametry last to z wartościami domyślnymi.

Abdullah
źródło
1
Dlaczego Swift miałby zmuszać mnie do umieszczenia convenienceprzed moim inicjatorem tylko dlatego, że muszę self.initz niego zadzwonić ? Wydaje się to zbędne i trochę niewygodne.
Desmond Hume
1

Zgodnie z dokumentacją Swift 2.1 , convenienceinicjatory muszą przestrzegać pewnych określonych zasad:

  1. convenienceInicjator może wywołać tylko intializers w tej samej klasie, nie w Super klas (tylko w poprzek, a nie w górę)

  2. convenienceInicjator musi zadzwonić wyznaczony initializer gdzieś w sieci

  3. convenienceInicjator nie może zmienić ŻADNEJ własność zanim wezwał inny inicjator - natomiast wyznaczony inicjator musi zainicjować właściwości, które zostały wprowadzone przez obecną klasę przed wywołaniem kolejnej inicjatora.

Używając conveniencesłowa kluczowego, kompilator Swift wie, że musi sprawdzić te warunki - w przeciwnym razie nie mógłby.

Oko
źródło
Prawdopodobnie kompilator mógłby to rozwiązać bez conveniencesłowa kluczowego.
nhgrif
Ponadto twoja trzecia uwaga jest myląca. Wygodny inicjator może zmieniać tylko właściwości (i nie może zmieniać letwłaściwości). Nie może zainicjować właściwości. Wyznaczony inicjator jest odpowiedzialny za zainicjowanie wszystkich wprowadzonych właściwości przed wywołaniem superwyznaczonego inicjatora.
nhgrif,
1
Przynajmniej słowo kluczowe wygody wyjaśnia deweloperowi, liczy się również czytelność (plus sprawdzenie inicjalizatora pod kątem oczekiwań programisty). Twoja druga uwaga jest dobra, odpowiednio zmieniłem odpowiedź.
TheEye,
1

Klasa może mieć więcej niż jeden wyznaczony inicjator. Wygodny inicjator to pomocniczy inicjator, który musi wywołać wyznaczony inicjator tej samej klasy.

Abdul Yasin
źródło