Po co tworzyć „niejawnie nieopakowane opcje”, ponieważ oznacza to, że wiesz, że istnieje wartość?


493

Dlaczego miałbyś stworzyć „niejawnie nieopakowaną opcję” zamiast tworzyć zwykłą zmienną lub stałą? Jeśli wiesz, że można go pomyślnie rozpakować, to po co w ogóle tworzyć opcjonalne? Na przykład dlaczego:

let someString: String! = "this is the string"

będą bardziej przydatne niż:

let someString: String = "this is the string"

Jeśli ”opcjonalne wskazuje, że stała lub zmienna może mieć„ brak wartości ””, ale „czasami ze struktury programu wynika, że ​​opcjonalne zawsze będą miały wartość po pierwszym ustawieniu tej wartości”, jaki jest sens czy w ogóle jest to opcjonalne? Jeśli wiesz, że opcja zawsze będzie miała wartość, czy nie oznacza to, że nie jest opcjonalna?

Odpowiedzi:


127

Rozważ przypadek obiektu, który może mieć zero właściwości podczas jego konstruowania i konfigurowania, ale jest niezmienny i później zerowy (NSImage jest często traktowany w ten sposób, chociaż w jego przypadku nadal czasem przydatne jest mutowanie). Niejawnie nieopakowane opcje wyczyściłyby jego kod całkiem dobrze, przy stosunkowo niskiej utracie bezpieczeństwa (o ile posiadana była jedna gwarancja, byłby bezpieczny).

(Edytuj) Żeby było jasne: prawie zawsze preferowane są zwykłe opcje.


459

Zanim będę mógł opisać przypadki użycia opcji niejawnie nieopakowanych, powinieneś już zrozumieć, jakie są opcje i niejawnie nieopakowane opcje w programie Swift. Jeśli nie, polecam najpierw przeczytać mój artykuł na temat opcji

Kiedy używać opcji niejawnie nieopakowanej

Istnieją dwa główne powody, dla których można utworzyć niejawnie nieopakowane opcjonalne. Wszystko to ma związek ze zdefiniowaniem zmiennej, do której nigdy nie będzie dostęp, nilponieważ w przeciwnym razie kompilator Swift zawsze zmusi cię do jawnego rozpakowania Opcjonalnego.

1. Stała, której nie można zdefiniować podczas inicjalizacji

Każda stała elementu musi mieć wartość przed zakończeniem inicjalizacji. Czasami stała nie może zostać zainicjalizowana z jej prawidłową wartością podczas inicjalizacji, ale nadal można zagwarantować, że będzie miała wartość przed uzyskaniem dostępu.

Użycie zmiennej opcjonalnej pozwala obejść ten problem, ponieważ opcja jest inicjowana automatycznie, nila wartość, którą ostatecznie będzie zawierała, pozostanie niezmienna. Jednak ciągłe rozpakowywanie zmiennej, która na pewno nie jest zerowa, może być uciążliwe. Niejawnie nieopakowane opcje osiągają takie same korzyści jak opcjonalne z dodatkową korzyścią, że nie trzeba ich wszędzie rozpakowywać.

Świetnym przykładem tego jest sytuacja, gdy zmiennej członka nie można zainicjować w podklasie UIView, dopóki widok nie zostanie załadowany:

class MyView: UIView {
    @IBOutlet var button: UIButton!
    var buttonOriginalWidth: CGFloat!

    override func awakeFromNib() {
        self.buttonOriginalWidth = self.button.frame.size.width
    }
}

W tym przypadku nie można obliczyć oryginalnej szerokości przycisku, dopóki widok nie zostanie załadowany, ale wiadomo, że awakeFromNibzostanie on wywołany przed jakąkolwiek inną metodą w widoku (inną niż inicjalizacja). Zamiast wymuszać jawne rozpakowywanie wartości bez sensu w całej klasie, możesz zadeklarować ją jako niejawnie nieopakowaną opcję.

2. Gdy aplikacja nie może odzyskać sprawności po zmiennej treści nil

