Перейти к содержимому
Теория · Строки и символы · урок 4 из 7

Почему s[0] не работает

Во многих языках первый символ строки достают по номеру: s[0]. В Swift такая запись не компилируется:

SWIFT
let text = "café"
print(text[0]) // ✗
TEXT
error: 'subscript(_:)' is unavailable: cannot subscript String with an Int, use a String.Index instead.

Компилятор говорит прямо: строку нельзя спрашивать по номеру типа Int, нужен индекс типа String.Index. Что это такое и чем плох номер?

Вспомните нитку с бусинами. Крупные бусины на ней разного размера: под одной висит одна мелкая, под другой две, под третьей четыре. Чтобы найти третью крупную бусину, нельзя отмерить линейкой два одинаковых шага. Придётся пройти нитку с начала и отсчитать бусины по одной.

Почему Swift запрещает номер

Если бы все символы занимали одинаковое число байтов, номер символа сразу давал бы его место в памяти. В UTF-8 размеры разные. Чтобы найти символ номер 1000, Swift пришлось бы пройти 999 символов перед ним.

Запись s[1000] выглядела бы мгновенной, а работала бы медленно. В цикле такие медленные шаги складывались бы незаметно. Поэтому Swift не даёт ей скомпилироваться и предлагает способы, в которых этот проход виден в коде.

Этот вопрос часто задают на собеседовании: почему в Swift нельзя написать s[0]? Хороший ответ: символы занимают разное число байтов, поэтому символ по номеру можно найти только проходом от начала строки. Вместо номеров Swift использует индексы String.Index.

Символы с краёв строки

Чаще всего нужен не символ в середине, а начало или конец строки. Для этого у строки есть готовые средства:

SWIFT
let word = "Swift"
let empty = ""
print(word.first ?? "?", word.last ?? "?") // S t
print(empty.first ?? "?") // ?
print(word.prefix(3), word.dropFirst()) // Swi wift
print(word.prefix(10)) // Swift

first и last

first — первый символ строки, last — последний. У пустой строки их нет, поэтому оба возвращают опционал Character?: символ или nil. Перед печатью коробку открывают, здесь через ??:

  1. word.first ?? "?" даёт S, а word.last ?? "?" — t.
  2. У empty первого символа нет, и ?? подставляет запасной символ "?".

prefix и dropFirst

  • prefix(3) даёт первые три символа: Swi.
  • dropFirst() даёт всё, кроме первого символа: wift.
  • prefix(10) при пяти символах отдаёт всю строку и не завершает программу аварийно.

Результат prefix и dropFirst имеет тип Substring — это часть исходной строки. print печатает Substring как обычную строку. Чтобы получить отдельную строку String, оберните результат: String(word.prefix(3)).

Индекс строки

Символ в середине строки достают по индексу строки. Это значение типа String.Index: оно помнит место символа в строке, а не его номер. Индекс получают от самой строки:

SWIFT
let third = word.index(word.startIndex, offsetBy: 2)
print(word[third]) // i

index(_:offsetBy:)

  1. word.startIndex — место первого символа.
  2. word.index(word.startIndex, offsetBy: 2) отсчитывает от него два символа вперёд и возвращает индекс третьего.
  3. word[third] достаёт символ по этому индексу — букву i.

Запись длинная, и в этом её смысл: в коде видно, что Swift отсчитывает символы от начала строки.

Здесь картинка с бусинами неточна. На нитке нужную бусину находят глазами, а индекс String.Index можно получить только у самой строки — например, отсчитав символы от startIndex.

Частая ошибка

Последний символ попробовали достать через endIndex:

SWIFT
let city = "Oslo"
print(city[city.endIndex]) // ✗
TEXT
Fatal error: String index is out of bounds

Программа компилируется, но аварийно завершается. endIndex — это не место последнего символа, а место сразу за ним, как конец нитки после последней бусины. Символа там нет. Последний символ даёт city.last.

Главное

  • Строку нельзя индексировать номером: s[0] не компилируется, потому что символы занимают разное число байтов.
  • first и last возвращают Character?, prefix(n) даёт первые n символов, а dropFirst() — всё, кроме первого.
  • Символ в середине достают по индексу: s[s.index(s.startIndex, offsetBy: 2)].
  • endIndex указывает за последний символ, поэтому s[s.endIndex] аварийно завершает программу.