Навіщо створювати "Неявно розгорнуті опціони", оскільки це означає, що ви знаєте, що існує цінність


493

Чому б ви створили "Неявно розкручений необов'язковий" проти створення просто звичайної змінної чи константи? Якщо ви знаєте, що його можна успішно розкрутити, то навіщо взагалі створювати необов’язковий? Наприклад, чому це:

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

буде корисніше, ніж:

let someString: String = "this is the string"

Якщо "необов'язково вказує на те, що постійній чи змінній дозволено мати значення" немає значення ", але" іноді з структури програми видно, що необов'язково завжди буде мати значення після того, як це значення вперше встановлено ", в чому полягає що робить його необов'язковим в першу чергу? Якщо ви знаєте, що необов'язковий завжди матиме значення, чи не робить це необов’язковим?

Відповіді:


127

Розглянемо випадок об’єкта, який може мати властивості нуля під час його створення та конфігурування, але незмінний і ненульовий згодом (NSImage часто трактується таким чином, хоча в його випадку іноді все ще корисно мутувати). Неявно розгорнуті опціони добре очистять його код із відносно низькими втратами безпеки (доки ця гарантія зберігається, це буде безпечно).

(Редагувати) Для того, щоб бути зрозумілим: звичайні необов'язкові варіанти майже завжди бажані.


459

Перш ніж я можу описати випадки використання необов’язково необов’язаних необов’язкових, ви вже повинні зрозуміти, що таке опціональні та нечітко розгорнуті необов'язкові додатки у Swift. Якщо ви цього не зробите, рекомендую спершу прочитати мою статтю за факультативом

Коли використовувати необов’язково розмотану необов’язково

Є дві основні причини, з яких можна створити неявно необов’язану необов’язковість. Все це стосується визначення змінної, до якої ніколи не буде доступно, nilоскільки в іншому випадку компілятор Swift завжди змусить вас явно відкрутити необов'язковий.

1. Константа, яку неможливо визначити під час ініціалізації

Кожна константа члена повинна мати значення до моменту завершення ініціалізації. Іноді константа не може бути ініціалізована з її правильним значенням під час ініціалізації, але все одно можна гарантувати, що вона матиме значення, перш ніж отримати доступ.

Використання необов'язкової змінної вирішує цю проблему, оскільки необов'язковий автоматично ініціалізується, nilі значення, яке воно в кінцевому підсумку міститиме, все ще буде незмінним. Однак може бути болем постійно розгортати змінну, яку ви точно знаєте, це не нуль. Неявно розгорнуті необов’язкові домагаються тих же переваг, що й Факультативні, з додатковою перевагою, яку не потрібно явно розгортати скрізь.

Чудовим прикладом цього є те, коли змінна члена не може бути ініціалізована в підкласі UIView, поки подання не завантажується:

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

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

Тут ви не можете обчислити початкову ширину кнопки до тих пір, поки подання не завантажиться, але ви знаєте, що awakeFromNibбуде викликано раніше, ніж будь-який інший метод перегляду (крім ініціалізації). Замість того, щоб змусити ціль явно розкручувати значення по всьому класу, ви можете оголосити його як необов’язкове розгорнуте необов’язкове.

2. Коли ваша програма не може відновитися від змінної істоти nil

Це має бути вкрай рідкісним, але якщо ваша програма не може продовжувати працювати, якщо доступ до змінної є nil, то марно витрачати час на тестування її nil. Як правило, якщо у вас є умова, яка повинна бути абсолютно вірною, щоб ваш додаток продовжував працювати, ви б використовували assert. Неявно розкручений необов’язковий має утвердження для нуля, вбудованого прямо в нього. Навіть тоді часто буває добре розгортати необов'язковий і використовувати більш описовий аргумент, якщо він дорівнює нулю.

Коли не використовувати необов’язково розмотану необов’язково

1. Ліниво обчислені змінні учасника

