Что нового в Swift 4.2

Оглавление

  1. Bool.toggle
  2. Алгоритмы для Sequence и Collection
  3. Перечисление всех cases enum
  4. Случайные числа
  5. Hashable redesign
  6. Условное соответствие (протоколам)
  7. Dynamic member lookup
  8. #error and #warning
  9. MemoryLayout.offset(of:)
  10. @inlinable
  11. Immutable withUnsafePointer

Требования

Этому playground требуется Xcode 10 или свежий слепок Swift 4.2. Скачать Xcode 10 beta можно здесь а слепок Swift 4.2 здесь.

Обратите внимание

Этот playground фокусируется на новых, ориентированных на программистов особенностях, которые могут быть легко продемонстрированы в формате playground. Swift 4.2 содержит гораздо больше изменений, чем перечислено здесь. Под капотом Swift 4.2 множество усовершенствований, в том числе новые возможности Swift Package Manager.

Взгляните на полный список изменений (changelog), а также на полный список реализованных в Swift 4.2 предложений (который содержит ряд предложений реализованных, но не упомянутых в changelog).

Bool.toggle

SE-0199 («Adding toggle to Bool») добавляет изменяющий (mutating) метод toggle к Bool.

Это особенно полезно, когда требуется «переключать» значение boolean глубоко внутри вложенной структуры данных, поскольку не требуется повторять одно и то же выражение на обеих сторонах оператора присваивания.

struct Layer {
    var isHidden = false
}

struct View {
    var layer = Layer()
}

var view = View()

// Раньше:
view.layer.isHidden = !view.layer.isHidden
view.layer.isHidden

// Теперь:
view.layer.isHidden.toggle()
view.layer.isHidden

Алгоритмы для Sequence и Collection

allSatisfy

SE-0207 («Add an allSatisfy algorithm to Sequence») добавляет алгоритм allSatisfy к последовательности Sequence. allSatisfy возвращает true только тогда, когда ВСЕ элементы последовательности удовлетворяют предикату. Эта фукция часто называется просто all в других функциональных языках программирования.

allSatisfy прекрасно дополняет contains(where:), которая позволяет выяснить, удовлетворяет ли хотя бы один элемент предикату.

let digits = 0...9

let areAllSmallerThanTen = digits.allSatisfy { $0 < 10 }
areAllSmallerThanTen

let areAllEven = digits.allSatisfy { $0 % 2 == 0 }
areAllEven

last(where:), lastIndex(where:) и lastIndex(of:)

SE-0204 («Add last(where:) and lastIndex(where:) Methods») добавляет метод last(where:) к последовательности Sequence и добавляет методы lastIndex(where:) и lastIndex(of:) к коллекции Collection.

let lastEvenDigit = digits.last { $0 % 2 == 0 }
lastEvenDigit

let text = "Пойдем на пляж"

let lastWordBreak = text.lastIndex(where: { $0 == " " })
let lastWord = lastWordBreak.map { text[text.index(after: $0)...] }
lastWord

text.lastIndex(of: " ") == lastWordBreak

Переименование index(of:) и index(where:) в firstIndex(of:) и firstIndex(where:)

Для единообразия SE-0204 также переименовывает index(of:) и index(where:) в firstIndex(of:) и firstIndex(where:).

let firstWordBreak = text.firstIndex(where: { $0 == " " })
let firstWord = firstWordBreak.map { text[..<$0] }
firstWord

 

Перечисление всех cases enum

SE-0194 («Derived Collection of Enum Cases»): Компилятор может автоматически генерировать свойство allCases для перечислений enums, обеспечивая вас постоянно актуальным списком всех enum cases. Всё, что для этого нужно — это чтобы ваш enum соответствовал новому протоколу CaseIterable.

enum Terrain: CaseIterable {
    case water
    case forest
    case desert
    case road
}

Terrain.allCases
Terrain.allCases.count

Обратите внимание, что автоматический синтез работает только для enum без связанных значений (associated values) — потому, что связанные значения позволяют перечислению enum потенциально иметь бесконечное число возможных значений.

