obo.dev

Коллекции. Словари (Dictionary)

05 Dec 2022

Словари (Dictionary)

Словарь - это еще один тип коллекций в программировании на Swift. С помощью словаря можно хранить данные в формате ключ-значение.

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

Словарь - это коллекция неуникальных неупорядоченных однотипных данных в формате “ключ-значения”. При этом ключи в этой коллекции - уникальные и хэшируемые.

Словарь в Swift можно представить как обычный орфографический словарь, в котором, каждое определяемое слово уникально (Ключ) и имеет определение (Значение).

Что из себя представляет словарь?

Словарь — это тип хеш-таблицы, обеспечивающий быстрый доступ к содержащимся в ней записям. Каждая запись в таблице идентифицируется с помощью своего ключа, который является хешируемым типом, таким как строка или число. Этот ключ используется для получения соответствующего значения, которое может быть любым объектом. В других языках подобные типы данных известны как хэши или ассоциированные массивы.

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

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

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

Если в качестве значений пары “ключ-значение” необходимо указать данные разных типов, тогда тип данных для значений в словаре будет по типу “Any”.

Нельзя изменить тип словаря после того, как он был объявлен.

Где используют словари?

Можно использовать словарь для хранения коллекции элементов в формате “ключ-значение”. Словарь похож на массив в том смысле, что оба они содержат элементы только одного типа.

Пример:

let scores = ["Bob": 42, "Alice": 10, "Daisy": 33]
 
print(scores)

// Output
// ["Alice": 10, "Bob": 42, "Daisy": 33]

Тип словаря scores [String: Int]. Это обозначает, что тип ключей в словаре и тип его значений являются “String” и “Int” соответственно. В приведенном выше примере тип словаря явно не указан, потому что Swift может вывести его из контекста.

Пример структуры данных формата JSON, который широко используется различными API и веб-сервисами:

[
    {
        "username": "@reinder42",
        "profile_url": "https://twitter.com/profiles/reinder42.jpg"
        "tweets": [...]
    }, {
        "username": "@arthurdent",
        "profile_url": "https://twitter.com/profiles/arthur_dent.jpg"
        "tweets": [...]
    }, {
        "username": "@xxx_darthvader1999",
        "profile_url": "https://twitter.com/profiles/darthvader.jpg"
        "tweets": [...]
    }
]

Можно увидеть, что элемент верхнего уровня — это массив, обозначенный как [...]. И элементы в массиве являются словарями, обозначенными {...}. Они связывают ключи со значениями, такими как “username” и “@reinder42”.

Можно преобразовать JSON в массив словарей Swift:

let twitter = [
    [
        "username": "@reinder42",
        "profile_url": "https://twitter.com/profiles/reinder42.jpg"
        "tweets": [···]
    ],
    ···
]

При переборе массива “twitter” с помощью цикла “for-in” можно получить имя пользователя Twitter:

for item in twitter {
    let username = item["username"]
 
    // Выполнить код.
}

Также словари в Swift используются:

  • При сохранении пользовательских настроек по умолчанию в UserDefaults;
  • При сохранение значений ключей в iCloud;
  • Объекты Firebase Firestore являются словарями;
  • Основой файлов в формате Plist является словарь, закодированный как XML.

Создание словаря

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

Тип словаря в Swift в полной форме пишется как Dictionary<Key, Value>, где Key - это тип значения который используется как ключ словаря, а Value - это тип значения который словарь может хранить для этих ключей.

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

В сокращенной форме тип словаря можно написать как [Key: Value].

Хотя две формы функционально идентичны, краткая форма является предпочтительной и используется чаще.

Декларирование словаря:

var dict1: Dictionary<String, Int>

Или так:

var dict2: [String: Int]

После объявления словаря для дальнейшей работы необходимо инициализировать словарь:

dict1 = ["Adam": 20]
dict2 = ["Ben": 22, "Charlie": 25]

Общая форма объявления и инициализации словаря:

