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

03 мая 2018

Swift 4.2 это второй минорный релиз языка Swift 4. Данное обновление приносит новую порцию крутых функциональных возможностей. Похоже что этот год будет невероятным для Swift. Это еще раз подтверждает то, что движимый сообществом процесс Swift Evolution помогает языку становиться лучше.
Обновление включает в себя такие функциональные возможности как формирование массива из условий перечисления, директивы компилятора warning и error, динамический поиск элементов, и многие другие. Все они становятся на уровень с другими крутыми функциональными возможностями из Swift 4.1. О том что было нового в Swift 4.1 вы можете узнать подробнее в моей статье про это обновление. Версия Swift 4.2 на днях будет слита в основную ветку языка, и будет дополняться только исправлениями багов. Если произойдут изменения другого рода, статья будет дополнена ими.
Xcode пока еще не включает в себя Swift 4.2, но есть возможность попробовать его сейчас, достаточно скачать снимок ветки development, и активировать его в текущей версии Xcode.
Если вам интересно, то попробуйте запустить файл Xcode Playground который я подготовил для вас. В нём, с примерами, описываются функциональные возможности из Swift 4.2.

Формирование коллекций из условий перечисления

Предложение SE-0194 привносит новый протокол CaseIterable, при использовании которого генерируется массив из всех условий указанных в перечислении.

До версии Swift 4.2. для реализации этой функциональности использовались грязные хаки, прописывание условий вручную или самописные реализации. Сейчас, для перечисления достаточно соответствовать протоколу CaseIterable. Во время компиляции, будет сгенерировано свойство allCases подразумевающее под собой массив из всех условий перечисления, в том порядке, в котором вы их объявили ранее.

Данный пример реализует перечисление названий форм итальянской пасты, и генерирует массив allCases.


enum Pasta: CaseIterable {
    case cannelloni, fusilli, linguine, tagliatelle
}

Далее вы можете у перечисления брать свойство allCases, оно будет выглядеть в качестве массива [Pasta]. Можно даже вывести print для каждого значения массива.


for shape in Pasta.allCases {
    print("I like eating \(shape).")
}

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


enum Car: String, CaseIterable {
    static var allCases: [Car] {
        return [.ford, .toyota, .jaguar, .bmw, .porsche(convertible: false), .porsche(convertible: true)]
    }
    case ford, toyota, jaguar, bmw
    case porsche(convertible: Bool)
}

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

А в этом примере, Swift не сможет сгенерировать свойство allCases, если одно из условий перечисления помечено как unavailable, что значит - недоступное. Если вам понадобится свойство allCases, тогда вам необходимо его добавить самостоятельно.


enum Direction: CaseIterable {
    static var allCases: [Direction] {
        return [.north, .south, .east, .west]
    }
    case north, south, east, west
    @available(*, unavailable)
    case all
}

Запомните: Вам нужно добавлять CaseIterable к исходной реализации вашего перечисления, если вы хотите получить сгенерированное свойство allCases. Добавление CaseIterable к расширению перечисления не приведет к такому результату.

Диагностические директивы warning и error

Предложение SE-0196 представляет новые директивы компилятора, которые помогают нам помечать проблемные места в коде. Директивы будут знакомы тем, кто ранее использовал Objective-C.

Две новые директивы это #warning и #error. Директива #warning потребует у Xcode вызывать предупреждение при компиляции. Директива #error потребует вызвать ошибку компиляции, и код не будет скомпилирован. Вот примерное описание случаев использования этих директив:

  • #warning полезен для напоминания того, что написанная часть кода не до конца реализована. Шаблоны Xcode часто используют #warning у заглушек методов, которые вы должны убрать.
  • #error необходим в том случае если вы разрабатываете библиотеку и нужно описать проблемную ситуацию при компиляции. Допустим, мы требуем у разработчиков использовать свой ключ для аутентификации к веб API и будем вызывать ошибку, через #error, в тех случаях когда стандартный ключ аутентификации к веб API, не был изменён. Тем самым заставляя разработчиков менять его.