Іноді у вас є змінна елемента, яка ніколи не повинна бути нульовою, але вона не може бути встановлена ​​на правильне значення під час ініціалізації. Одним із варіантів є використання непримітно розгорнутого факультативу, але кращим способом є використання ледачої змінної:

class FileSystemItem {
}

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

Тепер змінна члена contentsне ініціалізується до першого доступу до неї. Це дає класу можливість перейти у правильний стан перед обчисленням початкового значення.

Примітка. Це може здатися суперечливим №1 зверху. Однак слід зробити важливе розмежування. buttonOriginalWidthВище повинні бути встановлено у viewDidLoad , щоб запобігти будь-якому мінливі кнопки ширини до властивості доступу.

2. Скрізь скрізь

Здебільшого слід уникати неочікуваних необов’язаних додатків, оскільки при неправильному використанні весь ваш додаток вийде з ладу під час доступу до нього nil. Якщо ви ніколи не знаєте, чи може змінна може бути нульовою, завжди використовуйте звичайну необов'язково. Розгортання змінної, яка ніколи, nilзвичайно, не дуже шкодить.


4
Цю відповідь слід оновити для бета-версії 5. Ви більше не можете її використовувати if someOptional.
Дід Мороз

2
@SantaClaus hasValueвизначено право на Факультативно. Я віддаю перевагу семантиці того, hasValueщо є != nil. Я вважаю, що це набагато зрозуміліше для нових програмістів, які не використовували nilінших мов. hasValueнабагато логічніше, ніж nil.
drewag

2
Схоже, hasValueйого витягнули з бета-версії 6. Еш повернув її назад, хоча ... github.com/AshFurrow/hasValue
Кріс Вагнер

1
@newacct Що стосується типу повернення ініціалізаторів Objc, то це скоріше неявна неявна необов’язковість. Поведінка, яку ви описали для використання "необов'язкового", саме те, що зробить неявно необов’язаний факультатив (не відмовить до доступу). Що стосується відмови програми раніше, примусово розгортаючись, я погоджуюся, що використання необов’язкового є кращим, але це не завжди можливо.
drewag

1
@confile ні. Незважаючи ні на що, він з'явиться в Objective-C у вигляді вказівника (якщо він був необов’язковим, неявно розгорнутим або необов'язковим).
drewag

56

Неявно розгорнуті необов’язкові елементи корисні для подання властивості як необов'язкової, коли вона дійсно має бути необов’язковою під обкладинкою. Це часто необхідно для "зав'язування вузла" між двома спорідненими об'єктами, які потребують посилання на інший. Це має сенс, коли жодна посилання насправді не є обов'язковою, але одна з них повинна бути нульовою під час ініціалізації пари.

Наприклад:

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

У будь-якому Bекземплярі завжди має бути дійсна myBuddyAпосилання, тому ми не хочемо змушувати користувача ставитися до цього як до необов'язкового, але нам потрібно, щоб він був необов’язковим, щоб ми могли побудувати а, Bперш ніж ми будемо Aзвертатися до нього.

ЗАРАЗ! Цей вид взаємної довідкової вимоги часто є вказівкою на тісну сполуку та поганий дизайн. Якщо ви опираєтесь на неявно розгорнуті опціони, вам, мабуть, слід подумати про рефакторинг для усунення перехресних залежностей.


7
Я думаю, що одна з причин, по якій вони створили цю мовну особливість,@IBOutlet
Jiaaro

11
+1 для застереження "ТАКЕ" Це може бути неправдою весь час, але на це, безумовно, варто дивитися.
JMD

4
У вас все ще є сильний еталонний цикл між A і B. Неодноразово розгорнуті факультативи НЕ створюють слабких посилань. Ви все ще повинні оголосити myByddyA або myBuddyB слабкими (можливо, myBuddyA)
drewag

