Pull to refresh

Swift 5.1 — что нового?

Reading time6 min
Views11K
Original author: Paul Hudson


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

Ключевая особенность Swift 5.1 — модульная стабильность (module stability), которая позволяет нам использовать сторонние библиотеки, не волнуясь о том, при помощи какой версии компилятора Swift они были созданы. Похоже на ABI stability, которую мы получили в Swift 5.0, но есть небольшая разница: ABI stability разрешает различия в версиях Swift на этапе выполнения, а module stability — на этапе компиляции.

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

Универсальный Self


SE-0068 расширяет использование Self, так что он ссылается на содержащий его тип внутри классов, структур и перечислений. Обычно это полезно для динамических типов, когда необходимо определить точный тип чего-либо во время выполнения.

В качестве примера рассмотрим следующий код:

class NetworkManager {
    class var maximumActiveRequests: Int {
        return 4
    }

    func printDebugData() {
        print("Maximum network requests: \(NetworkManager.maximumActiveRequests).")
    }
}

Здесь мы определяем статическое свойство maximumActiveRequests внутри класса NetworkManager и добавляем метод printDebugData() чтобы распечатать это свойство. Здесь все отлично, но только до тех пор, пока мы не решим наследоваться от NetworkManager:

class ThrottledNetworkManager: NetworkManager {
    override class var maximumActiveRequests: Int {
        return 1
    }
}

В этом наследнике мы меняем свойство maximumActiveRequests, так что теперь оно становится равным единице, но если мы вызовем printDebugData(), то он выведет значение из родительского класса:

let manager = ThrottledNetworkManager()
manager.printDebugData()

Здесь мы должны получить 1 вместо 4, и вот тут на помощь приходит SE-0068: мы можем использовать Self (с прописной 'S'), чтобы сослаться на текущий тип. Так что теперь мы можем переписать метод printDebugData() родительского класса вот так:

class ImprovedNetworkManager {
    class var maximumActiveRequests: Int {
        return 4
    }

    func printDebugData() {
        print("Maximum network requests: \(Self.maximumActiveRequests).")
    }
}

То есть, Self работает таким же образом, как он работал в протоколах в ранних версиях Swift.

Предупреждения в случае двусмысленности варианта none


Optionals в Swift реализованы в качестве перечисления с двумя вариантами: some и none. Это может привести к путанице, если мы создадим своё собственное перечисление, у которого есть вариант none, и обернём его в optional. Например:

enum BorderStyle {
    case none
    case solid(thickness: Int)
}

При использовании non-optional всё чисто:

let border1: BorderStyle = .none
print(border1)

Это выведет “none”. Но если мы используем optional для этого перечисления, то мы столкнёмся с проблемой:

let border2: BorderStyle? = .none
print(border2)

Здесь будет напечатано nil, так как Swift полагает, что .none означает, что optional пуста, хотя на самом деле это optional со значением BorderStyle.none.
В Swift 5.1 в случае такой двусмысленности будет выведено предупреждение:
“Assuming you mean 'Optional.none'; did you mean 'BorderStyle.none' instead?”
Таким образом разработчик будет проинформирован о том, что с его кодом может быть не всё гладко.

Сопоставление optional и non-optional перечислений


Swift достаточно сообразителен чтобы разобраться в конструкции switch/case при сочетании optional/non-optional текстовых и целых значений, но только не в случае перечислений.

Теперь в Swift 5.1 мы можем использовать switch/case для поиска соответствия перечислений-optional и non-optional варианта:

enum BuildStatus {
    case starting
    case inProgress
    case complete
}

let status: BuildStatus? = .inProgress

switch status {
case .inProgress:
    print("Build is starting…")
case .complete:
    print("Build is complete!")
default:
    print("Some other build status")
}

Swift в состоянии сопоставить optional перечисление с non-optional вариантами, и здесь будет выведено “Build is starting…”

Сравнение упорядоченных коллекций


SE-0240 представило возможность вычислять различия между упорядоченными коллекциями, а также применять к коллекциям полученный результат сравнения. Это может представлять интерес для разработчиков, у которых сложные коллекции в tableview, и им нужно добавлять или удалять много элементов, используя анимацию.

Основной принцип простой — Swift 5.1 предоставляет новый метод difference(from:), который определяет отличия между двумя упорядоченными коллекциями — какие элементы добавить, а какие — удалить. Это применимо к любым упорядоченным коллекциям, которые содержат элементы, отвечающий протоколу Equatable.

