В главе 5 книги мы познакомились со строковыми типами String и Character, позволяющими работать с текстовыми данными в приложениях, и получили первые и основные знания о стандарте Unicode. Напомню, что строки состоят из отдельных символов, а для каждого символа существует кодовая точка (уникальная последовательность чисел из стандарта Unicode, соответствующая данному символу). Кодовые точки могут быть использованы для инициализации текстовых данных в составе юникод-скаляров (служебных конструкций \n{}). В этой главе будет подробно рассказано о том, как функционируют строковые типы в Swift.
В самом простом смысле, концептуально, строка в Swift — это коллекция. Да, именно коллекция! String соответствует требованиям протокола Collection и является коллекцией, подобной массивам, наборам и словарям, но со своими особенностями, которые мы сейчас обсудим.
Во-первых, так как String — коллекция, то вам доступно большинство возможностей, характерных для коллекций. К примеру, можно получить количество всех элементов с помощью свойства count (листинг 12.1) или осуществить их перебор с помощью оператора for-in (с ним мы познакомимся несколько позже).
Листинг 12.1
var str = "Hello!"
str.count // 6
Переменная str имеет строковое значение, состоящее из 6 символов, что видно, в том числе, по выводу свойства count. Возможность использования данного свойства вы могли видеть и у других видов коллекций.
Во-вторых, раз значение типа String — это коллекция, то возникает вопрос: чем являются элементы этой коллекции?
Каждый элемент строкового значения типа String представляет собой значение типа Character, то есть отдельный символ, который может быть представлен с помощью юникод-скаляра (конструкции \u{}, включающей кодовую точку).
В-третьих, значение типа String — это упорядоченная коллекция. Элементы в ней находятся именно в том порядке, который определил разработчик при инициализации значения.
На данный момент можно сказать, что строка (значение типа String) — это упорядоченная коллекция, каждый элемент которой представляет собой значение типа Character.
Так как String является упорядоченной коллекцией, было бы правильно предположить, что ее элементы коллекции имеют индексы, по которым они сортируются и выстраиваются в последовательность, а также по которым к этим элементам можно обратиться (для чтения, изменения или удаления). И правда, довольно часто возникает задача получить определенный символ в строке. Если у вас есть опыт разработки на других языках программирования, то, возможно, вам в голову пришла идея попробовать использовать целочисленные индексы для доступа к элементам строки (точно как в массивах). Но, неожиданно, в Swift такой подход приведет к ошибке (листинг 12.2).
Листинг 12.2
str[2] // error: 'subscript' is unavailable: cannot subscript String with an Int
Но в чем проблема? Почему такой простой вариант доступа не может быть использован в современном языке программирования? Чтобы ответить на этот вопрос, нам потребуется вновь поговорить о стандарте Unicode и о работе с ним в Swift.
Значение типа String — это коллекция, каждый элемент которой представлен индексом (являющимся значением пока еще не рассмотренного типа данных) и значением типа Character. Как мы видели ранее, каждый отдельный символ может быть представлен в виде юникод-скаляра. В листинге 12.3 параметру типа Character инициализируется значение через юникод-скаляр.
Листинг 12.3
var char: Character = "\u{E9}"
char // "é"
Символ é (латинская e со знаком ударения) представлен в данном примере с использованием кодовой точки E9 (или 233 в десятеричной системе счисления). Но удивительным становится тот факт, что существует и другой способ написания данного символа: с использованием двух юникод-скаляров (а соответственно и двух кодовых точек). Первый будет описывать латинскую букву e (\u{65}), а второй символ ударения (\u{301}) (листинг 12.4).
Листинг 12.4
var anotherChar: Character = "\u{65}\u{301}"
anotherChar // "é"
И так как тип данных этих параметров — Character, ясно, что в обоих случаях для Swift значение состоит из одного символа. Если провести сравнение значений этих переменных, то в результате будет возвращено true, что говорит об их идентичности (листинг 12.5).
Листинг 12.5
char == anotherChar //true
Выглядит очень странно, согласны? Но дело в том, что в переменной anotherChar содержится комбинированный символ, состоящий из двух кодовых точек, но по сути являющийся одним полноценным.
Существование таких комбинированных символов становится возможным благодаря специальным символам, модифицирующим предыдущий по отношению к ним символ (как знак ударения в листинге выше, он изменяет отображение латинской буквы e).
В связи с этим при работе со строковыми значениями не всегда корректным будет говорить именно о символах, так как мы видели ранее, что символ, по своей сути, сам может состоять из нескольких символов. В этом случае лучше обратиться к понятию графем-кластера.
Графем-кластер — это совокупность юникод-скаляров (или кодовых точек), при визуальном представлении выглядящих как один символ. Графем-кластер может состоять из одного или двух юникод-скаляров. Таким образом в будущем, говоря о значении типа Character, мы будем подразумевать не просто отдельный символ, а графем-кластер.
Графем-кластеры могут определять не только буквы алфавита, но и эмодзи. В листинге 12.6 приведен пример комбинирования символов «Thumbs up sign» (кодовая точка — 1f44d) и «Emoji Modifier Fitzpatrick Type-4» (кодовая точка — 1f3fd) в единый графем-кластер для вывода нового эмодзи (палец вверх со средиземноморским цветом кожи).
Листинг 12.6
var thumbsUp = "\u{1f44d}" // ""
var blackSkin = "\u{1f3fd}" // ""
var combine = "\u{1f44d}\u{1f3fd}" // ""
ПРИМЕЧАНИЕ Каждый символ помимо кодовой точки также имеет уникальное название. Эти данные при необходимости можно найти в таблицах юникод-символов в Интернете.
Вернемся к примеру с символом é. В листинге 12.7 создаются две строки, содержащие данный символ, первая из них содержит непосредственно сам символ é, а вторая — комбинацию из латинской e и знака ударения.
Листинг 12.7
let cafeSimple = "caf\u{E9}" //"café"
let cafeCombine = "cafe\u{301}" //"café"
cafeSimple.count // 4
cafeCombine.count // 4
Как видно из данного примера, несмотря на то что в переменной cafeCombine пять символов, свойство count для обоих вариантов возвращает значение 4. Это связано с тем, что для Swift строка — это не просто коллекция символов, а коллекция графем-кластеров. Стоит отметить, что время, необходимое для выполнения подсчета количества элементов в строке, растет линейно с увеличением количества этих элементов (букв). Причина заключается в том, что компьютер не может заранее знать, сколько графем-кластеров в коллекции, для этого ему необходимо полностью обойти строку с первого до последнего символа.
Графем-кластеры являются одновременно огромным плюсом стандарта Unicode, а также причиной отсутствия в Swift доступа к отдельным символам через целочисленный индекс. Вы не можете просто взять третий или десятый символ, так как нет никакой гарантии, что он окажется полноценным графем-кластером, а не отдельным символом в его составе. Для доступа к любому элементу коллекции типа String необходимо пройти через все предыдущие элементы. Только в этом случае можно однозначно получить корректный графем-кластер.
Тем не менее строки — это упорядоченные коллекции, а значит, в составе каждого элемента присутствует не только значение типа Character, но и индекс, позволяющий однозначно определить положение этого элемента в коллекции и получить к нему доступ. Именно об этом пойдет разговор в следующем разделе.
Почему Swift не позволяет использовать целочисленные индексы для доступа к отдельным графем-кластерам в строке? На самом деле для разработчиков этого языка не составило бы никакого труда слегка расширить тип String и добавить соответствующий функционал. Это же программирование, тут возможно все! Но они лишили нас такой возможности сознательно.
Дело в том, что Swift, в лице его разработчиков, а также сообщества, хотел бы, чтобы каждый из нас понимал больше, чем требуется для «тупого» набивания кода, чтобы мы знали, как работает технология «под капотом». Скажите честно, пришло бы вам в голову разбираться со стандартом Юникод, кодовыми точками, кодировками и графем-кластерами, если бы вы могли просто получить символ по его порядковому номеру?
Хотя лучше оставим этот вопрос без ответа и вернемся к строковым индексам.
Значение типа String имеет несколько свойств, позволяющих получить индекс определенных элементов. Первым из них является startIndex, возвращающий индекс первого элемента строки (листинг 12.8).
Листинг 12.8
let name = "e\u{301}lastic" //"élastic"
var index = name.startIndex
Обратите внимание, что первый графем-кластер в константе name состоит из двух символов. Свойство startIndex возвращает индекс, по которому можно получить именно графем-кластер, а не первый символ в составе графем-кластера. Теперь в переменной index хранится индекс первой буквы, и его можно использовать точно так же, как индексы других коллекций (листинг 12.9).
Листинг 12.9
let firstChar = name[index]
firstChar // "é"
type(of: firstChar) //Character.Type
В данном листинге был получен первый символ строки, хранящейся в константе name. Обратите внимание, что тип полученного значения — Character, что соответствует определению строки (это коллекция Character). Но вопрос в том, а что такое строковый индекс, какой тип данных он имеет (листинг 12.10)?
Листинг 12.10
type(of: index) //String.Index.Type
Тип строкового индекса — String.Index, значит, что это тип данных Index, вложенный в String. С вложенными типами мы встречались ранее во время изучения словарей (Dictionary). Значение типа String.Index определяет положение графем-кластера внутри строкового значения, то есть содержит ссылки на область памяти, где он начинается и заканчивается. И это, конечно же, вовсе не значение типа Int.
Помимо startIndex, вам доступно свойство endIndex, позволяющее получить индекс, который следует за последним символом в строке. Таким образом, он указывает не на последний символ, а за него, туда, куда будет добавлен новый графем-кластер (то есть добавлена новая буква, если она, конечно, будет добавлена). Если вы попытаетесь использовать значение свойства endIndex напрямую, то Swift сообщит о критической ошибке (листинг 12.11).
Листинг 12.11
var indexLastChar = name.endIndex
name[indexLastChar] //Fatal error: String index is out of bounds
Метод index(before:) позволяет получить индекс символа, предшествующего тому, индекс которого передан во входном аргументе before. Другими словами, передавая в before индекс символа, на выходе вы получите индекс предшествующего ему символа. Вызывая данный метод, в него можно, к примеру, передать значение свойства endIndex для получения последнего символа в строке (листинг 12.12).
Листинг 12.12
var lastChar = name.index(before: indexLastChar)
name[lastChar] //"c"
Метод index(after:) позволяет получить индекс последующего символа (листинг 12.13).
Листинг 12.13
var secondCharIndex = name.index(after: name.startIndex)
name[secondCharIndex] // "l"
Метод index(_:offsetBy:) позволяет получить требуемый символ с учетом отступа. В качестве значения первого аргумента передается индекс графем-кластера, от которого будет происходить отсчет, а в качестве значения входного параметра offsetBy передается целое число, указывающее на отступ вправо (листинг 12.14).
Листинг 12.14
var fourCharIndex = name.index(name.startIndex, offsetBy:3)
name[fourCharIndex] // "s"
При изучении возможностей Swift по работе со строками вам очень поможет окно автодополнения, в котором будут показаны все доступные методы и свойства значения типа String (рис. 12.1).
Рис. 12.1. Окно автодополнения в качестве краткой справки
Отмечу еще одно свойство, которое, возможно, понадобится вам в будущем. С помощью unicodeScalars можно получить доступ к коллекции юникод-скаляров, из которых состоит строка. Данная коллекция содержит не графем-кластеры, а именно юникод-скаляры с обозначением кодовых точек каждого символа строки. В листинге 12.15 показано, что количество элементов строки и значения, возвращенного свойством uncodeScalars, отличается, так как в составе строки есть сложный графем-кластер (состоящий из двух символов).
Листинг 12.15
name.count // 7
name.unicodeScalars.count // 8
Обратите внимание, что в данном примере впервые в одном выражении использована цепочка вызовов, когда несколько методов или свойств вызываются последовательно в одном выражении (name.unicodeScalars.count).
Цепочка вызовов — очень полезный функциональный механизм Swift. С ее помощью можно не записывать возвращаемое значение в параметр для последующего вызова очередного свойства или метода.
В результате вызова свойства unicodeScalars возвращается коллекция, а значит, у нее есть свойство count, которое тут же может быть вызвано.
Суть работы цепочек вызовов заключается в том, что если какая-либо функция, метод или свойство возвращают объект, у которого есть свои свойства или методы, то их можно вызывать в том же самом выражении. Длина цепочек вызова (количество вызываемых свойств и методов) не ограничена. В следующих главах вы будете все чаще использовать эту прекрасную возможность.
Четвертая версия Swift порадовала разработчиков новым типом данных Substring, описывающим подстроку некоторой строки. Substring для String — это как ArraySlice для Array. В ранних версиях языка, получая подстроку, возвращалась новая строка (значение типа String), для которой выделялась отдельная область памяти. Теперь при получении подстроки возвращается значение типа Substring, ссылающееся на ту же область памяти, что и оригинальная строка.
Другими словами, основной целью создания типа Substring была оптимизация. Значение типа Substring делит одну область памяти с родительской строкой, то есть для нее не выделяется дополнительная память.
Для получения необходимой подстроки можно воспользоваться операторами диапазона (листинг 12.16).
Листинг 12.16
var abc = "abcdefghijklmnopqrstuvwxyz"
// индекс первого символа
var firstCharIndex = abc.startIndex
// индекс четвертого символа
var fourthCharIndex = abc.index(firstCharIndex, offsetBy:3)
// получим подстроку
var subAbc = abc[firstCharIndex...fourthCharIndex]
subAbc // "abcd"
type(of: subAbc) // Substring.Type
В результате выполнения кода в переменной subAbc будет находиться значение типа Substring, включающее в себя первые четыре символа строки abc.
Подстроки обладают тем же функционалом, что и строки. Но при необходимости вы всегда можете использовать функцию String(_:) для преобразования подстроки в строку (листинг 12.17).
Листинг 12.17
type( of: String(subAbc) ) //String.Type
Также хотелось бы показать пример использования полуоткрытого оператора диапазона для получения подстроки, состоящей из всех символов, начиная с четвертого и до конца строки. При этом совершенно неважно, какого размера строка, вы всегда получите все символы до ее конца (листинг 12.18).
Листинг 12.18
var subStr = abc[fourthCharIndex...]
subStr //"defghijklmnopqrstuvwxyz"
На этом наше знакомство со строками закончено. Уверен, что оно было интересным и познавательным. Советую самостоятельно познакомиться со стандартом Unicode в отрыве от изучения Swift. Это позволит еще глубже понять принципы его работы и взаимодействия с языком.