6
Щоб бути ще більш зрозумілим, чому ця відповідь є помилковою та небезпечно оманливою: Неявно розгорнуті опціони абсолютно не мають нічого спільного з керуванням пам’яттю та не перешкоджають утриманню циклів. Однак необов'язково розмотані необов’язкові все ще корисні в обставинах, описаних для встановлення двосторонньої посилання. Тож просто додаючи weakдекларацію та видаляючи "без створення міцного циклу збереження"
drewag

1
@drewag: Ти маєш рацію - я відредагував відповідь, щоб видалити цикл збереження. Я мав намір зробити зворотну референцію слабкою, але, мабуть, це прослідковувало мою думку.
n8gray

37

Неявно розгорнуті додаткові опціони є прагматичним компромісом, щоб зробити роботу в гібридному середовищі, яке має взаємодіяти з існуючими рамками какао та їх конвенціями, приємнішими, а також дозволяючи поетапну міграцію в безпечнішу програмувальну парадигму - без нульових покажчиків - застосовувати компілятор Swift.

Книга "Швидка", в розділі "Основи ", розділ " Непомітно розгорнуті факультативи", говорить:

Неявно розгорнуті необов'язкові елементи корисні, коли підтверджено, що значення необов'язкового існує відразу після першого визначення факультативу, і, безумовно, можна вважати, що воно існує в кожному пункті згодом. Основне використання неявно розгорнутих необов’язків у Swift відбувається під час ініціалізації класу, як описано у Невідомих посиланнях та Неявно розгорнуті необов’язкові властивості .

Ви можете вважати неявно розгорнуту необов’язковість як дозвіл на автоматичне розгортання необов'язкового кожного разу, коли він використовується. Замість того, щоб ставити знак оклику після імені необов'язкового кожного разу, коли ви його використовуєте, ви кладете знак оклику після типу необов’язкового, коли ви його оголошуєте.

Це зводиться до того, використовувати випадки , коли не- nil-ness властивостей встановлюються за допомогою використання конвенції, і не може бути насильницьким компілятором під час ініціалізації класу. Наприклад, UIViewControllerвластивості, ініціалізовані з NIB або дошками розкадрів, де ініціалізація розбита на окремі фази, але після viewDidLoad()ви можете припустити, що властивості взагалі існують. В іншому випадку, щоб задовольнити компілятор, вам довелося використовувати примусове розгортання , необов'язкове прив'язування або необов'язкове ланцюжок лише для затемнення основної мети коду.

Вгорі частина книги Swift стосується також глави автоматичного підрахунку посилань :

Однак існує третій сценарій, коли обидва властивості завжди повинні мати значення, і жодна властивість не повинна бути nilколись ініціалізацією завершена. У цьому сценарії корисно поєднувати невідоме властивість одного класу з неявно розгорнутою необов'язковою властивістю для іншого класу.