Вызываются они в одной и той же форме:


func encrypt(_ string: String, with password: String) -> String {
    #warning("This is terrible method of encryption")
    return password + String(string.reversed()) + password
}
struct Configuration {
    var apiKey: String {
        #error("Please enter your API key below then delete this line.")
        return "Enter your key here"
    }
}

Директивы #warning и #error могут использоваться вместе с условием-директивой #if:


#if os(macOS)
#error("MyLibrary is not supported on macOS.")
#endif

Динамический поиск элементов

Предложение SE-0195 делает Swift похожим на скриптовые языки, такие как Python. Сохраняя строгую типизацию, получаем возможность писать код похожий на синтаксис PHP и Python.

Суть данного предложения выражается в атрибуте под названием @dynamicMemberLookup, который просит Swift во время доступа к свойствам, вызвать метод сабскрипта. Помимо атрибута необходимо реализовать метод сабскри́пта, subscript(dynamicMember:). Метод просит на вход ключ в виде строки, на выходе мы получим значение по данному ключу.

Для понимания сути, обратим внимание на простой пример. Мы можем создать структуру Person, которая дает читать свои свойства из словаря:


@dynamicMemberLookup
struct Person {
    subscript(dynamicMember member: String) -> String {
        let properties = ["name": "Taylor Swift", "city": "Nashville"]
        return properties[member, default: ""]
    }
}

Атрибут @dynamicMemberLookup требует, чтобы тип имел реализацию метода subscript(dynamicMember:) для обработки динамического поиска элементов. Как вы видите, я реализовал этот метод, и он принимает на вход имя элемента в виде строки и возвращает строку. Изнутри это выглядит, как будто он принимает на вход ключ словаря в виде строки и возвращает значение по этому ключу.

Теперь данная структура позволяет обратиться к ней в таком виде:


let person = Person()
print(person.name)
print(person.city)
print(person.favoriteIceCream)

Данный код является валидным, хотя данные свойства не указаны в типе Person. Они будут вызваны во время выполнения: результатами выполнения для первых двух свойств будут “Taylor Swift” и “Nashville”, далее будет пустая строка, так как наш словарь не содержит ключа с таким именем.

Мой метод subscript(dynamicMember:) будет строго возвращать строку, если мы указали это. Если вы хотите возвращать другие типы, реализуйте другой subscript(dynamicMember:) таким образом:


@dynamicMemberLookup
struct Employee {
    subscript(dynamicMember member: String) -> String {
        let properties = ["name": "Taylor Swift", "city": "Nashville"]
        return properties[member, default: ""]
    }
    subscript(dynamicMember member: String) -> Int {
        let properties = ["age": 26, "height": 178]
        return properties[member, default: 0]
    }
}

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


let employee = Employee()
let age: Int = employee.age

Другими словами, Swift должен знать точно, какой сабскрипт будет вызван.

Даже можно перегрузить метод сабскрипта для возвращения замыканий:


@dynamicMemberLookup
struct User {
    subscript(dynamicMember member: String) -> (_ input: String) -> Void {
        return {
            print("Hello! I live at the address \($0).")
        }
    }
}
let user = User()
user.printAddress("555 Taylor Swift Avenue")

При вызове перегруженного сабскрипта, user.printAddress с аргументом "555 Taylor Swift Avenue", будет возвращено замыкание, которое, в свою очередь, возвратит результат функции print.

Заметка

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


struct Singer {
    public var name = "Justin Bieber"
    subscript(dynamicMember member: String) -> String {
        return "Taylor Swift"
    }
}
let singer = Singer()
print(singer.name)

Этот код выведет "Justin Bieber", потому что свойство name будет приоритетнее чем сабскрипт для динамических элементов.

Использование атрибута @dynamicMemberLookup не ограничено структурами, его также можно использовать с классами, протоколами, перечислениями и классами с пометкой @objc.

На практике это означает две вещи. Во-первых, вы можете создать класс с пометкой @dynamicMemberLookup, и данный атрибут будет у всех унаследованных классов. Этот пример выведет "I'm a sandwich", так как HotDog наследуется от Sandwich:


@dynamicMemberLookup
class Sandwich {
    subscript(dynamicMember member: String) -> String {
        return "I'm a sandwich!"
    }
}
class HotDog: Sandwich { }
let chiliDog = HotDog()
print(chiliDog.description)

Во вторых, можно помечать атрибутом @dynamicMemberLookup уже существующие типы. Для этого создаем протокол с атрибутом @dynamicMemberLookup, делаем расширение этого протокола, реализуем метод subscript(dynamicMember:). Добавляем расширенный протокол как принятый протокол к нужному для нас типу и в самом типе, описываем реализацию принятого протокола.

Например, данный код описывает новый протокол Subscripting, со стандартной реализацией метода subscript(dynamicMember:) который возвращает строку. Потом идет описание расширения типа String, которое соответствует принятому протоколу:


@dynamicMemberLookup
protocol Subscripting { }
extension Subscripting {
    subscript(dynamicMember member: String) -> String {
        return "This is coming from the subscript"
    }
}
extension String: Subscripting { }
let str = "Hello, Swift"
print(str.username)

Автор данного предложения, Крис Латтнер (Chris Lattner), дает пример, перечисления JSON которое использует динамический поиск элементов для того, чтобы использовать более привычный синтаксис для навигации по JSON:


@dynamicMemberLookup
enum JSON {
    case intValue(Int)
    case stringValue(String)
    case arrayValue(Array)
    case dictionaryValue(Dictionary)
    var stringValue: String? {
        if case .stringValue(let str) = self {
            return str
        }
        return nil
    }
    subscript(index: Int) -> JSON? {
        if case .arrayValue(let arr) = self {
            return index < arr.count ? arr[index] : nil } return nil } subscript(key: String) -> JSON? {
        if case .dictionaryValue(let dict) = self {
            return dict[key]
        }
        return nil
    }
    subscript(dynamicMember member: String) -> JSON? {
        if case .dictionaryValue(let dict) = self {
            return dict[member]
        }
        return nil
    }
}

Не будь, динамического поиска элементов, вы бы обращались к экземпляру перечисления JSON, в таком виде:


let json = JSON.stringValue("Example")
json[0]?["name"]?["first"]?.stringValue

Но он теперь есть, и вы можете использовать данный синтаксис:


json[0]?.name?.first?.stringValue

Думаю что этот пример полностью раскрывает суть @dynamicMemberLookup. Это синтаксический сахар который преобразует кастомный сабскрипт в простой синтаксис с точками.

Примечание к данному предложению: На данный момент, использование динамического поиска элемента, подразумевает, что при использовании данной функциональной возможности будет отсутствовать автодополнение кода. Это особо не удивляет, так как в тех же IDE для Python, первое время отсутствовало автодополнение кода при использовании данной функциональной возможности. Крис Латтнер, автор данного предложения SE-0195, описал планы по автодополнению, вы узнаете много нового прочитав эти планы.

Улучшение для условных соответствий

Впервые, условные соответствия были представлены в Swift 4.1, позволяя типам соответствовать протоколу только при определенных условиях.

Вот если бы у нас был протокол Purchaseable...


protocol Purchaseable {
    func buy()
}

... и простой тип который соответствует этому протоколу ...


struct Book: Purchaseable {
    func buy() {
        print("You bought a book")
    }
}

... тогда бы мы сделали Array, подходящим требованиям Purchaseable, но с одним условием. Все элементы внутри массива должны соответствовать протоколу Purchasable. Вот как это выглядит:


extension Array: Purchaseable where Element: Purchaseable {
    func buy() {
        for item in self {
            item.buy()
        }
    }
}

Этот код компилируется, но тут есть нюанс. Если во время работы программы, вам нужно сделать запрос об условном соответствии, то программа упадёт. Причина заключается в том, что такой запрос не поддерживался в Swift 4.1

