Что нового в Swift 5.0

Туториалы

Что нового в Swift 5.0

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

Если вы хотите протестировать Swift 5.0 еще до его официального релиза в начале следующего года, загрузите development версию Swift по ссылке (примерно 2ГБ), активируйте ее в текущей версии Xcode, а затем следуйте приведенным ниже примерам!

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

"Сырые" строки

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

Чтобы использовать"сырые" строки, поместите один или несколько символов # перед строками, например:

let rain = #"The "rain" in "Spain" falls mainly on the Spaniards.”#

Символы # в начале и в конце строки становятся частью разделителя строк, поэтому Swift понимает, что автономные кавычки вокруг «rain» и «Spain» следует рассматривать как буквальные кавычки, а не как конец или начало строки.

"Сырые" строки также позволяют использовать обратный слеш:

let keypaths = #"Swift keypaths such as \Person.name hold uninvoked references to properties."#

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

let answer = 42
let dontpanic = #"The answer to life, the universe, and everything is \#(answer).”#

Обратите внимание, как я использовал \#(answer) для интерполяции строк - обычный

 \(answer) будет интерпретироваться как обычные символы в строке, поэтому, если вы хотите, чтобы интерполяция строки происходила в "сырой" строке, то вам нужно добавить дополнительный символ #.

Одной из интересных особенностей "сырых" строк в Swift является использование хеш-символов в начале и в конце, потому что вы можете использовать более одного символа (?) при необходимости (маловероятно, но вдруг). Трудно привести хороший пример, потому что эта подобная ситуация действительно очень редка, но давайте рассмотрим такую строку: My dog said "woof"#gooddog. Поскольку перед хеш нет пробела, Swift увидит «#» и сразу же интерпретирует его как символ окончания строки. В этой ситуации нам нужно изменить наш разделитель с # на ##, например так:

let str = ##"My dog said “woof"#gooddog"##

Обратите внимание, что количество хеш-символов в конце должно совпадать с числом в начале.

"Сырые" строки полностью совместимы с многострочной строковой системой Swift - просто используйте #”"" в начале, и затем """# для завершения, вот так:

let multiline = #"""
The answer to life,
the universe,
and everything is \#(answer).
“""#

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

let regex1 = "\\\\[A-Z]+[A-Za-z]+\\.[a-z]+"

Благодаря "сырым" строкам, можно написать это выражение без половины обратных слешей:

let regex2 = #"\\[A-Z]+[A-Za-z]+\.[a-z]+"#

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

Динамически вызываемые типы

SE-0216 добавляет в Swift новый атрибут @dynamicCallable, что дает возможность отмечать тип как вызываемый напрямую. Это синтаксический сахар, а не какая-либо магия компилятора, эффективно преобразующая этот код:

let result = random(numberOfZeroes: 3)

В этот:

let result = random.dynamicallyCall(withKeywordArguments: ["numberOfZeroes": 3])

Ранее я писал о функции @dynamicMemberLookup в Swift 4.2. @dynamicCallable является естественным расширением @dynamicMemberLookup и служит той же цели: сделать работу кода Swift более легкой в связке с динамическими языками, такими как Python и JavaScript.

Чтобы добавить эту функциональность вашим собственным типам, вам нужно добавить атрибут @dynamicCallable и один или оба из этих методов:

func dynamicallyCall(withArguments args: [Int]) -> Double
func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Double

Первый из них используется, когда вы вызываете тип без имен параметров (например, a (b, c)), а второй используется, когда вы предоставляете имена (например, a (b: cat, c: dog)).

@dynamicCallable действительно гибок к типам данных, принимающих и возвращающих его методы, что позволяет вам воспользоваться всеми преимуществами безопасности типов в Swift, в то же время сохраняя пространство для их расширенного использования. Таким образом, для первого метода (без имен параметров) вы можете использовать все, что соответствует ExpressibleByArrayLiteral, например массивы, срезы массивов и сеты, а для второго метода (с именами параметров) вы можете использовать все, что соответствует ExpressibleByDictionaryLiteral, например словари и пары ключ-значение.

Примечание. Если вы ранее не использовали KeyValuePairs, сейчас самое время узнать, что они из себя представляют, потому что они чрезвычайно полезны с @dynamicCallable. Здесь подробнее о них: Что такое KeyValuePairs?

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