Чтобы продемонстрировать это, мы создадим два массива значений, вычислим отличия одного от другого, а затем пройдемся по списку отличий и применим их, чтобы сделать две коллекции одинаковыми.

Замечание: так как Swift теперь распространяется в составе операционных систем Apple, новые средства языка должны использоваться с проверкой #available, чтобы быть уверенным, что код запускается на ОС, которая поддерживает требуемый функционал. Для функционала, запускаемого на неизвестных, неанонсированных ОС, которые могут быть выпущены в будущем, используется специальный номер версии “9999”, который означает: «Мы еще не знаем правильный номер версии».

var scores1 = [100, 91, 95, 98, 100]
let scores2 = [100, 98, 95, 91, 100]

if #available(iOS 9999, *) {
    let diff = scores2.difference(from: scores1)

    for change in diff {
        switch change {
        case .remove(let offset, _, _):
            scores1.remove(at: offset)
        case .insert(let offset, let element, _):
            scores1.insert(element, at: offset)
        }
    }

    print(scores1)
}

Для более продвинутой анимации мы можем использовать третий параметр в полученном списке различий: associatedWith. Таким образом, вместо .insert(let offset, let element, _) мы можем написать .insert(let offset, let element, let associatedWith). Это даёт нам возможность одновременно отслеживать пары изменений: передвижение элемента в коллекции на две позиции вниз это удаление элемента с последующим добавлением, а associatedWith «связывает» эти два изменения вместе и позволяет вам считать это передвижением.

Вместо применения различий вручную, одно за одним, вы можете применить их одним махом, используя новый метод applying():

if #available(iOS 9999, *) {
    let diff = scores2.difference(from: scores1)
    let result = scores1.applying(diff) ?? []
}

Создание неинициализированных массивов


SE-0245 представило новый инициалайзер для массивов, который не заполняет его дефолтными значениями. Это было доступно и раньше как private API, что означало, что Xcode не подсказывал это в code completion, но вы могли это использовать если вам это было нужно и вы понимали риск того, что этого функционала может не быть в дальнейшем.

Для использования инициалайзера задайте размер массива, затем передайте замыкание, которое заполнит массив значениями. Замыкание получит небезопасный указатель на mutable буфер, а также второй параметр, в котором вы обозначите, сколько значений вы на самом деле используете.

Например мы можем создать массив из 10 случайных целых чисел вот так:

let randomNumbers = Array<Int>(_unsafeUninitializedCapacity: 10) { buffer, initializedCount in
    for x in 0..<10 {
        buffer[x] = Int.random(in: 0...10)
    }
    
    initializedCount = 10
}

Тут есть несколько правил:

  1. вам не нужно использовать весь объём, который вы запросили, но вы не можете превысить его. То есть, если вы задали размер массива 10, то вы можете установить initializedCount в пределах от 0 до 10, но не 11.
  2. если вы не инициализировали используемые элементы в массиве, например, вы установили initializedCount равным 5, но не предоставили реальных значений элементам с 0 по 4, то они вероятнее всего получат рандомные значения. Как вы понимаете, это плохой вариант.
  3. Если вы не установите initializedCount, то он будет равным 0 и все данные, которые вы назначили, будут потеряны.

Да, мы вполне могли бы переписать код с использованием map():

let randomNumbers2 = (0...9).map { _ in Int.random(in: 0...10) }

Это очевидно более читабельно, но не так эффективно: мы создаём диапазон, затем новый пустой массив, назначаем ему размер, и «пробегаемся» по всему диапазону, применяя замыкание к каждому элементу.

Заключение


Swift 5.1 все еще находится в стадии разработки, и хотя окончательное ветвление для самого Swift прошло, все еще видны изменения от некоторых других связанных проектов.

Итак, самое важное изменение — это module stability, и известно, что команда разработчиков напряженно над этим работает. Они не называют точной даты выхода, хотя они говорили, что у Swift 5.1 значительно меньший срок разработки по сравнению с Swift 5.0, который потребовал незаурядной концентрации сил и внимания. Можно предположить выход к WWDC19, но очевидно, что это не тот случай, когда нужно спешить к определённой дате.

Ещё одно замечание, которое заслуживает внимания. Два изменения в этом списке («Предупреждения в случае двусмысленности варианта none» и «Сопоставление optional и non-optional перечислений») стали не результатом эволюции Swift, а были распознаны как баги и скорректированы.
Tags:
Hubs:
+15
Comments9

Articles

Change theme settings