var dict3: Dictionary<String, Int> = ["Adam": 22, "Ben": 25]
var dict4: [String: Int] = ["Adam": 22, "Ben": 25]

Объявление и инициализация словаря без указания типа данных (Создание словаря с литералом словаря):

var dict5 = Dictionary<Int, String>(dictionaryLiteral: (10, "Gin"), (20, "Henry"), (30, "Isaak"))
var dict6 = [Int: String](dictionaryLiteral: (1, "John"), (2, "Kim"), (3, "Lasly"))

Объявление и инициализация словаря без указания типа данных (Создание словаря с литералом словаря):

var dict7 = [2: "Mary", 3: "Nansy", 6: "Olaph"]

Тип коллекции будет определен исходя из типа данных элементов, с которыми коллекция инициализируется. Такая сокращенная форма используется чаще всего.

Объявление пустого словаря:

let dict8 = [Int: String]()
print(dict8)

// Output
// [:]

Еще один способ объявления пустого словаря:

let dict9: [Int: String] = [:]
print(dict9)

// Output
// [:]

Просто присвоить пустой словарь без указания типа - нельзя, так как будет ошибка: нельзя определить тип данных элементов словаря.

Работа со словарем

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

Работа с элементами словаря: добавление, получение, изменение и удаление элементов словаря

Расположение элементов в словаре неупорядочено. То есть, пары “ключ-значение” в словаре не имеют порядка по умолчанию или какой-либо сортировки.

Для примера работы со словарем создаем словарь “scores”:

var scores = ["Bob": 42, "Alice": 10, "Daisy": 33]

Добавление элемента словаря

Для добавления нового элемента (пары “ключ-значение”) в словарь, необходимо обратится к словарю и в квадратных скобках указать “ключ”, и присвоить ему значение:

scores["Finn"] = 99

Приведенный выше код добавляет новый ключ "Finn" в словарь scores со значением 99.

Тип ключа и значения должны соответствовать типу данных словаря. Для словаря scores типы данных для ключей - “String” и для значений - “Int”.

Удаление элементов словаря

Удаление элемента из словаря выполняется путем присвоения ему значения “nil”:

scores["Alice"] = nil

Кроме того, можно удалить пару “ключ-значение” из словаря с помощью метода removeValue(forKey:). Этот метод удаляет пару “ключ-значение”, если она существует, и затем - возвращает значение, либо возвращает “nil”, если значения не существует:

if let removedValue = scores.removeValue(forKey: "Alice") {
  print("The removed score value is \(removedValue).")
} else {
  print("The dictionary does not contain a value for Alice.")
}

// Output
// Выведет "The removed score value is 101."

Метод removeValue(forKey:Key) удаляет указанный ключ и связанное с ним значение из словаря. При этом он также возвращает удаленное значение (опционального типа), если оно существует или nil - если его нет:

var hues = ["Heliotrope": 296, "Coral": 16, "Aquamarine": 156]
if let value = hues.removeValue(forKey: "Coral") {
    print("The value \(value) was removed.")
}

// Output
// The value 16 was removed.

В этом примере, функция removeValue(forKey: "Coral") удалила элемент 16 с ключом "Coral" и вернула его значение, которое было использовано для вывода в консоль.

Получение и изменение элементов словаря

Для получения значения из словаря необходимо обратиться к словарю и в квадратных скобках указать необходимый ключ:

let score = scores["Bob"]

При этом тип полученного значения из словаря будет опциональным. Почему это происходит? Потому что, нельзя быть уверенным, что элемент с данным ключом существует в словаре. В случае, когда используется ключ, которого нет в словаре, возвращается значение nil.

В примере, для словаря score значение будет с типом Int? (опциональный тип Int):

var scores = [
    "Bob": 42,
    "Alice": 10,
    "Daisy": 33
]
print(scores)

// Output
// ["Daisy": 33, "Bob": 42, "Alice": 10]

let score_bob = scores["Bob"]
print(score_bob)

// Output
// Optional(42)
 