При желании, вы можете вручную реализовать протокол, если список возможных значений конечный. В качестве примера, вот условное соответствие протоколу для завёрнутых в Optionals типов, при том, что сами эти типы соответствуют CaseIterable:

extension Optional: CaseIterable where Wrapped: CaseIterable {
    public typealias AllCases = [Wrapped?]
    public static var allCases: AllCases {
        return Wrapped.allCases.map { $0 } + [nil]
    }
}

// Обратите внимание: это не цепочка optional (optional chaining)!
// Мы обращаемся к переменной типа Optional.
Terrain?.allCases
Terrain?.allCases.count

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

Случайные числа

Работа со случайными числами была слегка болезненна в Swift, поскольку вам (a) приходилось напрямую вызывать C APIs и (b) не было хорошего кросс-платформенного API для случайных чисел.

SE-0202 («Random Unification») добаляет генерирование случайных чисел в стндартную библиотеку.

Генерирование случайных чисел

Все числовые типы теперь имеют метод random(in:), который возвращает случайное число в указанном диапазоне (по-умолчанию используется равномерное распределение):

Int.random(in: 1...1000)
UInt8.random(in: .min ... .max)
Double.random(in: 0..<1)

Этот API аккуратно оберегает вас от распростаненных ошибок при генерировании случайных чисел, таких как смещение по модулю.

Bool.random это тоже вещь:

func coinToss(count tossCount: Int) -> (heads: Int, tails: Int) {
    var tally = (heads: 0, tails: 0)
    for _ in 0..<tossCount {
        let isHeads = Bool.random()
        if isHeads {
            tally.heads += 1
        } else {
            tally.tails += 1
        }
    }
    return tally
}

let (heads, tails) = coinToss(count: 100)
print("100 подбрасываний монеты — орёл: \(heads), решка: \(tails)")

Случайные элементы коллекции (Collection)

Коллекции (Collections) получают метод randomElement (который возвращает optional если коллекция пуста, также как это делают min and max):

let emotions = "😀😂😊😍🤪😎😩😭😡"
let randomEmotion = emotions.randomElement()!

Используйте метод shuffled, чтобы перетасовать последовательность Sequence или коллекцию:

let numbers = 1...10
let shuffled = numbers.shuffled()

Есть и изменяющий (mutating) вариант под названием shuffle. Он доступен на всех типах, соответвующих протоколам MutableCollection и RandomAccessCollection:

var mutableNumbers = Array(numbers)
// Перемешивает на месте
mutableNumbers.shuffle()

Пользовательские нестандарные генераторы случайных чисел

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

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

/// Фиктивный (для примера) генератор случайных чисел, который просто имитирует `Random.default`.
struct MyRandomNumberGenerator: RandomNumberGenerator {
var base = Random.default
mutating func next() -> UInt64 {
return base.next()
}
}

var customRNG = MyRandomNumberGenerator()
Int.random(in: 0...100, using: &customRNG)

Расширение собственных типов

Можно предоставить API случайных данных для собственных типов, следуя тому же шаблону:

enum Suit: String, CaseIterable {
case diamonds = "♦"
case clubs = "♣"
case hearts = "♥"
case spades = "♠"

static func random(using generator: inout T) -> Suit {
// Using CaseIterable for the implementation
return allCases.randomElement(using: &generator)!

}

static func random() -> Suit {
return Suit.random(using: &Random.default)
}
}

let randomSuit = Suit.random()
randomSuit.rawValue

Редизайн Hashable