Что ж, такое поведение было поправлено в Swift 4.2. Теперь, если вы получаете данные одного типа и пожелаете узнать, конвертируются ли они в протокол с условным соответствием, то программа не упадет.

Вот, смотрите как это выглядит:


let items: Any = [Book(), Book(), Book()]
if let books = items as? Purchaseable {
    books.buy()
}

Только добавлю, проверка на соответствие протоколу Hashable, была улучшена в Swift 4.2. Множество встроенных типов из «Swift standard library», включая: опционалы, массивы, словари, и диапазоны - теперь будут соответствовать Hashable, если их элементы соответствуют Hashable.

Теперь, вот, посмотрите как это выглядит:


struct User: Hashable {
    var name: String
    var pets: [String]
}

В Swift 4.2 данная структура будет соответствовать протоколу Hashable, в Swift 4.1 такая запись бы не сработала.

Метод удаления элементов у коллекции

Предложение SE-0197 открывает нам новый метод removeAll(where:) Он является высокопроизводительным filter() для коллекций. Вы даете ему на вход условие-замыкание, а он удалит всё объекты которые подойдут под условие.

Представьте, что у вас есть коллекция с именами и вы хотите удалить имя "Terry" с использованием removeAll:


var pythons = ["John", "Michael", "Graham", "Terry", "Eric", "Terry"]
pythons.removeAll { $0.hasPrefix("Terry") }
print(pythons)

С filter() ваше решение выглядело бы вот так:


pythons = pythons.filter { !$0.hasPrefix("Terry") }

Однако и этот подход не позволяет эффективно использовать память, потому как здесь вы указываете не то, что вы хотите видеть в своих данных, а наоборот, то, что вы не хотите видеть, но есть и более продвинутые решения, которые реализуются под конкретные нужды и под конкретные задачи, которые не подходят начинающим разработчикам. Автор этого предложения SE-0197, Бен Коэн (Ben Cohen), рассказал о реализации этого предложении более подробно.

Логический переключатель

Предложение SE-0199 привносит незаменимый метод toggle(), который изменяет состояние у булева типа на противоположное. Это предложение вызвало очень бурное обсуждение в сообществе Swift, отчасти из-за того, что некоторые думают о нём, как об очень тривиальном и недостойным для включения в стандарт языка, но и также по причине того , что обсуждение данного предложения на форумах Swift временами выходило из под контроля.

Вся реализация данного предложения занимает всего лишь несколько строк:


extension Bool {
    mutating func toggle() {
        self = !self
    }
}

В конечном счете, теперь наш код пишется почти человеческим языком:


var loggedIn = false
loggedIn.toggle()

Как и было отмечено в предложении, это поможет при сложной структуре данных, как например в такой myVar.prop1.prop2.enabled.toggle(). У нас будет меньше поводов сделать опечатку в коде, которая могла произойти из-за не указания отрицания.

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

Взгляд на будущий Swift 5.0

Apple описывает Swift 4.2 как «путевую точку на пути к стабильности Application Binary Interface в Swift 5», но я думаю вы понимаете - это преуменьшение. Сейчас в языке появилось много улучшений касающихся: новых функциональных возможностей, переосмысления предыдщего функционала, изменений в ABI.

Возможно, мы даже увидим новые возможности которые также появятся в финальном релизе. Предполагаю что это будут SE-0192, SE-0193, SE-0202, SE-0206. И все они появятся в одно и то же время.

Подводя итоги, мы все еще ожидаем от Swift 5.0 большей стабильности ABI, которую многие люди так ждут. Разумеется, осторожный и взвешенный подход Apple, похоже, окупится, и, надеюсь, это означает, что Swift 5.0 нас даже удивит.

 

Ссылка на оригинал статьи.
Автор статьи: Paul Hudson
Перевел статью: Дмитрий Петухов (mail@dphov.com)

Содержание