Продолжая свое знакомство с коллекциями (Collection), мы разберем, что такое словари, для чего они предназначены, чем отличаются и что у них общего с другими видами коллекций, а также как их можно использовать при создании программ.
Словарь — это неупорядоченная коллекция элементов, для доступа к значениям которых используются специальные индексы, называемые ключами. Каждый элемент словаря состоит из уникального ключа, указывающего на данный элемент, и значения. В качестве ключа выступает не автоматически генерируемый целочисленный индекс (как в массивах), а уникальное для словаря значение произвольного типа, определяемое программистом. Чаще всего в качестве ключей используются строковые или целочисленные значения. Все ключи словаря должны иметь единый тип данных. То же относится и к значениям.
ПРИМЕЧАНИЕ Уникальные ключи словарей не обязаны иметь тип String или Int. Чтобы какой-либо тип данных мог использоваться для ключей словаря, он должен быть хешируемым, то есть выполнять требования протокола Hashable (о нем мы говорили в одном из примечаний предыдущей главы).
Как и наборы, словари — это неупорядоченная коллекция. Это значит, что вы не можете повлиять на то, в каком порядке Swift расположит элементы.
Каждый элемент словаря — это пара «ключ—значение». Идея словарей в том, чтобы использовать уникальные произвольные ключи для доступа к значениям, при этом, как и в наборах, порядок следования элементов неважен.
Значение словаря может быть задано с помощью литерала словаря.
Синтаксис
[ключ_1:значение_1, ключ_2:значение_2, ..., ключ_N:значение_N]
• ключ: Hashable — ключ очередного элемента словаря, должен иметь хешируемый тип данных.
• значение: Any — значение очередного элемента словаря произвольного типа данных.
Литерал словаря возвращает словарь, состоящий из N элементов. Литерал обрамляется квадратными скобками, а указанные в нем элементы разделяются запятыми. Каждый элемент — это пара «ключ—значение», где ключ отделен от значения двоеточием. Все ключи между собой должны иметь один и тот же тип данных. Это относится также и ко всем значениям.
Пример
[200:"success",300:"warning",400:"error"]
Данный словарь состоит из трех элементов, ключи которых имеют тип Int, а значение — String. Int является хешируемым типом данных, то есть соответствует требованиям протокола Hashable.
ПРИМЕЧАНИЕ Для создания неизменяемого словаря используйте оператор let, в противном случае — оператор var.
Пример создания словаря приведен в листинге 11.1.
Листинг 11.1
var dictionary = ["one":"один", "two": "два", "three":"три"]
dictionary //["three": "один", "one": "два", "two": "три"]_
Словарь dictionary содержит три элемента. Здесь "one", "two" и "three" — это ключи, которые позволяют получить доступ к значениям элементов словаря. Типом данных ключей, как и типом данных значений элементов словаря, является String.
При попытке создания словаря с двумя одинаковыми ключами Xcode сообщит об ошибке.
Другим вариантом создания словаря служит функция Dictionary(dictionaryLiteral:), принимающая список кортежей, каждый из которых определяет пару «ключ—значение».
Синтаксис
Dictionary(dictionaryLiteral: (ключ_1, значение_1), (ключ_2, значение_2), ..., (ключ_N, значение_N))
• ключ: Hashable — ключ очередного элемента словаря, должен иметь хешируемый тип данных.
• значение: Any — значение очередного элемента словаря произвольного типа данных.
Функция возвращает cловарь, состоящий из N элементов. Кортежи, каждый из которых состоит из двух элементов, передаются в виде списка в качестве значения входного параметра dictionaryLiteral. Первые элементы каждого кортежа (выше обозначены как ключ_1, ключ_2 и т.д.) должны быть одного и того же типа данных. То же относится и ко вторым элементам каждого кортежа (выше обозначены как значение_1, значение_2 и т.д.).
Каждый кортеж отделяется от последующего запятой. Все первые элементы кортежа (определяющие ключи) должны быть одного и того же типа данных. Это относится и ко вторым элементам (определяющим значения).
Пример
Dictionary(dictionaryLiteral: (100, "Сто"), (200, "Двести"),
(300, "Триста"))
Еще одним способом, который мы рассмотрим, будет использование функции Dictionary(uniqueKeysWithValues:), позволяющей создать словарь на основе коллекции однотипных кортежей.
В листинге 11.2 приведен пример создания словаря из массива кортежей с помощью данной функции.
Листинг 11.2
// базовая коллекция кортежей (пар значений)
let baseCollection = [(2, 5), (3, 6), (1, 4)]
// создание словаря на основе базовой коллекции
let newDictionary = Dictionary(uniqueKeysWithValues: baseCollection)
newDictionary //[3: 6, 2: 5, 1: 4]
В функции Dictionary(uniqueKeysWithValues:) используется входной параметр uniqueKeysWithValues, которому передается коллекция пар значений. Результирующий словарь содержит в качестве ключей первый элемент каждой пары значений (каждого кортежа) базовой коллекции, а в качестве значений — второй элемент каждой пары значений.
Вся полезность данного способа проявляется тогда, когда вам необходимо сформировать словарь на основе двух произвольных последовательностей. В этом случае вы можете сформировать из них одну последовательность пар «ключ—значение» с помощью функции zip(_:_:) и передать ее в функцию Dictionary(uniqueKeysWithValues:) (листинг 11.3).
Листинг 11.3
// массив звезд
let nearestStarNames = ["Proxima Centauri", "Alpha Centauri A", "Alpha Centauri B"]
// массив расстояний до звезд
let nearestStarDistances = [4.24, 4.37, 4.37]
// получение словаря, содержащего пары значений
let starDistanceDict = Dictionary(uniqueKeysWithValues: zip(nearestStarNames, nearestStarDistances))
starDistanceDict // ["Proxima Centauri": 4.24, "Alpha Centauri B": 4.37, "Alpha Centauri A": 4.37]
Функция zip(_:_:) пока еще не была нами рассмотрена, мы вернемся к ней в одной из следующих глав. Суть ее работы состоит в том, что она возвращает последовательность пар значений, основанную на двух базовых последовательностях (в данном случае это nearestStarNames и nearestStarDistances). То есть она берет очередное значение каждой последовательности, объединяет их в кортеж и добавляет в результирующую последовательность в качестве элемента.
После этого сформированная последовательность передается входному аргументу uniqueKeysWithValues. В качестве ключей результирующий словарь будет содержать значения первой базовой коллекции (nearestStarNames), а в качестве значения — элементы второй базовой коллекции.
В повседневном программировании этот способ используется не так часто, поэтому запоминать его синтаксис не обязательно. Вам нужно лишь знать, что словарь может быть создан таким образом. При необходимости вы всегда сможете вернуться к книге или к официальной документации от Apple.
Словари, как и другие виды коллекций, имеют обозначение собственного типа данных, у которых есть полная и краткая форма записи.
Синтаксис
Полная форма записи:
Dictionary<Т1,Т2>
Краткая форма записи:
[Т1:Т2]
• T1: Hashable — наименование хешируемого типа данных ключей элементов словаря.
• T2: Any — наименование произвольного типа данных значений элементов словаря.
Словарь с типом данных Dictionary<T1,T2> или [T1:T2] должен состоять из элементов, ключи которых имеют тип данных T1, а значения T2. Обе представленные формы определяют один и тот же тип словаря.
Рассмотрим пример из листинга 11.4.
Листинг 11.4
// Словарь с типом данных [Int:String]
var codeDesc = [200: "success", 300: "warning", 400: "error"]
type(of: codeDesc) //Dictionary<Int, String>.Type
Тип данных словаря codeDesc задан неявно и определен на основании переданного ему значения. Тип задается единожды, и в будущем при взаимодействии с данной коллекцией необходимо его учитывать.
Помимо неявного определения тип словаря также может быть задан явно. В этом случае его необходимо указать через двоеточие после имени параметра.
Синтаксис
Полная форма записи:
var имяСловаря: Dictionary<T1,T2> = литерал_словаря
Краткая форма записи:
var имяСловаря: [T1:T2] = литерал_словаря
В обоих случаях объявляется словарь, ключи элементов которого должны иметь тип T1, а значения — T2.
Пример
var dictOne: Dictionary<Int,Bool> = [100: false, 200: true, 400: true]
var dictTwo: [String:String] = ["Jonh":"Dave", "Eleonor":"Green"]
Как отмечалось ранее, доступ к элементам словаря происходит с помощью уникальных ключей. Как и при работе с массивами, ключи предназначены не только для получения значений элементов словаря, но и для их изменения (листинг 11.5).
Листинг 11.5
var countryDict = ["RUS":"Россия", "BEL": "Беларусь", "UKR":"Украина"]
// получаем значение элемента
var countryName = countryDict["BEL"]
countryName // "Беларусь"
// изменяем значение элемента
countryDict["RUS"] = "Российская Федерация"
countryDict // ["RUS": "Российская Федерация", "BEL": "Беларусь", "UKR": "Украина"]
В результате исполнения данного кода словарь countryDict получает новое значение для элемента с ключом RUS.
Изменение значения элемента словаря также может быть произведено с помощью метода updateValue(_:forKey:). В случае, если изменяемый элемент отсутствует, будет возвращен nil (с ним мы уже встречались в предыдущей главе). В случае успешного изменения будет возвращено старое значение элемента.
Как показано в листинге 11.6, при установке нового значения данный метод возвращает старое значение или nil, если значения по переданному ключу не существует).
Листинг 11.6
var oldValueOne = countryDict.updateValue("Республика Белоруссия", forKey: "BEL")
// в переменной записано старое измененное значение элемента
oldValueOne //"Беларусь"
var oldValueTwo = countryDict.updateValue("Эстония", forKey: "EST")
// в переменной записан nil, так как элемента с таким ключом
// не существует
oldValueTwo //nil
Для изменения значения в метод updateValue передаются новое значение элемента и параметр forKey, содержащий ключ изменяемого элемента.
Для того чтобы создать новый элемент в словаре, достаточно обратиться к несуществующему элементу и передать ему значение (листинг 11.7).
Листинг 11.7
countryDict["TUR"] = "Турция"
countryDict //["BEL": "Республика Белоруссия", "TUR": "Турция", "UKR": "Украина", "EST": "Эстония", "RUS": "Российская Федерация"]
Для удаления элемента (пары «ключ—значение») достаточно присвоить удаляемому элементу nil или использовать метод removeValue(forKey:), указав ключ элемента (листинг 11.8).
Листинг 11.8
countryDict["TUR"] = nil
countryDict.removeValue(forKey: "BEL")
countryDict //["RUS": "Российская Федерация", "UKR": "Украина", "EST": "Эстония"]
При использовании метода removeValue(forKey:) возвращается значение удаляемого элемента.
ПРИМЕЧАНИЕ Есть один секрет, к которому вы, вероятно, пока не готовы, но о котором все же нужно сказать. Если вы попытаетесь получить доступ к несуществующему элементу словаря, это не приведет к ошибке. Swift просто вернет nil.
А это значит, что любое возвращаемое словарем значение — опционал, но познакомиться с этим понятием нам предстоит лишь в одной из будущих глав.
let someDict = [1:"one", 3:"three"]
someDict[2] // nil
type(of: someDict[2]) //Optional<String>.Type
Пустой словарь не содержит элементов (как и пустые набор и массив). Для того чтобы создать пустой словарь, необходимо использовать литерал без элементов. Для этого предназначена конструкция [:] или функция Dictionary<типКлючей:типЗначений>() без входных аргументов (листинг 11.9).
Листинг 11.9
var emptyDictionary: [String:Int] = [:]
var anotherEmptyDictionary = Dictionary<String,Int>()
С помощью конструкции [:] также можно уничтожить все элементы словаря, если проинициализировать ее словарю в качестве значения (листинг 11.10).
Листинг 11.10
var birthYears = [1991: ["John", "Ann", "Vasiliy"], 1993: ["Alex", "Boris"] ]
birthYears = [:]
birthYears //[:]
Обратите внимание, что в качестве значения каждого элемента словаря в данном примере используется массив с типом [String]. В результате тип самого словаря birthYears будет [Int:[String]].
Вы совершенно не ограничены в том, значения каких типов использовать для вашей коллекции.
Словари, как и массивы с наборами, имеют большое количество свойств и методов, наиболее важные из которых будут рассмотрены в этом разделе.
Свойство count возвращает количество элементов в словаре (листинг 11.11).
Листинг 11.11
var someDictionary = ["One":1, "Two":2, "Three":3]
// количество элементов в словаре
someDictionary.count // 3
Если свойство count равно нулю, то свойство isEmpty возвращает true (листинг 11.12).
Листинг 11.12
var emptyDict: [String:Int] = [:]
emptyDict.count //0
emptyDict.isEmpty //true
При необходимости вы можете получить все ключи или все значения словаря с помощью свойств keys и values (листинг 11.13).
Листинг 11.13
// все ключи словаря countryDict
var keys = countryDict.keys
type(of: keys) // Dictionary<String, String>.Keys.Type
keys // Dictionary.Keys(["UKR", "RUS", "EST"])
// все значения словаря countryDict
var values = countryDict.values
type(of: values) //Dictionary<String, String>.Values.Type
values //Dictionary.Values(["Украина", "Эстония", "Российская Федерация"])
При обращении к свойствам keys или values Swift возвращает не массив или набор, а значение специального типа данных Dictionary<ТипКлюча,ТипЗначения>.Keys и Dictionary<ТипКлюча,ТипЗначения>.Values.
Не пугайтесь столь сложной записи. Как неоднократно говорилось, Swift используется огромное количество различных типов, у каждого из которых свое предназначение. В данном случае указанные типы служат для доступа к ключам или значениям исходного словаря. При этом они являются полноценными коллекциями (соответствуют требованиям протокола Collection), а значит, могут быть преобразованы в массив или набор (листинг 11.14).
Листинг 11.14
var keysSet = Set(keys)
keysSet //{"UKR", "RUS", "EST"}
var valuesArray = Array(values)
valuesArray //["Эстония", "Украина", "Российская Федерация"]
Вернемся еще раз к новому для вас обозначению типов данных Dictionary<T1,T2>.Keys и Dictionary<T1,T2>.Values. Обратите внимание, что Keys и Values пишутся через точку после типа словаря. Это говорит о том, что типы данных Keys и Values реализованы внутри типа Dictionary<T1,T2>, то есть они не существуют отдельно от словаря и могут быть использованы только в его контексте. Таким образом, вы не можете создать параметр типа Values (var a: Values = ... не будет работать), так как глобально такого типа нет. Он существует только в контексте какого-либо словаря.
Это связано с тем, что вам, в принципе, никогда не понадобятся значения этих типов отдельно от родительского словаря. При этом Swift не знает, в каком виде вы хотели бы получить эти значения, а значит, возвращать готовый массив или набор было бы бесцельной тратой ресурсов.
В результате вы получаете значение типа Dictionary<T1,T2>.Keys и Dictionary<T1,T2>.Values и спокойно обрабатываете его (преобразовываете в другой вид коллекции или проходитесь по его элементам).
Не переживайте, если данный материал оказался для вас сложным. Вероятно, вы еще не готовы полностью воспринять его. В будущем, углубляясь в разработку на Swift и в создание собственных типов, вы сможете вернуться к этому описанию. Сейчас вам важно запомнить лишь то, что свойства keys и values возвращают коллекции элементов, которые могут быть преобразованы в массив или набор.