Powinno to być niezwykle rzadkie, ale jeśli aplikacja nie może kontynuować działania, jeśli zmienna jest nildostępna, dostęp do niej byłby stratą czasu nil. Zwykle jeśli masz warunek, który musi być absolutnie prawdziwy, aby aplikacja mogła nadal działać, możesz użyć assert. Implicitly Unwrapped Opcjonalnie ma wbudowane w nią potwierdzenie zerowe. Nawet wtedy dobrze jest rozpakować opcjonalne i użyć bardziej opisowego potwierdzenia, jeśli jest zero.

Kiedy nie należy używać opcji niejawnie nieopakowanej

1. Zmienne obliczane przez Lazily

Czasami masz zmienną składową, która nigdy nie powinna być zerowa, ale podczas inicjalizacji nie można jej ustawić poprawnej wartości. Jednym z rozwiązań jest użycie opcjonalnie nieopakowanej opcji, ale lepszym sposobem jest użycie leniwej zmiennej:

class FileSystemItem {
}

class Directory : FileSystemItem {
    lazy var contents : [FileSystemItem] = {
        var loadedContents = [FileSystemItem]()
        // load contents and append to loadedContents
        return loadedContents
    }()
}

Teraz zmienna contentsskładowa nie jest inicjowana aż do pierwszego dostępu. Daje to klasie szansę na przejście do właściwego stanu przed obliczeniem wartości początkowej.

Uwaga: może to wydawać się sprzeczne z nr 1 z góry. Należy jednak wprowadzić istotne rozróżnienie. buttonOriginalWidthPowyżej muszą być ustawione podczas viewDidLoad aby zapobiec nikomu zmieniających przyciski Szerokość przed nieruchomość jest dostępna.

2. Wszędzie gdzie indziej

W przeważającej części należy unikać opcji niejawnie nieopakowanych, ponieważ jeśli zostanie użyte błędnie, cała aplikacja ulegnie awarii, gdy będzie do niej dostęp nil. Jeśli nie masz pewności, czy zmienna może być zerowa, zawsze domyślnie używaj normalnej Opcjonalnej. Rozpakowanie zmiennej, która z nilpewnością nigdy nie jest bardzo bolesna.


4
Ta odpowiedź powinna zostać zaktualizowana do wersji beta 5. Nie możesz już używać if someOptional.
Święty Mikołaj

2
@SantaClaus hasValuejest zdefiniowane bezpośrednio w Opcjonalne. Wolę semantykę od hasValuetych != nil. Wydaje mi się, że jest to znacznie bardziej zrozumiałe dla nowych programistów, którzy nie używali nilinnych języków. hasValuejest o wiele bardziej logiczne niż nil.
drewag

2
Wygląda na to, że hasValuezostał wycofany z wersji beta 6. Ash jednak odłożył go z powrotem ... github.com/AshFurrow/hasValue
Chris Wagner

1
@newacct Jeśli chodzi o typ zwracanych inicjatorów Objc, jest to niejawnie niejawnie nieopakowane. Zachowanie, które opisałeś przy użyciu „nie opcjonalnego”, jest dokładnie tym, co zrobiłby niejawnie nieopakowany Opcjonalny (nie zawiódł, dopóki nie uzyskasz dostępu). Jeśli chodzi o wcześniejsze zawalenie programu przez wymuszone rozpakowanie, zgadzam się, że preferowane jest użycie opcji nieobowiązkowej, ale nie zawsze jest to możliwe.
drewag

1
@confile no. Bez względu na wszystko, pojawi się w Objective-C jako wskaźnik (jeśli byłby opcjonalny, domyślnie rozpakowany lub nie opcjonalny).
Drawag

56

Niejawnie nieopakowane opcje są przydatne do przedstawienia właściwości jako nie opcjonalnej, gdy tak naprawdę musi być opcjonalna pod przykryciem. Jest to często konieczne do „wiązania węzła” między dwoma powiązanymi obiektami, z których każdy potrzebuje odniesienia do drugiego. Ma to sens, gdy żadne odwołanie nie jest w rzeczywistości opcjonalne, ale jeden z nich musi być zerowy podczas inicjowania pary.

Na przykład:

// These classes are buddies that never go anywhere without each other
class B {
    var name : String
    weak var myBuddyA : A!
    init(name : String) {
        self.name = name
    }
}

