Функции опций против броска

Рассмотрим следующую функцию поиска, которую я написал, которая использует опции и необязательную привязку, сообщает сообщение, если ключ не найден в словаре

func lookUp<T:Equatable>(key:T , dictionary:[T:T]) -> T? {
    for i in dictionary.keys {
       if i == key{
           return dictionary[i]
       }
    }
    return nil
}

let dict = ["JO":"Jordan",
   "UAE":"United Arab Emirates",
   "USA":"United States Of America"
]

if let a = lookUp( "JO",dictionary:dict ) {
    print(a) // prints Jordan 
} else {
    print("cant find value")
}

Я переписал следующий код, но на этот раз, используя обработку ошибок, инструкцию охраны, удалив -> T? и записывая перечисление, которое соответствует ErrorType:

enum lookUpErrors : ErrorType {
    case noSuchKeyInDictionary
}

func lookUpThrows<T:Equatable>(key:T , dic:[T:T])throws {
    for i in dic.keys{
        guard i == key else {
            throw lookUpErrors.noSuchKeyInDictionary
        }
        print(dic[i]!)   
    }
}

do {
    try lookUpThrows("UAE" , dic:dict) // prints united arab emirates
}
catch lookUpErrors.noSuchKeyInDictionary{
    print("cant find value")
}

Обе функции работают хорошо, но:

  • эта функция обеспечивает лучшую производительность

  • какая функция "безопаснее"

  • какая функция рекомендуется (на основе плюсов и минусов)

Ответ 1

Производительность

Оба подхода должны иметь сопоставимую производительность. Под капотом они делают очень похожие вещи: возвращают значение с отмеченным флажком и только если флаг показывает, что результат действителен, продолжайте. С optionals этот флаг является перечислением (.None vs .Some), при этом throws этот флаг является неявным, который запускает переход к блоку catch.

Стоит отметить, что ваши две функции не делают одно и то же (один возвращает nil, если ни один ключ не совпадает, другой бросает, если первый ключ не соответствует).

Если производительность имеет решающее значение, вы можете написать это, чтобы работать намного быстрее, устраняя ненужный поиск в нижнем индексе так:

func lookUp<T:Equatable>(key:T , dictionary:[T:T]) -> T?  {
    for (k,v) in dictionary where k == key {
        return v
    }
    return nil
}

и

func lookUpThrows<T:Equatable>(key:T , dictionary:[T:T]) throws -> T  {
    for (k,v) in dic where k == key {
        return v
    }
    throw lookUpErrors.noSuchKeyInDictionary
}

Если вы сравниваете оба из них с допустимыми значениями в замкнутом цикле, они выполняют одинаковые действия. Если вы сравниваете их с недопустимыми значениями, дополнительная версия выполняет примерно в два раза больше скорости, поэтому, по-видимому, на самом деле бросание имеет небольшой накладные расходы. Но, возможно, ничего заметного не произойдет, если вы действительно не вызываете эту функцию в очень узком цикле и не ожидаете много сбоев.

Что безопаснее?

Они оба одинаково безопасны. В любом случае вы не можете вызвать функцию, а затем случайно использовать недопустимый результат. Компилятор заставляет вас либо разворачивать опцию, либо улавливать ошибку.

В обоих случаях вы можете обойти проверки безопасности:

// force-unwrap the optional
let name = lookUp( "JO", dictionary: dict)!
// force-ignore the throw
let name = try! lookUpThrows("JO" , dic:dict)

В самом деле, доходит до того, какой стиль принуждения вызывающего абонента обрабатывать возможный сбой является предпочтительным.

Какая функция рекомендуется?

Хотя это более субъективно, я думаю, что ответы довольно ясны. Вы должны использовать дополнительный, а не метать.

Для руководства по языковому стилю нам нужно только посмотреть на стандартную библиотеку. Словарь уже имеет ключевой поиск (который дублирует эту функцию), и он возвращает необязательный.

Большая причина необязательная - лучший выбор заключается в том, что в этой функции есть только одна вещь, которая может пойти не так. Когда возвращается nil, это происходит только по одной причине, а именно, что ключ отсутствует в словаре. Нет никаких обстоятельств, когда функция должна указывать, какая причина нескольких из них была выбрана, а причина возврата nil должна быть полностью очевидна для вызывающего.

Если, с другой стороны, существует несколько причин, и, возможно, функция должна вернуть объяснение (например, функция, которая выполняет сетевой вызов, который может выйти из строя из-за сбоя сети или из-за поврежденных данных), тогда ошибкой, классифицирующей отказ и, возможно, включая некоторый текст ошибки, будет лучший выбор.

Другая причина необязательно в этом случае - это то, что отказ может даже ожидаться/общий. Ошибки больше для необычных/неожиданных сбоев. Преимущество возврата необязательно - это очень простое использование других дополнительных функций для его обработки - например, необязательная цепочка (lookUp("JO", dic:dict)?.uppercaseString) или дефолт с использованием nil-coalescing (lookUp("JO", dic:dict) ?? "Team not found"). Напротив, try/catch - это немного боль для настройки и использования, если только вызывающий не хочет "исключительной" обработки ошибок, т.е. Собирается делать кучу вещей, некоторые из которых могут терпеть неудачу, но хочет собрать этот сбой обращаясь вниз внизу.