Синтезируемые компилятором соответствия протоколам Equatable и Hashable, введённые в Swift 4.1 ([SE-0185](https://github.com/apple/swift-evolution/blob/master/proposals/0185-synthesize-equatable-hashable.md «Synthesizing Equatable and Hashable conformance»)) разительно сокращают количество реализаций Hashable, которые вы должны писать вручную.

Но, если вам нужно кастомизировать для типа соответствие протоколу Hashable, то реконструция протокола Hashable ([SE-0206](https://github.com/apple/swift-evolution/blob/master/proposals/0206-hashable-enhancements.md «Hashable Enhancements»)) делает эту задачу намного проще.

В мире нового Hashable вместо того чтобы реализовывать hashValue, теперь вам требуется реализовать метод hash(into:). Этот метод предоставляет объект Hasher, и все, что вам нужно сделать в вашей реализации, — это передать ему значения, которые вы хотите включить в свое хэш-значение, многократно вызывая hasher.combine(_:).

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

В качестве примера здесь приводится Тип с одним хранимым свойством, выполняющем роль кэша для дорогостоящих вычислений. При этом нам следует игнорировать значение distanceFromOrigin в наших реализациях Equatable и Hashable:

struct Point {
    var x: Int { didSet { recomputeDistance() } }
    var y: Int { didSet { recomputeDistance() } }

    /// Кешировано. Должно игнорироваться Equatable and Hashable [иначе теряется смысл кеширования].
    private(set) var distanceFromOrigin: Double

    init(x: Int, y: Int) {
        self.x = x
        self.y = y
        self.distanceFromOrigin = Point.distanceFromOrigin(x: x, y: y)
    }

    private mutating func recomputeDistance() {
        distanceFromOrigin = Point.distanceFromOrigin(x: x, y: y)
    }

    private static func distanceFromOrigin(x: Int, y: Int) -> Double {
        return Double(x * x + y * y).squareRoot()
    }
}

extension Point: Equatable {
    static func ==(lhs: Point, rhs: Point) -> Bool {
        // При определении равенства distanceFromOrigin не учитывается
        return lhs.x == rhs.x && lhs.y == rhs.y
    }
}

В нашей реализации hash(into:), всё что нам нужно сделать это передать релевантные свойства аргументу hasher.

Это проще (и эффективнее), чем придумывать собственную функцию комбинирования хэша. Например, наивная реализация для hashValue могла бы представлять собой XOR двух координат: return x ^ y. Это было бы менее эффективной хэш-функцией (чем стандартная), потому что Point(3, 4) и Point(4, 3) окажутся с одинаковым хэш-значением.

extension Point: Hashable {
    func hash(into hasher: inout Hasher) {
        // distanceFromOrigin игнорируется для хеширования
        hasher.combine(x)
        hasher.combine(y)
    }
}

let p1 = Point(x: 3, y: 4)
p1.hashValue
let p2 = Point(x: 4, y: 3)
p2.hashValue
assert(p1.hashValue != p2.hashValue)

Усовершенствования, касающиеся условного соответствия

Динамические приведения 👻👻 (Dynamic casts)

Условные соответсвия протоколу ([SE-0143](https://github.com/apple/swift-evolution/blob/master/proposals/0143-conditional-conformances.md «Conditional conformances»)) были заглавной функциональной возможностью Swift 4.1. Последняя часть соответсвующего предложения предложения сThe final piece of the proposal, запрос условных соотвтетвий в runtime, «приземлилась» в Swift 4.2. Это означает динамическое приведение к типу протокола (с использованием is или as?), где значение условно удовлетворяет протоколу, теперь будет удаваться когда условные требования выполнены.

Пример:

func isEncodable(_ value: Any) -> Bool {
    return value is Encodable
}

// Это вернуло бы false в Swift 4.1
let encodableArray = [1, 2, 3]
isEncodable(encodableArray)

// Проверим, что динамическая проверка не проходит, если не выполняются критерии соответствия.
struct NonEncodable {}
let nonEncodableArray = [NonEncodable(), NonEncodable()]
assert(isEncodable(nonEncodableArray) == false)

Синтезированные соответствия в расширениях (extensions)

Небольшое, но важное усовершенствование к синтезируемым компилятором соответствиям протоколу, такое как автоматическое соответствие протоколам Equatable и Hashable предложенные в [SE-0185](https://github.com/apple/swift-evolution/blob/master/proposals/0185-synthesize-equatable-hashable.md «Synthesizing Equatable and Hashable conformance»).

Соответствия протоколам теперь можно синтезировать в расширениях, а не только в определении типа (расширение должно быть в том же файле, что и определение типа). Это больше косметическое изменение, потому что это делает возможным автоматический синтез условных соответствий протоколам Equatable, Hashable, Encodable, и Decodable.

Это пример из What’s New in Swift session at WWDC 2018. Мы можем заставить Either условно соответствовать протоколам Equatable и Hashable:

enum Either {
    case left(Left)
    case right(Right)
}

// Код не требуется
extension Either: Equatable where Left: Equatable, Right: Equatable {}
extension Either: Hashable where Left: Hashable, Right: Hashable {}

Either.left(42) == Either.left(42)

Динамический поиск членов (Dynamic member lookup)

SE-0195 («Introduce User-defined ‘Dynamic Member Lookup’ Types») вводит атрибут @dynamicMemberLookup для деклараций типов.

Переменная типа @dynamicMemberLookup может вызываться с любым геттером (accessor) свойств (используя точечную нотацию) — компилятор не станет проверять существует ли член с указанным именем или нет. Вместо этого компилятор превращает такие обращения в вызовы геттера по сабскрипту, которому передается имя члена в виде строки.

Цель этой функции — обеспечить совместимость между Swift и динамическими языками, такими как Python. Команда Swift for Tensorflow в Google, выдвинувшая это предложение, реализовала мост Python, который делает возможным вызвать код Pyton из Swift Call Python code from Swift. Pedro José Pereira Vieito запаковал это в SwiftPM package названный PythonKit.

SE-0195 не является обязательным для обеспечения такой совместимости, но делает получаемый синтакс Swift намного аккуратнее. Стоит отметить, что SE-0195 имеет дело только с поиском property-style членов (т.е. простых геттеров и сеттеров без аргументов). Второе предложение, «динамически вызываемое» («dynamic callable») для синтаксиса вызова динамического метода всё ещё в работе.

Хотя Python был в центре внимания людей, которые работали над этим предложением, прослойки для взаимодействия с другими динамическими языками, такими как Ruby или JavaScript, также смогут воспользоваться им.

И это также не единственный вариант использования. Любой тип, который в настоящее время имеет стиль API со строками в сабскрипте (string-based subscript-style API), может быть преобразован в стиль динамического поиска членов (dynamic member lookup style). SE-0195 показывает Тип JSON в качестве примера, где вы можете детализировать вложенные словари, используя точечную нотацию.

Вот еще один пример, любезно предоставленный Дугом Грегором: Тип Environment, который дает вам property-style доступ к переменным среды вашего процесса. Обратите внимание, что мутации также работают.

import Darwin

/// Среда (environment) текущего процесса.
///
/// - Author: Doug Gregor, https://gist.github.com/DougGregor/68259dd47d9711b27cbbfde3e89604e8
@dynamicMemberLookup
struct Environment {
    subscript(dynamicMember name: String) -> String? {
        get {
            guard let value = getenv(name) else { return nil }
            return String(validatingUTF8: value)
        }
        nonmutating set {
            if let value = newValue {
                setenv(name, value, /*overwrite:*/ 1)
            } else {
                unsetenv(name)
            }
        }
    }
}

let environment = Environment()

environment.USER
environment.HOME
environment.PATH

// Мутации допускаются, если у сабскрипта есть сеттер
environment.MY_VAR = "Hello world"
environment.MY_VAR

Это большая особенность, имеющая потенциал, способный изменить, использование Swift фундаментальным образом, если применять эту особенность неправильно. Скрывая принципиально «небезопасный» доступ на основе строк (string-based access) за кажущейся «безопасной» конструкцией, вы можете создать у читателей вашего кода неправильное впечатление, что компилятор всё проверил.

Прежде чем вы примете подобные конструкции в своем собственном коде, спросите себя, действительно ли environment.USER намного более читабелен, чем environment["USER"], чтобы быть привносить сопутствующие проблемы. В большинстве ситуаций, я думаю, что ответ должен быть «нет».

Директивы #error и #warning

SE-0196 («Compiler Diagnostic Directives») вводит директивы #error и #warning для инициирования ошибки или предупреждения при сборке исходного кода.

Например, используйте #warning, чтобы не забыть важное действие TODO перед, тем как зафиксировать код:

func doSomethingImportant() {
    #warning("TODO: отсутствует реализация")
}
doSomethingImportant()

/*:
 `#error` может пригодиться, если ваш код не поддерживает определённые среды (environments):
 */
#if canImport(UIKit)
    // ...
#elseif canImport(AppKit)
    // ...
#else
    #error("Для этого playground требуется UIKit или AppKit")
#endif

MemoryLayout.offset(of:)a name=»#MemoryLayoutOffsetOf»>

SE-0210 («Add an offset(of:) method to MemoryLayout») добавляет метод offset(of:) к типу MemoryLayout, дополняня существующий API для получения размера, шага (stride) и выравнивания типа.

Метод offset(of:) принимает в качестве аргумента ключевой путь (key path) к сохраненному свойству типа и возвращает байтовое смещение свойства. Примером, где это полезно, является передача массива значений чередующихся пикселей в графический API.

struct Point {
    var x: Float
    var y: Float
    var z: Float
}

MemoryLayout.offset(of: \Point.z)

@inlinable

SE-0193 («Cross-module inlining and specialization») вводит два новых атрибута @inlinable и @usableFromInline.

Они не являются необходимыми для кода приложения. Авторы библиотеки могут помечать некоторые публичные (public) функции, как @inlinable. Это дает компилятору возможность оптимизировать обобщённый код за пределами модуля.

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

Пример (адаптирован из примера, приведенного в SE-0193):

// Внутри модуля CollectionAlgorithms:
extension Sequence where Element: Equatable {
    /// Возвращает `true`, если все элементы последовательности равны.
    @inlinable
    public func allEqual() -> Bool {
        var iterator = makeIterator()
        guard let first = iterator.next() else {
            return true
        }
        while let next = iterator.next() {
            if first != next {
                return false
            }
        }
        return true
    }
}

[1,1,1,1,1].allEqual()
Array(repeating: 42, count: 1000).allEqual()
[1,1,2,1,1].allEqual()

Хорошенько подумайте, прежде чем делать функцию inlinable (т.е поддерживающей подстановку). Использование @inlinable фактически делает тело функции частью открытого интерфейса вашей библиотеки. Если вы позже измените реализацию (например, чтобы исправить ошибку), двоичные файлы, скомпилированные со старой версией, могут продолжать использовать старый (встроенный) код или даже сочетание старого и нового (потому что @inlinable — это только подсказка; оптимизатор решает для каждого вызова, следует ли вставлять код или нет).

Поскольку inlinable функции могут быть эмитированы в клиентский двоичный файл, они не могут ссылаться на объявления, которые не видны в клиентском двоичном файле. Вы можете использовать директиву @usableFromInline, чтобы сделать некоторые внутренние объявления в вашей библиотеке «ABI-public», что позволит использовать их в inlinable функциях.

Вызов withUnsafePointer(to:_:) с withUnsafeBytes(of:_:) с неизменяемыми значениями (immutable values)

Это мелочь, но если вам когда-либо приходилось использовать функции верхнего уровня withUnsafePointer(to:_:) и withUnsafeBytes(of:_:), вы, возможно, заметили, что они требовали, чтобы их аргумент был изменяемым значением, потому что параметр был inout.

SE-0205 («withUnsafePointer(to:_:) and withUnsafeBytes(of:_:) for immutable values») добавляет перегрузки (overloads), работающие с неизменяемыми значениями (immutable values).

let x: UInt16 = 0xabcd
let (firstByte, secondByte) = withUnsafeBytes(of: x) { ptr in
    (ptr[0], ptr[1])
}
String(firstByte, radix: 16)
String(secondByte, radix: 16)

Ссылки

Источник: Ole Begemann • Июнь 2018 (Исходник на GitHub)
Код: Playground к статье
Переведено Laconic.Website специально для BestKora