class A {
    var name : String
    var myBuddyB : B
    init(name : String) {
        self.name = name
        myBuddyB = B(name:"\(name)'s buddy B")
        myBuddyB.myBuddyA = self
    }
}

var a = A(name:"Big A")
println(a.myBuddyB.name)   // prints "Big A's buddy B"

Każde Bwystąpienie powinno zawsze mieć prawidłowe myBuddyAodwołanie, więc nie chcemy, aby użytkownik traktował je jako opcjonalne, ale potrzebujemy, aby było opcjonalne, abyśmy mogli skonstruować Bprzed nim A.

JEDNAK! Tego rodzaju wymóg wzajemnego odniesienia jest często oznaką ścisłego połączenia i złej konstrukcji. Jeśli okaże się, że polegasz na niejawnie nieopakowanych opcjach, prawdopodobnie powinieneś rozważyć refaktoryzację w celu wyeliminowania wzajemnych zależności.


7
Myślę, że jednym z powodów, dla których stworzyli tę funkcję językową, jest@IBOutlet
Jiaaro

11
+1 za zastrzeżenie „JEDNAK”. Może nie zawsze tak jest, ale na pewno jest coś, na co trzeba uważać.
JMD

4
Nadal masz silny cykl odniesienia między A i B. Niejawnie nieopakowane opcje NIE tworzą słabego odniesienia. Nadal musisz zadeklarować myByddyA lub myBuddyB jako słaby (prawdopodobnie myBuddyA)
zwrócił się

6
Aby jeszcze bardziej wyjaśnić, dlaczego ta odpowiedź jest błędna i niebezpiecznie wprowadzająca w błąd: niejawnie nieopakowane opcje nie mają absolutnie nic wspólnego z zarządzaniem pamięcią i nie zapobiegają cyklom przechowywania. Jednak niejawnie nieopakowane opcje są nadal przydatne w okolicznościach opisanych dla konfiguracji dwukierunkowego odniesienia. Więc po prostu dodając weakdeklarację i usuwając „bez tworzenia silnego cyklu przechowywania”
narysował

1
@drewag: Masz rację - zredagowałem odpowiedź, aby usunąć cykl przechowywania. Chciałem osłabić odniesienie wsteczne, ale myślę, że wymknęło mi się to z głowy.
n8gray

37

Niejawnie nieopakowane opcje są pragmatycznym kompromisem, aby uczynić pracę w środowisku hybrydowym, które musi współpracować z istniejącymi strukturami Cocoa i ich konwencjami, a jednocześnie umożliwia stopniową migrację do bezpieczniejszego paradygmatu programistycznego - bez zerowych wskaźników - wymuszonym przez kompilator Swift.

Szybka książka, w rozdziale Podstawy , sekcja Implicitly Unwrapped Optionals mówi:

Niejawnie nieopakowane opcje są przydatne, gdy wartość opcji jest potwierdzona, że ​​istnieje natychmiast po jej zdefiniowaniu, i można zdecydowanie założyć, że istnieje ona w każdym punkcie później. Podstawowym zastosowaniem niejawnie nieopakowanych opcji w Swift jest inicjalizacja klasy, jak opisano w Nieznanych referencjach i Niejawnie nieopakowanych właściwościach opcjonalnych .

Możesz myśleć o nieopakowanym niejawnie opcjonalnym wyrażeniu zgody na automatyczne rozpakowywanie opcjonalnego za każdym razem, gdy jest używane. Zamiast umieszczać wykrzyknik po nazwie opcjonalnego za każdym razem, gdy go używasz, umieszczasz wykrzyknik po typie opcjonalnego, kiedy go deklarujesz.

To wszystko sprowadza się do przypadków użycia, gdzie nie- nil-ness właściwości ma siedzibę poprzez konwencję użytkowania, i nie mogą być egzekwowane przez kompilator podczas inicjalizacji klasy. Na przykład UIViewControllerwłaściwości, które są inicjowane z NIB lub Storyboard, gdzie inicjalizacja jest podzielona na osobne fazy, ale po tym viewDidLoad()można założyć, że właściwości na ogół istnieją. W przeciwnym razie, aby spełnić wymagania kompilatora, trzeba było użyć wymuszonego rozpakowywania , opcjonalnego wiązania lub opcjonalnego łączenia tylko w celu zaciemnienia głównego celu kodu.