Ответ 2

@AirspeedVelocity уже имеет отличный ответ, но я думаю, что стоит немного поработать над тем, почему использовать опционы и ошибки.

В принципе есть четыре способа, чтобы что-то пошло не так:

  • Простая ошибка: она терпит неудачу только одним способом, поэтому вам не нужно заботиться о том, почему что-то пошло не так. Проблема может возникнуть либо из логики программиста, либо из пользовательских данных, поэтому вам нужно иметь возможность обрабатывать ее во время выполнения и разрабатывать вокруг нее при кодировании.

    Это относится к таким вещам, как инициализация Int из String (либо строка обрабатывается как целое, либо нет), либо поиск в стиле словаря (либо есть значение для ключа, либо нет), Варианты работы очень хорошо подходят для этого в Swift.

  • Логическая ошибка: это та ошибка, которая (теоретически) возникает только во время разработки, в результате Doing It Wrong - например, индексирование за пределами массива.

    В ObjC, NSException охватывает такие случаи. В Swift у нас есть функции типа fatalError. Я бы предположил, что часть того, почему NSException не всплывает в Swift, заключается в том, что, как только ваша программа встретит логическую ошибку, небезопасно предполагать что-либо о ее дальнейшей работе. Ошибки логики должны быть либо пойманы во время разработки, либо вызвать (красиво отлаживаемый) сбой, а не позволить программе продолжить состояние undefined (и, следовательно, небезопасное).

  • Универсальная ошибка: существует множество способов отказа, но они не очень связаны с логикой программиста или действием пользователя. У вас может быть нехватка памяти для выделения, получения прерывания на низком уровне или (дождитесь ее...), но это может произойти практически с любым, что вы делаете, а не из-за какой-либо конкретной вещи, которую вы делаете.

    Вы видите, что универсальные ошибки появляются в качестве исключений на некоторых других языках, но это означает, что вы должны кодировать возможность любого любого вызова, который вы делаете, чтобы иметь возможность провалиться. И в этот момент вы пишете больше обработки ошибок, чем фактический код.

  • Возвращаемая ошибка: это происходит, когда есть много способов пойти не так, но не такими способами, которые препятствуют дальнейшей работе, и то, что программа делает при столкновении с ошибкой, может измениться в зависимости от того, какая ошибка, Файловые системы и сетевое взаимодействие являются распространенными примерами здесь: если вы не можете загрузить файл, возможно, это связано с тем, что пользователь неправильно получил имя (так что вы должны сообщить об этом пользователю) или потому, что Wi-Fi мгновенно упал и скоро вернется (так вы можете отказаться от предупреждения и просто повторить попытку).

    В Cocoa исторически это параметры NSError. Быстрая обработка ошибок делает этот шаблон частью языка.

Итак, когда вы пишете новый API (для себя или кого-то другого) в Swift или используете новые аннотации ObjC, чтобы упростить использование существующего API от Swift, подумайте о том, какие ошибки вы имеете в виду с.

  • Есть ли только один явный способ выхода из строя, который не является результатом неправильного использования API? Используйте необязательный тип возврата.

  • Может произойти сбой только в том случае, если клиент не выполнит ваш контракт по API - скажем, если вы пишете контейнерный класс с индексом и count или требуете, чтобы определенная последовательность вызовов была сделал? Не обременяйте каждый бит кода, который использует ваш API с обработкой ошибок или необязательной разверткой - просто fatalError или assert (или throw NSException, если ваш Swift API является фронтом кода ObjC) и документируйте, что правильно для людей, чтобы использовать ваш API.

  • Хорошо, поэтому ваш метод ObjC init возвращает nil iff [super init] возвращает nil. Итак, следует ли пометить ваш инициализатор как неудачный для Swift или добавить ошибку-парпаметр? Подумайте, когда это произойдет - если -[NSObject init] возвращает нуль, это связано с тем, что вы связали его с вызовом alloc, который возвратил нуль. Если alloc терпит неудачу, это уже конец End для вашего процесса, поэтому не стоит обрабатывать этот случай.

  • Есть ли у вас несколько случаев сбоев, некоторые или все из которых могут быть полезны для пользователя? Или что клиент, вызывающий ваш API, может игнорировать некоторые, но не все? Напишите функцию Swift, которая throws и соответствующий набор значений ErrorType, или метод ObjC, который возвращает параметр NSError out.

Ответ 3

Если вы используете Swift 2.0, вы можете использовать обе версии. Вторая версия использует try/catch (представлена ​​с 2.0) и, таким образом, не имеет обратной совместимости, что может быть недостатком для рассмотрения.

Если есть разница в производительности, это будет пренебрежимо.

Мой личный фаворит - первый, так как он прямолинейный и хорошо читаемый. Если бы мне пришлось выполнять вторую версию, я бы спросил, почему автор взял подход try/catch для такого простого случая. Поэтому я предпочел бы смутить...

Если у вас было много сложных условий со многими точками выхода (броски), я бы пошел на второй. Но, как я уже сказал, это не так.