Давайте рассмотрим пример. Структура RandomNumberGenerator генерирует числа от 0 до определенного максимума, в зависимости от того, какие входные данные были переданы:

struct RandomNumberGenerator {
    func generate(numberOfZeroes: Int) -> Double {
        let maximum = pow(10, Double(numberOfZeroes))
        return Double.random(in: 0...maximum)
    }
}

Чтобы переключить все это на @dynamicCallable, мы бы написали что-то вроде этого:

@dynamicCallable
struct RandomNumberGenerator {
    func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Double {
        let numberOfZeroes = Double(args.first?.value ?? 0)
        let maximum = pow(10, numberOfZeroes)
        return Double.random(in: 0...maximum)
    }
}

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

Теперь мы можем создать экземпляр RandomNumberGenerator и вызвать его как функцию:

let random = RandomNumberGenerator()
let result = random(numberOfZeroes: 0)

Если бы вы использовали dynamicallyCall(withArguments:), вы бы написали это:

@dynamicCallable
struct RandomNumberGenerator {
    func dynamicallyCall(withArguments args: [Int]) -> Double {
        let numberOfZeroes = Double(args[0])
        let maximum = pow(10, numberOfZeroes)
        return Double.random(in: 0...maximum)
    }
}

let random = RandomNumberGenerator()
let result = random(0)

При использовании @dynamicCallable необходимо учитывать некоторые важные правила:

Вы можете применять его к структурам, перечислениям, классам и протоколам.

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

Если ваши реализации withKeywordArguments: или withArguments: помечены как throws, то вызов типа также будет throwing.

Вы не можете добавлять @dynamicCallable к расширению, только к первичному определению типа.

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

Возможно, более важно то, что отсутствует поддержка разрешения методов, что означает, что мы должны вызывать тип напрямую (например, random(numberOfZeroes: 5)), а не вызывать конкретные методы для типа (например, random.generate(numberOfZeroes: 5)). Уже обсуждалось добавление последнего, используя сигнатуры метода, вот так:

func dynamicallyCallMethod(named: String, withKeywordArguments: KeyValuePairs<String, Int>)

Если это станет возможным в будущих версиях Swift, то это может открыть очень интересные возможности для тестовых моков.

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

Обработка будущих кейсов перечисления

SE-0192 добавляет возможность различать фиксированные перечисления и перечисления, которые могут изменяться в будущем.

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

С помощью атрибута @unknown теперь мы можем различать два слегка различающихся сценария: «этот дефолтный кейс должен выполняться для всех остальных случаев, потому что я не хочу обрабатывать их по отдельности» и «Я хочу обрабатывать все кейсы по отдельности, но если в будущем что-нибудь произойдет, то используйте его, а не вызывайте ошибку».

Вот пример энума:

enum PasswordError: Error {
    case short
    case obvious
    case simple
}

Мы могли бы написать код для обработки каждого из этих кейсов, используя блок switch:

func showOld(error: PasswordError) {
    switch error {
    case .short:
        print("Your password was too short.")
    case .obvious:
        print("Your password was too obvious.")
    default:
        print("Your password was too simple.")
    }
}

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

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

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

Например:

func showNew(error: PasswordError) {
    switch error {
    case .short:
        print("Your password was too short.")
    case .obvious:
        print("Your password was too obvious.")
    @unknown default:
        print("Your password wasn't suitable.")
    }
}

Этот код теперь будет выдавать предупреждения, потому что блок switch больше не является исчерпывающим - Swift хочет, чтобы мы обрабатывали каждый кейс явно. И хорошо, что это только предупреждение, и именно этот факт делает этот атрибут таким полезным: если фреймворк добавит новый кейс в будущем, то вы будете предупреждены об этом, но он не нарушит ваш исходный код.

Выравнивание вложенных опционалов получающихся из try?

SE-0230 изменяет то, как работает try? так, что вложенные опционалы выравниваются, чтобы стать обычными опционалами. Это позволяет ему работать так же, как опциональным цепочкам и условным приведениям типов, которые также выравнивают опционалы в более ранних версиях Swift.

Вот практический пример, который демонстрирует изменение:

struct User {
    var id: Int

    init?(id: Int) {
        if id < 1 { return nil } self.id = id } func getMessages() throws -> String {
        // complicated code here
        return "No messages"
    }
}