Це дозволяє отримати доступ до обох властивостей безпосередньо (без необов'язкового розгортання), коли ініціалізація завершена, при цьому все ж уникаючи еталонного циклу.

Це зводиться до химерності, що це не мова, зібрана зі сміттям, тому переривання циклів збереження відбувається на вас як на програміста, і неявно розгорнуті додаткові опції є інструментом для приховування цієї примхи.

Це стосується "Коли використовувати неявно розгорнуті необов'язкові додатки у коді?" питання. Як розробник додатків, ви в основному стикаєтесь з ними в підписах методів бібліотек, написаних в Objective-C, які не мають можливості виражати необов'язкові типи.

З розділу Використання Swift з какао та Objective-C, розділ Робота з нулем :

Оскільки Objective-C не дає жодних гарантій того, що об'єкт не відповідає нулю, Swift робить усі класи типів аргументів та типи повернення необов’язковими в імпортованих API-адресах Objective-C. Перш ніж використовувати об’єкт Objective-C, слід перевірити, чи не відсутній він.

У деяких випадках ви можете бути абсолютно впевнені, що метод або властивість Objective-C ніколи не повертає nilпосилання на об'єкт. Щоб зробити об'єкти в цьому спеціальному сценарії зручніше працювати, Swift імпортує типи об'єктів як неявно розгорнуті необов'язкові . Неявно розгорнуті необов'язкові типи включають усі функції безпеки необов'язкових типів. Крім того, ви можете отримати доступ до значення безпосередньо, не перевіряючиnilабо розгортаючи його самостійно. Коли ви отримуєте доступ до значення цього виду необов’язкового типу, не безпечно розгортаючи його спочатку, неявно розгорнута опціональна перевіряє, чи немає цього значення. Якщо значення відсутнє, виникає помилка виконання. Як результат, ви завжди повинні перевіряти та розгортати неявно розгорнуту необов’язково, якщо ви не впевнені, що значення не може бути відсутнім.

... а далі тут лежало дракони


Дякую за цю грунтовну відповідь. Чи можете ви придумати короткий контрольний список про те, коли використовувати непримітно розгорнуті необов'язкові параметри та коли стандартної змінної вистачить?
Hairgami_Master

@Hairgami_Master Я додав власну відповідь зі списком і конкретними прикладами
drewag

18

Прості приклади з одним рядком (або з декількох рядків) не дуже добре висвітлюють поведінку необов'язкових - так, якщо ви оголосили змінну і надали їй значення відразу, необов'язково немає жодного сенсу.

Найкращий випадок, який я бачив до цього часу, - це налаштування, яке відбувається після ініціалізації об'єкта з подальшим використанням, яке "гарантовано" слідкує за цим налаштуванням, наприклад, у контролері перегляду:

class MyViewController: UIViewController {

    var screenSize: CGSize?

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

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

Ми знаємо, що printSizeбуде викликано після завантаження представлення - це метод дії, підключений до елемента управління всередині цього перегляду, і ми переконалися, що не називати його інакше. Таким чином, ми можемо зберегти собі кілька необов'язкових перевірок / зв’язування з !. Swift не може визнати цю гарантію (принаймні, поки Apple не вирішить проблему зупинки), тож ви скажете компілятору, що вона існує.

Однак це певною мірою порушує безпеку типу. У будь-якому місці, яке ви маєте неявно розгорнуте, - це місце, де ваше додаток може збійтися, якщо "гарантія" не завжди виконується, тому це функція, яка використовуватиметься економно. Крім того, використання !всього часу робить це, як ви кричите, і нікому це не подобається.


Чому б не просто ініціалізувати screenSize до CGSize (висота: 0, ширина: 0) і не врятувати проблему з необхідністю кричати змінну кожного разу, коли ви отримуєте доступ до неї?
Мартін Гордон

Розмір, можливо, не був найкращим прикладом, оскільки CGSizeZeroможе бути хорошим вартовим значенням у реальному використанні. Але що робити, якщо у вас завантажений розмір, який насправді може дорівнювати нулю? Тоді використання CGSizeZeroв якості дозорного не допоможе вам розрізнити значення, яке не встановлено, і встановлене на нуль. Більше того, це однаково добре стосується й інших типів, завантажених з нибу (або деінде після init): рядків, посилань на
підзагляди

2
Частина пункту Факультативу у функціональних мовах не має дозорних значень. У вас є цінність, або її немає. У вас не повинно бути випадку, коли у вас є значення, яке вказує на відсутнє значення.
Уейн Таннер

4
Я думаю, ви неправильно зрозуміли питання ОП. ОП не запитує про загальний випадок факультативів, скоріше, про потребу / використання неявно розгорнутих факультативів (тобто ні let foo? = 42, швидше let foo! = 42). Це не вирішує цього питання. (Це може бути відповідна відповідь про необов’язкові, зауважте, але не про неявно розгорнуті необов'язкові параметри, які є різними / спорідненими тваринами.)
JMD

15

Apple дає чудовий приклад мови швидкого програмування Swift -> Автоматичний підрахунок посилань -> Розв’язання сильних еталонних циклів між екземплярами класу -> Невідомі посилання та Неявно розгорнуті додаткові властивості

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

Ініціалізатор для Cityвикликається з ініціалізатора для Country. Однак ініціалізатор for Countryне може перейти selfдо Cityініціалізатора, поки новий Countryекземпляр не буде повністю ініціалізований, як описано у двофазній ініціалізації .

Щоб впоратися з цією вимогою, ви декларуєте capitalCityвластивість Countryяк неявно розгорнуте необов'язкове властивість.


Підручник, згаданий у цій відповіді, знаходиться тут .
Франклін Ю

3

Обґрунтування неявних необов’язків простіше пояснити, спершу розглядаючи обґрунтування примусового розгортання.

Примусове розгортання необов’язкової (неявна чи ні) з використанням! Оператор, означає, що ви впевнені, що у вашому коді немає помилок, і необов'язково вже має значення, де він розгортається. Без того! оператора, ви, ймовірно, просто стверджуєте з додатковим прив'язкою:

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

що не так приємно, як

println("\(value!)")

Тепер неявні опціони дозволяють висловити необов'язковий вибір, який, як ви очікуєте, завжди матиме значення при розгортанні у всіх можливих потоках. Тож це просто ще крок далі, щоб допомогти вам - послабивши вимогу написання! щоразу знімати та гарантувати, що час виконання буде все-таки помилятися, якщо ваші припущення щодо потоку помилкові.


1
@newacct: non-nil у всіх можливих потоках через ваш код не є тим самим, як non-nil від ініціалізації батьківського (класу / структури) через deallocation. Класичний приклад інтерфейсу Builder (але є багато інших шаблонів затримки ініціалізації): якщо клас використовується будь-коли лише з нибу, змінні розетки не встановлюватимуться init(що ви можете навіть не реалізувати), але вони ' повторно гарантовано буде встановлено після того, як awakeFromNib/ viewDidLoad.
рикстер

3

Якщо ви точно знаєте, значення , що повертається опціональний замість nil, імпліцитно розпакованих OPTIONALS використовувати безпосередньо зловити ці значення з додатковими опціями і не може , не є додатковими опціями.

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

Отже, це різниця між використанням

let someString : String! і let someString : String


1
Це не відповідає на питання ОП. ОП знає, що таке недбало розгорнута необов’язковість.
Франклін Ю

0

Я думаю Optional, що це погана назва цього конструкту, який бентежить багато початківців.

Інші мови (наприклад, Котлін та С #) використовують цей термін Nullable, і це набагато простіше зрозуміти це.

Nullableозначає, що ви можете призначити нульове значення змінній цього типу. Отже, якщо це так Nullable<SomeClassType>, ви можете призначити йому нулі, якщо це просто SomeClassType, ви не можете. Ось так працює Swift.

Навіщо їх використовувати? Ну, іноді потрібно мати нулі, ось чому. Наприклад, коли ви знаєте, що хочете мати поле в класі, але ви не можете привласнити його ні до чого, коли створюєте екземпляр цього класу, але згодом. Я не буду наводити приклади, тому що люди вже їх подали. Я просто пишу це, щоб дати свої 2 копійки.

До речі, я пропоную вам подивитися, як це працює іншими мовами, як-от Kotlin та C #.

Ось посилання, що пояснює цю функцію в Котліні: https://kotlinlang.org/docs/reference/null-safety.html

Інші мови, як-от Java та Scala, мають Optionals, але вони працюють інакше, ніж Optionals у Swift, тому що типи Java та Scala за замовчуванням є нульовими.

Загалом, я думаю, що ця функція повинна була бути названа Nullableв Swift, а не Optional...


0

Implicitly Unwrapped Optionalє синтаксичним цукром, Optionalщо не змушує програміста розгортати змінну. Він може бути використаний для змінної, яка не може бути ініціалізована під час two-phase initialization processі передбачає non-nil. Ця змінна поводиться як не-нульова, але насправді є необов'язковою змінною. Хороший приклад - торговельні точки Interface Builder

Optional зазвичай є кращими

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
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.