Powyższa część książki Swift odnosi się również do rozdziału Automatyczne zliczanie odniesień :

Istnieje jednak trzeci scenariusz, w którym obie właściwości powinny zawsze mieć wartość, a żadna z właściwości nie powinna być nigdy nilpo zakończeniu inicjalizacji. W tym scenariuszu przydatne jest połączenie nieposiadanej właściwości w jednej klasie z niejawnie nieopakowaną właściwością opcjonalną w drugiej klasie.

Umożliwia to bezpośredni dostęp do obu właściwości (bez opcjonalnego rozpakowywania) po zakończeniu inicjalizacji, przy jednoczesnym unikaniu cyklu odniesienia.

Sprowadza się to do dziwactwa, że ​​nie jest językiem zbierającym śmieci, dlatego łamanie cykli zatrzymania spoczywa na tobie jako programistie i niejawnie nieopakowane opcje są narzędziem do ukrycia tego dziwactwa.

Obejmuje to „Kiedy używać niejawnie rozpakowanych opcji w swoim kodzie?” pytanie. Jako twórca aplikacji najczęściej spotykasz je w sygnaturach metod bibliotek napisanych w Objective-C, które nie mają możliwości wyrażania typów opcjonalnych.

Od używania Swift z kakao i Objective-C, sekcja Praca z nilem :

Ponieważ Objective-C nie daje żadnych gwarancji, że obiekt jest inny niż zero, Swift sprawia, że ​​wszystkie klasy w typach argumentów i zwracane typy są opcjonalne w importowanych API Objective-C. Przed użyciem obiektu Objective-C należy sprawdzić, czy go nie brakuje.

W niektórych przypadkach możesz być absolutnie pewien, że metoda lub właściwość Objective-C nigdy nie zwraca nilodwołania do obiektu. Aby uczynić obiekty w tym specjalnym scenariuszu wygodniejszym do pracy, Swift importuje typy obiektów jako niejawnie nieopakowane opcje . Niejawnie nieopakowane typy opcjonalne obejmują wszystkie funkcje bezpieczeństwa typów opcjonalnych. Ponadto można uzyskać bezpośredni dostęp do wartości bez sprawdzanianillub rozpakuj go sam. Gdy uzyskujesz dostęp do wartości w tego rodzaju opcjonalnym typie bez bezpiecznego jej wcześniejszego rozpakowania, niejawnie rozpakowana opcjonalna sprawdza, czy brakuje wartości. Jeśli brakuje wartości, występuje błąd czasu wykonywania. W rezultacie zawsze należy samodzielnie sprawdzić i rozpakować opcjonalnie nieopakowane opcjonalne, chyba że masz pewność, że wartości nie można pominąć.

... i dalej leżą tutaj smoki


Dzięki za tę dokładną odpowiedź. Czy potrafisz wymyślić szybką listę kontrolną, kiedy użyć opcji niejawnie nieopakowanych, a kiedy wystarczy standardowa zmienna?
Hairgami_Master

@Hairgami_Master Dodałem własną odpowiedź z listą i konkretnymi przykładami
drawag

18

Proste, jednowierszowe (lub kilkuliniowe) przykłady nie pokrywają dobrze zachowania opcjonalnego - tak, jeśli zadeklarujesz zmienną i podasz jej wartość od razu, nie ma sensu opcjonalne.

Najlepszym przypadkiem, jaki do tej pory widziałem, jest konfiguracja, która następuje po zainicjowaniu obiektu, a następnie użycie, które „gwarantuje” przestrzeganie tej konfiguracji, np. W kontrolerze widoku:

class MyViewController: UIViewController {

    var screenSize: CGSize?

    override func viewDidLoad {
        super.viewDidLoad()
        screenSize = view.frame.size
    }

    @IBAction printSize(sender: UIButton) {
        println("Screen size: \(screenSize!)")
    }
}

Wiemy, że printSizezostanie wywołany po załadowaniu widoku - jest to metoda akcji podłączona do kontrolki w tym widoku, i upewniliśmy się, że inaczej go nie wywołamy. Możemy więc zaoszczędzić sobie trochę opcjonalnego sprawdzania / wiązania z !. Swift nie rozpoznaje tej gwarancji (przynajmniej dopóki Apple nie rozwiąże problemu zatrzymania), więc powiesz kompilatorowi, że istnieje.