let score_unknown = scores["Arthur"]
print(score_unknown)

// Output
// nil

Значение score_bob имеет тип Optional(42) или Int?. Значение score_unknown - nil.

Можно использовать опциональное связывание при получении значений из словаря:

if let score = scores["Bob"] {
    print(score)
}

Также можно изменить словарь с помощью похожего синтаксиса:

var scores = ["Bob": 42, "Alice": 10, "Daisy": 33]

scores["Alice"] = 101
print(scores)

// Output
// ["Alice": 101, "Bob": 42, "Daisy": 33]

Значение для ключа "Alice" теперь изменено на 101. Синтаксис для добавления, удаления, изменения и получения элементов из словаря по сути одинаков.

Но что, если необходимо узнать, была ли пара ключ-значение добавлена ​​в словарь или изменена по сравнению с существующей парой ключ-значение? Здесь поможет функция (метод словаря) updateValue(_:forKey:).

Данный метод можно использовать в качестве альтернативы индексам, чтобы установить или обновить значение для определенного ключа. Как и с примерами работы с индексами (ключами) выше, метод updateValue(_:forKey:) устанавливает значение для ключа, если оно не существует, или обновляет значение, если этот ключ уже существует. Однако, в отличие от индексов, метод updateValue(_:forKey:) возвращает старое значение после выполнения обновления. Это позволяет проверить, состоялось ли обновление или нет.

if let oldScore = scores.updateValue(99, forKey: "Daisy") {
    print("Счет Daisy был: \(oldScore)")
} else {
    print("Нет игрока с данным именем...")
}

Данный метод возвращает одно из двух значений:

  • Возвращает “nil”, когда ключ не присутствовал в словаре до его установки;
  • Возвращает опциональное текущее значение, то есть «старое значение» ключа перед его установкой, если ключ присутствует в словаре.

Метод updateValue(_:forKey:) возвращает опциональное значение, соответствующее типу значения словаря. Например, для словаря, который хранит String значения, метод возвратит String? тип, или “опциональный String”.

Работа со словарем

Также как и у массивов, можно узнать количество элементов в словаре через его read-only свойство count:

print("The dictionary contains \(scores.count) items.")

// Output
// Выведет "The dictionary contains 2 items."

Логическое свойство isEmpty можно использовать в качестве быстрого способа узнать, является ли свойство count равным 0 - если словарь пустой, то вернет true:

if scores.isEmpty {
  print("The dictionary is empty.")
} else {
  print("The dictionary is not empty.")
}

// Output
// Выведет "The dictionary is not empty."

Итерация по словарю

Можно сделать итерацию по парам “ключ-значение” в словаре с помощью “for-in” цикла. Каждое значение в словаре возвращается как кортеж (ключ, значение), и можно разложить части кортежа по временным константам или переменным в рамках итерации:

for (scoresName, scoresNumber) in scores {
  print("\(scoresName): \(scoresNumber)")
}

Также, можно получить коллекцию ключей или значений словаря через обращение к его свойствам keys и values:

for scoresName in scores.keys {
  print("Player name: \(scoresName)")
}

for scoresNumber in scores.values {
  print("Player score: \(scoresNumber)")
}

Если необходимо использовать ключи или значения словаря вместе с каким-либо API, которое принимает объект по типу “Array”, то можно инициализировать новый массив с помощью свойств keys и values:

let scoresName = [String](scores.keys)
let scoresNumber = [String](scores.values)

Тип словаря в Swift является неупорядоченной коллекцией. Для итерации по ключам или значениям словаря в определенной последовательности, следует использовать метод sorted() для свойств keys или values словаря.

Пример:

var scores = ["Bob": 42, "Alice": 10, "Daisy": 33]
print(scores.keys)

// Output
// ["Bob", "Daisy", "Alice"]

print(scores.keys.sorted())

// Output
// ["Alice", "Bob", "Daisy"]

for (scoresName, scoresNumber) in scores {
  print("\(scoresName): \(scoresNumber)")
}