let user = User(id: 1)
let messages = try? user?.getMessages()

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

Ключевая строка является последней: поскольку пользователь является опциональным, он использует опциональную цепочку, а поскольку getMessages() может выбросить ошибку, он использует try?, для того чтобы преобразовать метод throwing в опциональный и в итоге мы получим вложенный опционал. В более ранних версиях до Swift 4.2 это делалось через messages и String??- строка с двойным опционалом - но в Swift 5.0 и более поздних версиях try? не оборачивает значения в опционал, если они уже являются опционалами, поэтому messages будут просто String?.

Это новое поведение соответствует существующему поведению опциональной цепочки и условному приведению типов. То есть, если вы хотите, вы можете использовать опциональную цепочку дюжину раз в одной строке кода, но вы не получите 12 вложенных  опционалов. Точно так же, если вы используете опциональную цепочку с as?, то вы все равно получите только один уровень опциональности, ведь обычно это именно то, что вам нужно.

Проверка кратности целому числу

SE-0225 добавляет метод isMultiple(of:) к целым числам, позволяя нам проверять, является ли одно число кратным другому, гораздо более понятным способом, чем использование операции деления остатка, %.

Например:

let rowNumber = 4

if rowNumber.isMultiple(of: 2) {
    print("Even")
} else {
    print("Odd")
}

Да, мы могли бы написать ту же самую проверку, используя if rowNumber % 2 == 0, но вы должны признать, что это менее очевидно - использование isMultiple(of:) в качестве метода означает, что он может быть предложен функцией автодополнения Xcode, что само собой делает его более очевидным для использования.

Подсчет элементов, удовлетворяющих условию в последовательности

SE-0220 вводит новый метод count(where:), который выполняет эквивалент filter() и подсчитывает за один проход. Это экономит время на создание нового массива и обеспечивает красивое решение распространенной задачи.

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

let scores = [100, 80, 85]
let passCount = scores.count { $0 >= 85 }

Здесь подсчитывается, сколько имен в массиве начинаются с «Terry»:

let pythons = ["Eric Idle", "Graham Chapman", "John Cleese", "Michael Palin", "Terry Gilliam", "Terry Jones"]
let terryCount = pythons.count { $0.hasPrefix("Terry") }

Этот метод доступен для всех типов, которые соответствуют Sequence, поэтому вы можете использовать его также в наборах и словарях.

Преобразование и извлечение значений словаря с помощью compactMapValues()

SE-0218 добавляет новый метод compactMapValues() в словари, объединяя функциональность compactMap() из массивов («преобразуйте мои значения, извлеките результаты, затем отбросьте все, что nil») и с помощью метода mapValues() из словарей («оставьте ключи нетронутыми, но преобразуйте значения»).

В качестве примера приведу словарь, состоящий из участников гонки, а также время, которое им потребовалось, чтобы финишировать в секундах. Один человек не финишировавший, отмечен как «DNF»:

let times = [
    "Hudson": "38",
    "Clarke": "42",
    "Robinson": "35",
    "Hartis": "DNF"
]

Мы можем использовать compactMapValues() для создания нового словаря с именами и временем в виде целого числа с одной удаленной парой, содержащей значение DNF:

let finishers1 = times.compactMapValues { Int($0) }

Или вы можете просто передать инициализатор Int напрямую в compactMapValues(), например так:

let finishers2 = times.compactMapValues(Int.init)

Вы также можете использовать compactMapValues(), чтобы извлечь опционалы и отбросить значения nil без выполнения какого-либо преобразования, например так:

let people = [
    "Paul": 38,
    "Sophie": 8,
    "Charlotte": 5,
    "William": nil
]

let knownAges = people.compactMapValues { $0 }

И это только начало!

Это те функции, которые уже были реализованы в ветке Swift 5.0, но, несомненно, они появятся в ближайшие месяцы до официального релиза Swift 5.0.

И давайте не будем забывать, что стабильность ABI остается самой большой фишкой 5.0: многие крупные компании (включая почти наверняка саму Apple) ждут этого, и начнется великая миграция Swift.

Захватывающие времена!

Ссылка на оригинал статьи.
Автор статьи: Paul Hudson
Урок подготовил: Акулов Иван

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

Комментарии

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: