Туториал: Что нового в Swift 4.0?

Если вы нашли опечатку в тексте, выделите ее и нажмите CTRL + ENTER.

Xcode: 
9.0
Swift: 
4.0

Поехали!

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

Swift 4 представляет новый протокол Codable, который позволяет сериализовать и десериализовать данные без написания специального кода и не уйти от типов значения к ссылочным типам. Более того, вы можете выбрать то, как вы хотите чтобы ваши данные были сериализованы через формат JSON либо через формат классического списока (Property list file).

Да, вы все правильно прочитали, Swift 4 позволяет сериализовать ваши собственные типы данных в JSON без написания какого-либо специфичного кода.

Давайте посмотрим насколько это круто. Сначала давайте создадим какой-либо тип через структуру и создадим несколько экземпляров данного типа.

struct Language: Codable {  
    var name: String
    var version: Int
}

let swift = Language(name: "Swift", version: 4)  
let php = Language(name: "PHP", version: 7)  
let perl = Language(name: "Perl", version: 6) 

Как вы можете видеть я обозначил структуру Language через протокол Codable. Благодаря этому небольшому изменению мы можем представить наш формат Data в виде JSON:

let encoder = JSONEncoder()  
if let encoded = try? encoder.encode(swift) {  
    // save `encoded` somewhere
}

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

Если у вас, так же как и у меня есть большой опыт использования NSCoding, то вас скорее всего посещают сомнения о том, как можно проверить, что это работает. Что ж, давайте с вами добавим немного кода для конвертации объекта Data в String, чтобы мы могли распечатать его. А после этого мы с вами раскодируем в новый экземпляр структуры Language:

if let encoded = try? encoder.encode(swift) {  
    if let json = String(data: encoded, encoding: .utf8) {
        print(json)
    }

    let decoder = JSONDecoder()
    if let decoded = try? decoder.decode(Language.self, from: encoded) {
        print(decoded.name)
    }
}

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

И JSONEncoder и property list имеют множетсво опций для использования того, как они работают. Вы хотите компактный JSON или pretty-printed JSON? Вы хотите использовать дату формата ISO8601 или дату формата UNIX? Вы хотите использовать property list или XML? Подробнее о вероятных изменениях можно прочитать по ссылке.

Многострочные строковые литералы

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

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

let longString = """  
When you write a string that spans multiple  
lines make sure you start its content on a  
line all of its own, and end it with three  
quotes also on a line of their own.  
Multi-line strings also let you write "quote marks"  
freely inside your strings, which is great!  
"""

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

Улучшенные keypath для key-value coding

Одной из самых возлюбленных особенностей Objective-C является возможность создавать динамические ссылки на свойства, а не использование прямых ссылок, другими словами возможность сказать "у объекта X есть свойство, которое я бы хотел прочитать", но фактически не читая его. Такие ссылки называются keypath, и они отличаются от прямого доступа к свойству, потому что фактически они не читают и не записывают значение, они просто прячут это значение для дальнейшего использования.

Если вы никогда ранее не использовани keypath, то позвольте я покажу вам аналогию того, как они работают на обычных методах Swift. Мы определим струкруту Starship и струкруту Crew, затем создадим по одному экземпляру:

// an example struct
struct Crew {  
    var name: String
    var rank: String
}

// another example struct, this time with a method
struct Starship {  
    var name: String
    var maxWarp: Double
    var captain: Crew

    func goToMaximumWarp() {
        print("\(name) is now travelling at warp \(maxWarp)")
    }
}

// create instances of those two structs
let janeway = Crew(name: "Kathryn Janeway", rank: "Captain")  
let voyager = Starship(name: "Voyager", maxWarp: 9.975, captain: janeway)

// grab a reference to the `goToMaximumWarp()` method
let enterWarp = voyager.goToMaximumWarp

// call that reference
enterWarp()  

Так как функции являются типами первого класса в Swift, то последние две строки могут создать ссылку на goToMaximumWarp() при помощи нового метода enterWarp(), который вы можете вызвать когда захотите. Проблема в том, что вы не можете сделать то же самое со свойствами, потому что Swift просто читает значения свойств напрямую, и вы всегда получаете исходное значение.

Теперь это исправлено в keypath. Вы так же можете создавать невызванные ссылки на свойства, как и в случае с enterWarp(). Если вы обратитесь по ссылке сейчас, то получите значение, которое храниться сейчас, если обратитесь позже, то вы получите последнее значение, которое храниться по ссылке. Вы можете перебрать любые свойства и убедитесь, что выведение типов в Swift всегда будет возвращать правильный тип.

Комьюнити эволюции Swift провели длительное время в рассуждениях синтаксиса для keypath, чтобы этот синтаксис принципиально отличался от того, что используется сейчас Swift коде. В итоге пришли к такой фрме записи с обратными слешами: \Starship.name\Starship.maxWarp и \Starship.captain.name. Вы можете присваивать эти значения в переменные и использовать там, где вам нужно. Например:

let nameKeyPath = \Starship.name  
let maxWarpKeyPath = \Starship.maxWarp  
let captainName = \Starship.captain.name

let starshipName = voyager[keyPath: nameKeyPath]  
let starshipMaxWarp = voyager[keyPath: maxWarpKeyPath]  
let starshipCaptain = voyager[keyPath: captainName]  

Этот код создаете несколько констант, и каждая из них имеет свой тип, например, starshipName имеет тип StringstarShitMaxWarp - Double. Последний пример имеет ссылку не просто на свойсво, но на свойство свойства, и Swift все равно выводит его тип корректно.

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

Антракт

Вы можете так же посмотреть новое Natural Swift видео продолжительностью 75 минут на английском языке. Мы с вами разберем три важные темы: функциональное программирование, протоколоориентированное программирование и типы значения. Для того, чтобы скачать видео, вам нужно лишь пройти по ссылке.

Улучшенная функциональность словарей

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

Давайте начнем с вами с простого примера - фильтрация словая в Swift 3 не возвращает новый словарь. Вместо этого мы с вами получаем массив кортежей с key/value ярлыками. Например:

let cities = ["Shanghai": 24_256_800, "Karachi": 23_500_000, "Beijing": 21_516_000, "Seoul": 9_995_000];  
let massiveCities = cities.filter { $0.value > 10_000_000 }  

После того как код будет запущен, вы не сможете прочитать massiveCities["Shanghai"], потому что massiveCities уже не будет словарем. Для того, чтобы прочитать значение, вам нужно будет написать massiveCities[0].value, что не особо красиво. 

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

Аналогично и с методом map() - словари никогда не работали так, как ожидали от них разработчики. Если вы используете метод map со словарями, то вы получаете на выходе массив с какими-либо значениями. Например:

let populations = cities.map { $0.value * 2 }  

Это не поменялось и в Swift 4, но однако появлися новый метод mapValues(), который как раз помогает нам трансформировать значения и поместить их обратно в массив по исходным ключам.

Например, следующий код позволяет превратить численность городов в строковое значение и поместить их обратно в словарь под теми же ключами, что и ранее (ShanghaiKarachi и Seoul): 

let roundedCities = cities.mapValues { "\($0 / 1_000_000) million people" }  

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

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

let groupedCities = Dictionary(grouping: cities.keys) { $0.characters.first! }  
print(groupedCities)  

Вот что у нас будет в консоли:

["B": ["Beijing"], "S": ["Shanghai", "Seoul"], "K": ["Karachi"]]

Аналогично, мы могли бы  сгруппировать города, основываясь на длинне их имен, как например тут:

let groupedCities = Dictionary(grouping: cities.keys) { $0.count }  
print(groupedCities)  

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

[5: ["Seoul"], 7: ["Karachi", "Beijing"], 8: ["Shanghai"]]

И наконец, у нас появилась возможность указывать дефолтное значение, если ключ отсутствует:

let person = ["name": "Taylor", "city": "Nashville"]  
let name = person["name", default: "Anonymous"]  

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

let name = person["name"] ?? "Anonymous"  

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

var favoriteTVShows = ["Red Dwarf", "Blackadder", "Fawlty Towers", "Red Dwarf"]  
var favoriteCounts = [String: Int]()

for show in favoriteTVShows {  
    favoriteCounts[show, default: 0] += 1
}

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

Строки снова стали коллекциями!

Это небольшое изменение гарантирует большую радость для разработчиков. Это значит, что теперь вы сможете "разворачивать" строки, перебирать через циклы, использовать map()flatMap() и много другое. Например:

let quote = "It is a truth universally acknowledged that new Swift versions bring new features."  
let reversed = quote.reversed()

for letter in quote {  
    print(letter)
}

Это изменение было внесено в рамках широкого набора правок под названием  «Манифест строк».

Односторонние диапазоны

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

Пример использования:

let characters = ["Dr Horrible", "Captain Hammer", "Penny", "Bad Horse", "Moist"]  
let bigParts = characters[..<3]  
let smallParts = characters[3...]  
print(bigParts)  
print(smallParts)  

Код распечатает нам:

["Dr Horrible", "Captain Hammer", "Penny"]
["Bad Horse", "Moist"]

Для более подробной об этом нововведении в Swift смотрите тут.

Многое еще впереди...

Первый релиз Xcode с новой версией Swift 4 вероятно произойдет в июне, предположительно вместе с iOS 11, tvOS 11, watchOS 4 и новой версией macOS. Все, что мы уже с вами видели выглядит очень многообещающе, отчасти из-за того, что Swift разработчики очень усердно работают, чтобы сделать Swift 4 как можно более совместимым со старыми версиями Swift. В основном мы наблюдаем за добавлением новых особенностией в язык, а не за новыми конструкциями, которые уничтожают уже написанный код.

Несмотря на то, что эволюция Swift иногда может выгляжеть хаотичной (уровни доступа, есть идеи?), Swift 4 подтверждает серьезность подхода комьюнити Apple. Ссылки на предложения, которые широко обсуждаются комьюнити - это компромис, которого пытаются достичь, а не просто изменения языка, которые форсируют инженеры Apple. Эти изменения яыляются разумным и продуманным подходом к языку, который уже является глубоким и элегантным.

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

Автор/Переводчик: Акулов Иван

This article was translated with permission from the English original, What's new in Swift 4.0? on Hacking with Swift.