// Output
// Bob: 42
// Daisy: 33
// Alice: 10

for (scoresName, scoresNumber) in scores.sorted(by: <) {
  print("\(scoresName): \(scoresNumber)")
}

// Output
// Alice: 10
// Bob: 42
// Daisy: 33

Создание словаря из массивов

С помощью встроенной глобальной функции zip() можно соединить два массива в объект Zip2Sequence, который затем передается в инициализатор типа Dictionary:

let countries = ["Iran", "Iraq", "Syria", "Lebanon"]
let capitals = ["Tehran", "Bagdad", "Damascus", "Beirut"]
var seq = zip(countries, capitals)
 
var dict = Dictionary(uniqueKeysWithValues:seq)
for (key, value) in dict {
    print("\(key) - \(value)")
}

В данном случае каждый элемент из массива countries последовательно сопоставляется с соответствующим элементом из массива capitals. Затем результат через параметр uniqueKeysWithValues передается в инициализатор Dictionary. И таким образом образуется словарь. Результат программы:

Iran - Tehran
Iraq - Bagdad
Syria - Damascus
Lebanon - Beirut

Стоит учитывать, что если в обоих массивах есть повторяющиеся значения, то подобный способ их объединения завершится с ошибкой (“Fatal error: Duplicate values for key: …”), ведь в словаре все ключи должны быть уникальными.

И для этого надо использовать другую форму инициализатора Dictionary:

let countries = ["Iran", "Iraq", "Syria", "Lebanon", "Iran"]
let capitals = ["Tehran", "Bagdad", "Damascus", "Beirut", "Tehran"]
var seq = zip(countries, capitals)
 
var dict = Dictionary(seq, uniquingKeysWith:{return $1})
 
for (key, value) in dict {
    print("\(key) - \(value)")
}

В данном случае в инициализатор в качестве первого параметра опять же передается объединенные последовательности. А второй параметр uniquingKeysWith указывает на функцию, которая получает все значения из второго массива, которые соответствуют повторяющемуся ключу. В данном случае это два элемента. И затем надо возвратить какой-нибудь результат. Здесь просто возвращается значение второго параметра:

// Output
// Iran - Tehran
// Lebanon - Beirut
// Syria - Damascus
// Iraq - Bagdad

Добавление в словарь значение nil

Значение nil можно добавить при инициализации словаря:

var dict = [2: "Mary", 3: "Nansy", 6: "Olaph", 7: nil]
print(dict)

// Output
// [6: Optional("Olaph"), 3: Optional("Nansy"), 2: Optional("Mary"), 7: nil]

Но если добавить элемент обратившись по ключу и присвоить значение, то это действие удалит элемент из словаря:

dict[6] = nil
print(dict)

// Output
// [2: Optional("Mary"), 7: nil, 3: Optional("Nansy")]

Так же будет, если обратиться у элементу, у которого значение уже nil - этот элемент будет просто удален из словаря.

Чтоб добавить элемент со значением nil, необходимо задать переменную соответствующего типа со значением nil. После этого добавить эту переменную в качестве значения:

var nilValue: String?
dict[8] = nilValue
print(dict)

// Output
// [2: Optional("Mary"), 6: Optional("Olaph"), 8: nil, 3: Optional("Nansy"), 7: nil]

Либо используя метод updateValue(_:forKey:):

var dict: [Int: Int?] = [1: 345, 2: 45, 3: nil, 4: 1000]
print(dict)

// Output
// [3: nil, 4: Optional(1000), 1: Optional(345), 2: Optional(45)]

dict.updateValue(nil, forKey: 5)
print(dict)

// Output
// [5: nil, 3: nil, 4: Optional(1000), 1: Optional(345), 2: Optional(45)]

В обоих этих примерах можно увидеть, что к словарям было добавлено по одному элементу, у которых в качастве значения было присвоено “nil”.


Еще полезные ссылки

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

Ссылки на официальную документацию: