Массив — это один из наиболее важных представителей коллекции, который вы с большой долей вероятности будете использовать при реализации функционала любой программы. Уделите особое внимание приведенному в этой главе материалу.
Массив (Array) — это упорядоченная коллекция однотипных элементов, для доступа к которым используются целочисленные индексы. Упорядоченной называется коллекция, в которой элементы располагаются в порядке, определенном разработчиком.
Каждый элемент массива — это пара «индекс—значение».
Индекс элемента массива — это целочисленное значение, используемое для доступа к значениям элемента. Индексы генерируются автоматически при добавлении новых элементов. Индексы в массивах начинаются с нуля (не с единицы!). К примеру, у массива, содержащего 5 элементов, индекс первого равен 0, а последнего — 4. Индексы всегда последовательны и неразрывны. При удалении некоторого элемента, индексы всех последующих уменьшатся на единицу, чтобы обеспечить неразрывность.
Значение элемента массива — это произвольное значение определенного типа данных. Как говорилось ранее, значения доступны по соответствующим им индексам. Значения всех элементов массива должны быть одного и того же типа данных.
Значение массива задается с помощью литерала массива, в котором через запятую перечисляются значения элементов.
Синтаксис
[значение_1, значение_2, ..., значение_N]
• значение: Any — значение очередного элемента массива, может быть произвольного типа данных.
Литерал массива возвращает массив, состоящий из N элементов, значения которых имеют один и тот же тип данных. Литерал обрамляется квадратными скобками, а значения элементов в нем отделяются друг от друга запятыми. Массив может содержать любое количество элементов одного типа. Тип данных значений — произвольный, определяется вами в соответствии с контекстом задачи. Индексы элементов определяются автоматически в зависимости от порядка следования элементов.
Пример
[1,1,2,3,5,8,12]
В данном примере тип данных значения каждого элемента массива — Int. Массив имеет 7 элементов. Первые два (с индексами 0 и 1) имеют одинаковые значения — 1. Значение последнего элемента массива с индексом 6 равно 12.
Массивы, как и любые другие значения, могут быть записаны в параметры. При использовании константы инициализированный массив является неизменяемым. Пример создания изменяемого и неизменяемого массива приведен в листинге 9.1.
Листинг 9.1
// неизменяемый массив
// с элементами типа String
let alphabetArray = ["a", "b", "c"]
// изменяемый массив
// с элементами типа Int
var mutableArray = [2, 4, 8]1
В данном листинге массивы с помощью литералов проинициализированы переменным alphabetArray и mutableArray. Неизменяемый массив alphabetArray предназначен для хранения значений типа String, а изменяемый массив mutableArray — для хранения элементов типа Int. Оба массива содержат по три элемента. Индексы соответствующих элементов обоих массивов имеют значения 0, 1 и 2.
ПРИМЕЧАНИЕ Отмечу, что массивом принято называть как само значение, представленное в виде литерала, так и параметр, в который это значение записано.
Это можно отнести также и к кортежам, словарям, наборам (их нам только предстоит изучить) и другим структурам языка программирования.
Для создания массива помимо передачи литерала массива можно использовать специальные глобальные функции, одной из которых является Array(arrayLiteral:).
Синтаксис
Array(arrayLiteral: значение_1, значение_2, ...,значение_N)
• значение: Any — значение очередного элемента массива, может быть произвольного типа данных.
Функция возвращает массив, состоящий из N элементов, значения которых имеют один и тот же тип данных. Значения произвольного типа передаются в виде списка в качестве входного параметра arrayLiteral. Каждое значение отделяется от последующего запятой.
Пример
Array(arrayLiteral: 1, 1, 2, 3, 5, 8, 12)
В данном примере в результате выполнения функции будет возвращен массив, состоящий из 7 целочисленных элементов.
Пример создания массива с использованием данной функции приведен в листинге 9.2.
Листинг 9.2
// создание массива с помощью передачи списка значений
let newAlphabetArray = Array(arrayLiteral: "a", "b", "c")
newAlphabetArray // ["a", "b", "c"]
В результате в переменной newAlphabetArray будет находиться массив строковых значений. Индекс первого элемента — 0, а последнего — 2.
ПРИМЕЧАНИЕ Возможно, вы обратили внимание на то, что, по логике, мы передали значение входного аргумента arrayLiteral как «a» и еще два безымянных аргумента. На самом деле все это значение одного аргумента arrayLiteral. О том, как Swift позволяет выполнять такой прием, будет рассказано в одной из следующих глав.
Также для создания массива можно использовать глобальную функцию Array(_:), которой в качестве входного аргумента необходимо передать произвольную последовательность (Sequence).
Синтаксис
Array(последовательность_значений)
• последовательность_значений: Sequence — последовательность элементов, которая будет преобразована в массив.
Функция возвращает массив, состоящий из элементов, входящих в переданную последовательность. Последовательность значений, переданная в функцию, может быть представлена в виде любой доступной в Swift последовательности (Sequence). Каждому ее элементу будет присвоен уникальный целочисленный индекс.
Пример
Array(0...10)
Диапазон 0...10 является последовательностью значений, а значит, может быть передан в Array(_:) для формирования массива. В результате выполнения функции будет возвращен массив, состоящий из 11 целочисленных элементов, входящих в диапазон 0...10.
Мы познакомились лишь с двумя видами последовательностей (Sequence): диапазоны и массивы (не забывайте, что Collection — это расширенный Sequence). Рассмотрим пример использования диапазона для создания массива с помощью функции Array(_:). Создадим массив, состоящий из 10 значений от 0 до 9 (листинг 9.3).
Листинг 9.3
// создание массива с помощью оператора диапазона
let lineArray = Array(0...9)
lineArray // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Каждый элемент последовательности 0...9 получит целочисленный индекс. В данном примере индекс будет совпадать со значением элемента: так, первый элемент будет иметь индекс 0 и значение 0, а последний — индекс 9 и значение 9.
ПРИМЕЧАНИЕ Обратите внимание, что Array(_:) в качестве входного параметра может принимать любую последовательность. Но будьте осторожны с бесконечными диапазонами (тип PartialRangeFrom), так как в этом случае операция создания массива не завершится никогда, программа займет всю свободную память, после чего зависнет.
Помимо рассмотренных способов, существует возможность создания массива с помощью функции Array(repeating:count:), возвращающей массив, состоящий из указанного количества одинаковых (с одним и тем же значением) элементов.
Синтаксис
Array(repeating: значение, count: количество)
• значение: Any — значение произвольного типа, которое будет повторяться в каждом элементе массива.
• количество: Int — целое число, определяющее количество повторений произвольного значения.
Функция возвращает массив, состоящий из одинаковых значений, повторяющихся count раз. Аргумент repeating определяет значение, которое будет присутствовать в массиве столько раз, сколько указано в качестве значения аргумента count.
Пример
Array(repeating: "Ура", count: 3)
В результате выполнения функции будет возвращен массив, состоящий из трех строковых элементов с одинаковым значением "Ура". Индекс первого элемента будет равен 0, а последнего — 2.
Рассмотрим пример использования функции Array(repeating:count:) для создания массива (листинг 9.4).
Листинг 9.4
// создание массива с повторяющимися значениями
let repeatArray = Array(repeating: "Swift", count: 5)
repeatArray // ["Swift", "Swift", "Swift", "Swift", "Swift"]
ПРИМЕЧАНИЕ Наверняка вы обратили внимание на то, что я как будто бы трижды описал одну и ту же функцию с именем Array. Да, действительно, имя во всех трех вариантах одно и то же, но оно отличается набором входных параметров — и для Swift это разные значения. Подробнее этот вопрос будет рассмотрен в ходе изучения функций.
Синтаксис Swift позволяет использовать индексы для доступа к значениям элементов, которые указываются в квадратных скобках после массива (листинг 9.5).
Листинг 9.5
// неизменяемый массив
let alphabetArray = ["a", "b", "c"]
// изменяемый массив
var mutableArray = [2, 4, 8]
// доступ к элементам массивов
alphabetArray[1] // "b"
mutableArray[2] // 8
С помощью индексов можно получать доступ к элементам массива не только для чтения, но и для изменения значений (листинг 9.6).
Листинг 9.6
// изменяемый массив
var mutableArray = [2, 4, 8]
// изменение элемента массива
mutableArray[1] = 16
// вывод нового массива
mutableArray // [2, 16, 8]
ПРИМЕЧАНИЕ Попытка модификации массива, хранящегося в константе, вызовет ошибку.
При использовании оператора диапазона можно получить доступ сразу к множеству элементов в составе массива, то есть к его подмассиву. Данный оператор должен указывать на индексы крайних элементов выделяемого множества. В листинге 9.7 приведен пример замены двух элементов массива на один новый с помощью использования оператора диапазона (листинг 9.7).
Листинг 9.7
// изменяемый массив
var stringsArray = ["one", "two", "three", "four"]
// заменим несколько элементов
stringsArray[1...2] = ["five"]
stringsArray // ["one", "five", "four"]
stringsArray[2] // "four"
После замены двух элементов с индексами 1 и 2 на один со значением "five" индексы всех последующих элементов перестроились. Вследствие этого элемент "four", изначально имевший индекс 3, получил индекс 2, так как стал третьим элементом массива.
ПРИМЕЧАНИЕ Индексы элементов массива всегда последовательно идут друг за другом без разрывов в значениях, при необходимости они перестраиваются.
Тип данных массива основан на типе данных значений его элементов. Существует полная и краткая формы записи типа данных массива.
Синтаксис
Полная форма записи:
Array<T>
Краткая форма записи:
[T]
• T: Any — наименование произвольного типа данных значений элементов массива.
Массив с типом данных Array<T> должен состоять из элементов, значения которых имеют тип данных T. Обе представленные формы определяют один и тот же тип массива, указывая, что его элементы будут иметь значение типа T.
Рассмотрим пример из листинга 9.7a.
Листинг 9.7a
// Массив с типом данных [String] или Array<String>
var firstAr = Array(arrayLiteral: «a», «b», «c») // [«a», «b», «c»]
type(of: firstAr) // Array<String>.Type
// Массив с типом данных [Int] или Array<Int>
var secondAr = Array(1..<5) // [1, 2, 3, 4]
type(of: secondAr) // Array<Int>.Type
В данном листинге создаются два массива, тип данных которых определен неявно (на основании переданного значения).
Также тип массива может быть задан явно. В этом случае необходимо указать тип массива через двоеточие после имени параметра.
Синтаксис
Полная форма записи:
var имяМассива: Array<T> = литерал_массива
Краткая форма записи:
var имяМассива: [T] = литерал_массива
В обоих случаях объявляется массив, элементы которого должны иметь указанный тип данных. Тип массива в этом случае будет равен [T] (с квадратными скобками) или Array<T>. Напомню, что оба обозначения эквивалентны. Типом каждого отдельного элемента таких массивов является T (без квадратных скобок).
Пример
var arrayOne: Array<Character> = ["a", "b", "c"]
var arrayTwo: [Int] = [1, 2, 5, 8, 11]
Массив является значимым типом (value type), а не ссылочным (reference type). Это означает, что при передаче значения массива из одного параметра в другой создается его копия, редактирование которой не влияет на исходную коллекцию.
В листинге 9.7б показан пример создания копии исходного массива с последующей заменой одного из его элементов.
Листинг 9.7б
// исходный массив
var parentArray = ["one", "two", "three"]
// создаем копию массива
var copyParentArray = parentArray
copyParentArray // ["one", "two", "three"]
// изменяем значение в копии массива
copyParentArray[1] = "four"
// выводим значение массивов
parentArray // ["one", "two", "three"]
copyParentArray // // ["one", "four", "three"]
При передаче исходного массива в новый параметр создается его полная копия. При изменении данной копии значение исходного массива остается прежним.
ПРИМЕЧАНИЕ Будьте внимательны, так как иногда можно случайно создать многочисленные копии одного и того же массива, что приведет к росту бесцельно используемой памяти.
Массив может иметь пустое значение, то есть не иметь элементов (это словно строка без символов). Для создания пустого массива можно использовать один из следующих способов:
• явно указать тип создаваемого массива и передать ему значение [];
• использовать специальную функцию [типДанных](), где типДанных определяет тип значений элементов массива.
Оба способа создания пустого массива продемонстрированы в листинге 9.8.
Листинг 9.8
/* объявляем массив с пустым значением
с помощью переданного значения */
var emptyArray: [String] = [] // []
/* объявляем массив с пустым значением
с помощью специальной функции */
var anotherEmptyArray = [String]() // []
В результате создаются два пустых массива, emptyArray и anotherEmptyArray, уже с инициализированными значениями (хотя и не содержащими элементов).
Массивы, так же как и значения фундаментальных типов данных, можно сравнивать друг с другом. Два массива являются эквивалентными:
• если количество элементов в сравниваемых массивах одинаково;
• если каждая соответствующая пара элементов эквивалентна (имеет одни и те же типы данных и значения).
Рассмотрим пример сравнения двух массивов (листинг 9.9).
Листинг 9.9
/* три константы, которые
cтанут элементами массива */
let a1 = 1
let a2 = 2
let a3 = 3
var someArray = [1, 2, 3]
someArray == [a1, a2, a3] // true
Несмотря на то что в массиве [a1, a2, a3] указаны не значения, а константы, содержащие эти значения, условия эквивалентности массивов все равно выполняются.
ПРИМЕЧАНИЕ Если в вашем коде есть строка import Foundation или import UIKit (одну из них Xcode добавляет автоматически при создании нового playground), то при попытке произвести сравнение массивов с помощью кода
someArray == [1,2,3]
может быть отражена ошибка. И для этого есть две причины:
• С помощью литерала массива, как вы узнаете далее, также могут быть созданы и другие виды коллекций.
• Директива import подключает внешнюю библиотеку функций, в которой существует не один оператор эквивалентности (==), а целое множество. Все они позволяют сравнивать разные виды коллекций.
В результате Swift не может определить, какая именно коллекция передана в правой части, и вызывает ошибку, сообщающую об этом.
Со значением массива, как и со значениями фундаментальных типов данных, можно проводить различные операции. Одной из них является операция слияния, при которой значения двух массивов сливаются в одно, образуя новый массив. Обратите внимание на несколько моментов:
• результирующий массив будет содержать значения из обоих массивов, но индексы этих значений могут не совпадать с родительскими;
• значения элементов подлежащих слиянию массивов должны иметь один и тот же тип данных.
Операция слияния производится с помощью уже известного оператора сложения (+), как показано в листинге 9.10.
Листинг 9.10
// создаем три массива
let charsOne = ["a", "b", "c"]
let charsTwo = ["d", "e", "f"]
let charsThree = ["g", "h", "i"]
// создаем новый массив слиянием двух
var alphabet = charsOne + charsTwo
// сливаем новый массив с третьим
alphabet += charsThree
alphabet // ["a", "b", "c", "d", "e", "f", "g", "h", "i"]
Полученное в результате значение массива alphabet собрано из трех других массивов, причем порядок элементов соответствует порядку элементов в исходных массивах.
Элементами массива могут быть значения не только фундаментальных типов, но и любых других типов данных, включая сами массивы. Массивы, элементами которых также являются массивы, называются многомерными. Необходимо обеспечить единство типа всех вложенных массивов.
Рассмотрим пример в листинге 9.11.
Листинг 9.11
var arrayOfArrays = [[1,2,3], [4,5,6], [7,8,9]]
В данном примере создается коллекция, содержащая множество массивов типа [Int] в качестве своих элементов. Типом основного массива arrayOfArrays является [[Int]] (с удвоенными квадратными скобками с каждой стороны) — это значит, что это массив массивов.
Для доступа к элементу многомерного массива необходимо указывать несколько индексов (листинг 9.12).
Листинг 9.12
arrayOfArrays = [[1,2,3], [4,5,6], [7,8,9]]
// получаем вложенный массив
arrayOfArrays[2] // [7, 8, 9]
// получаем элемент вложенного массива
arrayOfArrays[2][1] // 8
Конструкция arrayOfArrays[2] возвращает третий вложенный элемент массива arrayOfArrays, а arrayOfArrays[2][1], использующая два индекса, возвращает второй элемент подмассива, содержащегося в третьем элементе массива arrayOfArrays.
Массивы — очень функциональные элементы языка. Об этом позаботились разработчики Swift, предоставив набор свойств и методов, позволяющих значительно расширить их возможности в сравнении с другими языками.
Свойство count возвращает количество элементов в массиве (листинг 9.13).
Листинг 9.13
var someArray = [1, 2, 3, 4, 5]
// количество элементов в массиве
someArray.count // 5
Если значение свойства count равно нулю, то и свойство isEmpty возвращает true (листинг 9.14).
Листинг 9.14
var emptyArray: [Int] = []
emptyArray.count // 0
emptyArray.isEmpty // true
Вы можете использовать свойство count для того, чтобы получить требуемые элементы массива (листинг 9.15).
Листинг 9.15
var numArray = [1, 2, 3, 4, 5]
// количество элементов в массиве
var sliceOfArray = numArray[numArray.count-3...numArray.count-1] // [3, 4, 5]
Другим средством получить множество элементов массива является метод suffix(_:) — в качестве входного параметра ему передается количество элементов, которые необходимо получить. Элементы отсчитываются с последнего элемента массива (листинг 9.16).
Листинг 9.16
let subArray = numArray.suffix(3) // [3, 4, 5]
Свойства first и last возвращают первый и последний элементы массива (листинг 9.17).
Листинг 9.17
// возвращает первый элемент массива
numArray.first // 1
// возвращает последний элемент массива
numArray.last // 5
С помощью метода append(_:) можно добавить новый элемент в конец массива (листинг 9.18).
Листинг 9.18
numArray // [1, 2, 3, 4, 5]
numArray.append(6) // [1, 2, 3, 4, 5, 6]
Если массив хранится в переменной (то есть является изменяемым), то метод insert(_:at:) вставляет в массив новый одиночный элемент с указанным индексом (листинг 9.19).
Листинг 9.19
numArray // [1, 2, 3, 4, 5, 6]
// вставляем новый элемент в середину массива
numArray.insert(100, at: 2) // [1, 2, 100, 3, 4, 5, 6]
При этом индексы массива пересчитываются, чтобы обеспечить их последовательность.
Так же как в случае изменения массива, методы remove(at:), removeFirst() и removeLast() позволяют удалять требуемые элементы. При этом они возвращают значение удаляемого элемента (листинг 9.20).
Листинг 9.20
numArray // [1, 2, 100, 3, 4, 5, 6]
// удаляем третий элемент массива (с индексом 2)
numArray.remove(at: 2) // 100
// удаляем первый элемент массива
numArray.removeFirst() // 1
// удаляем последний элемент массива
numArray.removeLast() // 6
/* итоговый массив содержит
всего четыре элемента */
numArray // [2, 3, 4, 5]
После удаления индексы оставшихся элементов массива перестраиваются. В данном случае в итоговом массиве numArray остается всего четыре элемента с индексами 0, 1, 2 и 3.
Для редактирования массива также можно использовать методы dropFirst(_:) и dropLast(_:), возвращающие новый массив, в котором отсутствует несколько первых или последних элементов, но при этом не изменяющие исходную коллекцию. Если в качестве входного аргумента ничего не передавать, то из результата удаляется один элемент, в противном случае — столько элементов, сколько передано (листинг 9.21).
Листинг 9.21
numArray // [2, 3, 4, 5]
// удаляем последний элемент
numArray.dropLast() // [2, 3, 4]
// удаляем три первых элемента
var anotherNumArray = numArray.dropFirst(3)
anotherNumArray // [5]
numArray // [2, 3, 4, 5]
При использовании данных методов основной массив numArray, с которым выполняются операции, не меняется. Они лишь возвращают получившееся значение, которое при необходимости может быть записано в новый параметр.
Метод contains(_:) определяет факт наличия некоторого элемента в массиве и возвращает Bool в зависимости от результата (листинг 9.22).
Листинг 9.22
numArray // [2, 3, 4, 5]
// проверка существования элемента
let resultTrue = numArray.contains(4) // true
let resultFalse = numArray.contains(10) // false
Для поиска минимального или максимального элемента в массиве применяются методы min () и max (). Данные методы работают только в том случае, если элементы массива можно сравнить между собой (листинг 9.23).
Листинг 9.23
let randomArray = [3, 2, 4, 5, 6, 4, 7, 5, 6]
// поиск минимального элемента
randomArray.min() // 2
// поиск максимального элемента
randomArray.max() // 7
Чтобы изменить порядок следования всех элементов массива на противоположный, используйте метод reverse(), как показано в листинге 9.24.
Листинг 9.24
var myAlphaArray = ["a", "bb", "ccc"]
myAlphaArray.reverse()
myAlphaArray // ["ccc", "bb", "a"]
Методы sort() и sorted() позволяют отсортировать массив по возрастанию. Разница между ними состоит в том, что sort() сортирует саму последовательность, для которой он вызван, а sorted(), заменяя оригинальный массив, возвращает отсортированную коллекцию (листинг 9.24а).
Листинг 9.24а
// исходная неотсортированная коллекция
var unsortedArray = [3, 2, 5, 22, 8, 1, 29]
// метод sorted() возвращает отсортированную последовательность
// при этом исходный массив не изменяется
var sortedArray = unsortedArray.sorted()
unsortedArray // [3, 2, 5, 22, 8, 1, 29]
sortedArray // [1, 2, 3, 5, 8, 22, 29]
//метод sort() изменяет исходный массив
unsortedArray.sort()
unsortedArray // [1, 2, 3, 5, 8, 22, 29]
Способ сортировки по убыванию значений будет рассмотрен далее в книге, в главе 16.
ПРИМЕЧАНИЕ Разработчики Swift с умом подошли к именованию доступных методов. В большинстве случаев если какой-либо метод заканчивается на –ed, то он, не трогая исходное значение, возвращает его измененную копию. Аналогичный метод без –ed на конце модифицирует саму последовательность.
При использовании некоторых из описанных ранее свойств и методов возвращается не массив, а значение некоего типа данных ArraySlice. Это происходит, к примеру, при получении части массива с помощью оператора диапазона или при использовании методов dropFirst() и dropLast() (листинг 9.25).
Листинг 9.25
// исходный массив
var arrayOfNumbers = Array(1...10)
// его тип данных - Array<Int>
type(of: arrayOfNumbers) // Array<Int>.Type
arrayOfNumbers // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// получим часть массива (подмассив)
var slice = arrayOfNumbers[4...6]
slice // [5, 6, 7]
// его тип данных отличается от типа исходного массива
type(of: slice) // ArraySlice<Int>.Type
Переменная slice имеет незнакомый вам тип данных ArraySlice<Int>. Если Array — это упорядоченное множество элементов, то ArraySlice — это его подмножество.
Но в чем необходимость создавать новый тип данных? Почему просто не возвращать значение типа Array?
Дело в том, что ArraySlice не копирует исходный массив, а ссылается на его подмножество (если быть точным, то ссылается на ту же самую область памяти). Это сделано для экономии ресурсов компьютера, так как не создаются излишние копии одних и тех же данных.
При работе с ArraySlice нужно быть предельно внимательными, тем более что Apple рекомендует максимально ограничить его использование. Дело в том, что если имеется ArraySlice, а вы удаляете параметр, хранящий исходный массив, то на самом деле незаметно для вас он все еще будет находиться в памяти, так как ссылка на его элементы все еще существует.
При работе с ArraySlice вам доступны те же возможности, что и при работе с массивом, но в большинстве случаев все же потребуется преобразовать ArraySlice в Array. Для этого можно использовать уже знакомую вам функцию Array(_:), где в качестве входного параметра передается коллекция типа ArraySlice (листинг 9.26).
Листинг 9.26
type(of: slice) // ArraySlice<Int>.Type
var arrayFromSlice = Array(slice)
type(of: arrayFromSlice) // Array<Int>.Type
Стоит обратить внимание на то, что индексы ArraySlice соответствуют индексам исходной коллекции, то есть они не обязательно начинаются с 0 (листинг 9.27).
Листинг 9.27
// исходный массив
arrayOfNumbers // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// его срез, полученные в одном из предыдущих листингов
slice // [5, 6, 7]
// отдельный элемент
arrayOfNumbers[5] // 6
slice[5] // 6