Jednak w pewnym stopniu narusza to bezpieczeństwo typu. Dowolne miejsce, w którym masz domyślnie nieopakowane opcjonalne, jest miejscem, w którym aplikacja może się zawiesić, jeśli Twoja „gwarancja” nie zawsze się utrzymuje, więc jest to funkcja, której należy używać oszczędnie. Poza tym ciągłe używanie !sprawia, że ​​brzmi to tak, jakbyś krzyczał, a nikomu się to nie podoba.


Dlaczego nie zainicjować screenSize do CGSize (wysokość: 0, szerokość: 0) i zaoszczędzić kłopotu z koniecznością krzyczenia zmiennej za każdym razem, gdy się do niej uzyskujesz?
Martin Gordon,

Rozmiar może nie być najlepszym przykładem, ponieważ CGSizeZeromoże być dobrą wartością wartownika w rzeczywistym użyciu. Ale co, jeśli masz rozmiar załadowany ze stalówki, który może faktycznie wynosić zero? Zatem użycie CGSizeZerojako wartownika nie pomaga w rozróżnieniu między wartością nieuzbrojoną a ustawioną na zero. Co więcej, odnosi się to równie dobrze do innych typów ładowanych ze stalówki (lub gdziekolwiek indziej po init): ciągów, odniesień do widoków podrzędnych itp.
rickster

2
Częścią opcjonalną w językach funkcjonalnych jest brak wartości wartowników. Albo masz wartość, albo nie. Nie powinieneś mieć przypadku, w którym masz wartość wskazującą brakującą wartość.
Wayne Tanner

4
Myślę, że źle zrozumiałeś pytanie OP. OP nie pyta o ogólny przypadek opcji, a konkretnie o potrzebę / użycie niejawnie nieopakowanych opcji (tj. let foo? = 42Raczej nie let foo! = 42). To nie dotyczy tego. (Może to być odpowiednia odpowiedź na temat opcji, pamiętaj, ale nie na temat niejawnie rozpakowanych opcji, które są innym / powiązanym zwierzęciem).
JMD

15

Apple podaje świetny przykład w Swift Programming Language -> Automatyczne zliczanie referencji -> Rozwiązywanie silnych cykli referencyjnych między instancjami klasy -> Nieznane referencje i niejawnie nieopakowane właściwości opcjonalne

class Country {
    let name: String
    var capitalCity: City! // Apple finally correct this line until 2.0 Prerelease (let -> var)
    init(name: String, capitalName: String) {
        self.name = name
        self.capitalCity = City(name: capitalName, country: self)
    }
}

class City {
    let name: String
    unowned let country: Country
    init(name: String, country: Country) {
        self.name = name
        self.country = country
    }
}

Inicjator dla Cityjest wywoływany z poziomu inicjatora dla Country. Jednak inicjalizator dla Countrynie może przejść selfdo Cityinicjalizatora, dopóki nowa Countryinstancja nie zostanie w pełni zainicjowana, jak opisano w Inicjalizacji dwufazowej .

Aby poradzić sobie z tym wymogiem, deklarujesz capitalCitywłaściwość Countryjako domyślnie nieopakowaną właściwość opcjonalną.


Samouczek wymieniony w tej odpowiedzi jest tutaj .
Franklin Yu,

3

Uzasadnienie ukrytych opcji jest łatwiejsze do wyjaśnienia, najpierw analizując uzasadnienie wymuszonego rozpakowywania.

Wymuszone rozpakowanie opcjonalne (niejawne lub nie) za pomocą! operator oznacza, że ​​masz pewność, że Twój kod nie zawiera błędów, a opcjonalny ma już wartość, w której jest rozpakowywany. Bez ! operator, prawdopodobnie po prostu powiedziałbyś z opcjonalnym powiązaniem:

 if let value = optionalWhichTotallyHasAValue {
     println("\(value)")
 } else {
     assert(false)
 }

co nie jest tak miłe jak

println("\(value!)")

Teraz niejawne opcje pozwalają wyrazić posiadanie opcjonalnego, który, jak się spodziewasz, będzie zawsze miał wartość po rozpakowaniu, we wszystkich możliwych przepływach. Więc idzie o krok dalej, pomagając ci - zmniejszając wymóg pisania! za każdym razem rozpakowywać, a także upewnić się, że środowisko wykonawcze nadal będzie zawierało błędy w przypadku błędnych założeń dotyczących przepływu.


1
@newacct: wartość różna od zera we wszystkich możliwych przepływach przez kod nie jest tym samym, co wartość różna od inicjalizacji nadrzędnej (klasy / struktury) poprzez dezalokację. Konstruktor interfejsów jest klasycznym przykładem (ale istnieje wiele innych opóźnionych wzorców inicjalizacji): jeśli klasa jest kiedykolwiek używana tylko ze stalówki, wówczas zmienne wyjściowe nie zostaną ustawione init(których możesz nawet nie zaimplementować), ale „ re gwarantowane do określenia po awakeFromNib/ viewDidLoad.
rickster

3

Jeśli wiesz na pewno, wartość zwracana z opcjonalnej zamiast z nil, niejawnie nieopakowanej opcji używają do bezpośredniego przechwytywania tych wartości z opcji, a opcje nie mogą.

//Optional string with a value
let optionalString: String? = "This is an optional String"

//Declaration of an Implicitly Unwrapped Optional String
let implicitlyUnwrappedOptionalString: String!

//Declaration of a non Optional String
let nonOptionalString: String

//Here you can catch the value of an optional
implicitlyUnwrappedOptionalString = optionalString

//Here you can't catch the value of an optional and this will cause an error
nonOptionalString = optionalString

To jest różnica między użyciem

let someString : String! i let someString : String


1
To nie odpowiada na pytanie PO. OP wie, czym jest niejawnie nieopakowana opcja.
Franklin Yu,

0

Myślę, że Optionalto zła nazwa tego konstruktu, która dezorientuje wielu początkujących.

Inne języki (na przykład Kotlin i C #) używają tego terminu Nullable, co znacznie ułatwia jego zrozumienie.

Nullableoznacza, że ​​możesz przypisać wartość zerową do zmiennej tego typu. Jeśli tak Nullable<SomeClassType>, możesz przypisać do niego wartości zerowe, jeśli tak jest SomeClassType, nie możesz. Tak właśnie działa Swift.

Po co z nich korzystać? Czasami trzeba mieć wartości zerowe, dlatego. Na przykład, jeśli wiesz, że chcesz mieć pole w klasie, ale nie możesz przypisać go do niczego podczas tworzenia instancji tej klasy, ale później. Nie podam przykładów, ponieważ ludzie już je tutaj podali. Piszę to, żeby dać moje 2 centy.

Przy okazji, sugeruję przyjrzeć się, jak to działa w innych językach, takich jak Kotlin i C #.

Oto link wyjaśniający tę funkcję w Kotlin: https://kotlinlang.org/docs/reference/null-safety.html

Inne języki, takie jak Java i Scala, mają Optionals, ale działają inaczej niż Optionalsw swift, ponieważ typy Java i Scala są domyślnie zerowane.

Podsumowując, uważam, że ta funkcja powinna zostać nazwana Nullablew Swift, a nie Optional...


0

Implicitly Unwrapped Optionaljest cukrem składniowym Optional, ponieważ nie zmusza programisty do rozpakowywania zmiennej. Może być użyty do zmiennej, której nie można zainicjować podczas two-phase initialization processi sugeruje, że nie ma wartości zero. Ta zmienna zachowuje się jako nie-zero, ale w rzeczywistości jest zmienną opcjonalną . Dobrym przykładem jest - Outlety Konstruktora interfejsów

Optional zwykle są lepsze

var nonNil: String = ""
var optional: String?
var implicitlyUnwrappedOptional: String!

func foo() {
    //get a value
    nonNil.count
    optional?.count

    //Danderour - makes a force unwrapping which can throw a runtime error
    implicitlyUnwrappedOptional.count

    //assign to nil
//        nonNil = nil //Compile error - 'nil' cannot be assigned to type 'String'
    optional = nil
    implicitlyUnwrappedOptional = nil
}
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.