Строки
В предыдущих уроках мы уже познакомились с важнейшим строительным блоком большинства программ — строками. Давайте вспомним их основные свойства:
Строка — это последовательность символов Unicode, закодированных в UTF-8. Строка неизменяема: если применить к ней модификацию, на самом деле получится новая строка с изменённым содержимым. Исходная строка останется прежней.
Строковые литералы обычно записываются в двойных кавычках (").
Интерполяция
Интерполяция строк — удобный способ объединения строк: #{...} внутри строкового литерала вставляет в этом месте строки значение выражения, заключённого в фигурные скобки.
name = "Crystal"
puts "Hello #{name}"
Выражение внутри интерполяции должно быть коротким: это может быть переменная или простой вызов метода. Более сложные выражения ухудшают читаемость кода.
Значение выражения не обязательно должно быть строкой. Подойдёт любой тип — он будет преобразован в строковое представление вызовом метода #to_s. Этот метод определён для любого объекта. Попробуем использовать число:
name = 6
puts "Hello #{name}!"
Примечание
Альтернатива интерполяции — конкатенация. Вместо "Hello #{name}!" можно написать "Hello " + name + "!". Но такой вариант многословнее и может привести к неожиданным проблемам с типами, отличными от строковых. Обычно интерполяцию предпочитают конкатенации.
Экранирование
Некоторые символы нельзя записать непосредственно в строковых литералах. Например, двойную кавычку: если использовать её внутри строки, компилятор воспримет её как завершающий разделитель.
Решение этой проблемы — экранирование: если перед двойной кавычкой стоит обратная косая черта (\), она воспринимается как escape-последовательность, и оба символа вместе обозначают символ двойной кавычки.
puts "I say: \"Hello World!\""
Есть и другие escape-последовательности: например, для непечатаемых символов, таких как перевод строки (\n) или табуляция (\t). Чтобы записать литеральную обратную косую черту, используется escape-последовательность из двух обратных косых черт (\\). Нулевой символ (кодовая точка 0) в строках Crystal является обычным символом. В некоторых языках программирования этот символ обозначает конец строки. Но в Crystal он определяется только своим свойством #size.
puts "I say: \"Hello \\\n\tWorld!\""
Совет
Подробнее о доступных escape-последовательностях можно узнать в справочнике по строковым литералам.
Альтернативные разделители
Некоторые строковые литералы могут содержать много двойных кавычек — например, HTML-теги с заключёнными в кавычки значениями атрибутов. Экранировать каждую кавычку обратной косой чертой было бы неудобно. Альтернативные разделители литералов — удобная замена. %(...) эквивалентно "...", но вместо двойных кавычек в качестве разделителей используются скобки (( и )).
puts %(I say: "Hello World!")
Escape-последовательности и интерполяция по-прежнему работают так же.
Совет
Подробнее об альтернативных разделителях можно узнать в справочнике по строковым литералам.
Unicode
Unicode — международный стандарт представления текста в различных системах письма. Помимо букв латинского алфавита, используемых в английском и многих других языках, он включает множество других наборов символов. Стандарт Unicode содержит не только обычный текст, но также эмодзи и значки.
В следующем примере для обращения ко всему миру используется символ Unicode U+1F310 (Глобус с меридианами):
puts "Hello 🌐"
Работа с символами Unicode иногда бывает непростой. Некоторые символы могут не поддерживаться шрифтом вашего редактора, а некоторые вообще невозможно вывести на экран. В качестве альтернативы символы Unicode можно записывать в виде escape-последовательностей. Обратная косая черта, за которой следует буква u, обозначает кодовую точку Unicode. Значение кодовой точки записывается шестнадцатеричными цифрами в фигурных скобках. Если кодовая точка состоит ровно из четырёх цифр, фигурные скобки можно опустить.
puts "Hello \u{1F310}"
Преобразование
Предположим, что вы хотите изменить строку. Может быть, сделать сообщение громким и написать его целиком заглавными буквами? Метод String#upcase преобразует все строчные буквы в соответствующие заглавные. Противоположный метод — String#downcase. Есть ещё несколько похожих методов, которые позволяют представить сообщение в разных стилях:
message = "Hello World! Greetings from Crystal."
puts "normal: #{message}"
puts "upcased: #{message.upcase}"
puts "downcased: #{message.downcase}"
puts "camelcased: #{message.camelcase}"
puts "capitalized: #{message.capitalize}"
puts "reversed: #{message.reverse}"
puts "titleized: #{message.titleize}"
puts "underscored: #{message.underscore}"
Методы #camelcase и #underscore не меняют эту конкретную строку, но попробуйте применить их к значениям "snake_cased" или "CamelCased".
Информация
Рассмотрим строку подробнее и узнаем, что о ней можно выяснить. Прежде всего, у строки есть длина — количество содержащихся в ней символов. Это значение доступно через String#size.
message = "Hello World! Greetings from Crystal."
p! message.size
Чтобы определить, пуста ли строка, можно проверить, равен ли её размер нулю, или просто использовать сокращённую запись String#empty?:
empty_string = ""
p! empty_string.size == 0,
empty_string.empty?
Метод String#blank? возвращает true, если строка пуста или содержит только пробельные символы. Похожий метод — String#presence: он возвращает nil, если строка пустая или состоит из пробелов, и в противном случае возвращает саму строку.
blank_string = ""
p! blank_string.blank?,
blank_string.presence
Равенство и сравнение
Проверить строки на равенство можно с помощью оператора равенства (==), а сравнить их — с помощью оператора сравнения (<=>). Оба оператора сравнивают строки строго посимвольно. Помните, что <=> возвращает целое число, указывающее на отношение между двумя операндами, а == возвращает true, если результат сравнения — 0, то есть оба значения равны.
Кроме того, есть метод #compare, который выполняет сравнение без учёта регистра.
message = "Hello World!"
p! message == "Hello World",
message == "Hello Crystal",
message == "hello world",
message.compare("hello world", case_insensitive: false),
message.compare("hello world", case_insensitive: true)
Частичные совпадения
Иногда неважно, совпадает ли одна строка с другой в точности, и нужно лишь узнать, содержит ли одна строка другую. Например, проверим с помощью метода #includes?, посвящено ли сообщение языку Crystal.
message = "Hello World!"
p! message.includes?("Crystal"),
message.includes?("World")
Иногда особый интерес представляет начало или конец строки. Здесь пригодятся методы #starts_with? и #ends_with?.
message = "Hello World!"
p! message.starts_with?("Hello"),
message.starts_with?("Bye"),
message.ends_with?("!"),
message.ends_with?("?")
Поиск подстрок по индексу
Получить более подробную информацию о положении подстроки можно с помощью метода #index. Он возвращает индекс первого символа первого вхождения подстроки. Результат 0 означает то же, что и starts_with?.
p! "Crystal is awesome".index("Crystal"),
"Crystal is awesome".index("s"),
"Crystal is awesome".index("aw")
У метода есть необязательный аргумент offset, который позволяет начать поиск не с начала строки, а с другой позиции. Это полезно, если подстрока может встречаться несколько раз.
message = "Crystal is awesome"
p! message.index("s"),
message.index("s", offset: 4),
message.index("s", offset: 10)
Метод #rindex работает так же, но выполняет поиск с конца строки.
message = "Crystal is awesome"
p! message.rindex("s"),
message.rindex("s", 13),
message.rindex("s", 8)
Если подстрока не найдена, результатом будет специальное значение nil. Оно означает «нет значения». Это логично, поскольку у подстроки, которой нет в строке, не может быть индекса.
Посмотрев на тип возвращаемого значения метода #index, мы увидим, что он возвращает либо Int32, либо Nil.
a = "Crystal is awesome".index("aw")
p! a, typeof(a)
b = "Crystal is awesome".index("meh")
p! b, typeof(b)
Совет
В следующем уроке мы подробнее рассмотрим nil.
Извлечение подстрок
Подстрока — это часть строки. Если нужно извлечь часть строки, это можно сделать несколькими способами.
Оператор индексирования #[] позволяет указать подстроку по индексу символа и длине. Индексы символов начинаются с 0 и заканчиваются на длине строки (то есть значении #size) минус один. Первый аргумент задаёт индекс первого символа подстроки, а второй — длину подстроки. message[6, 5] извлекает подстроку длиной пять символов, начиная с индекса шесть.
message = "Hello World!"
p! message[6, 5]
Предположим, мы выяснили, что строка начинается с Hello и заканчивается на !, и хотим извлечь всё, что между ними. Если бы сообщение было Hello Crystal, мы бы не получили слово Crystal целиком, потому что в нём больше пяти символов.
Можно решить эту задачу, вычислив длину подстроки как длину всей строки минус длины начала и конца.
message = "Hello World!"
p! message[6, message.size - 6 - 1]
Есть способ проще: оператор индексирования можно использовать с Range индексов символов. Литерал диапазона состоит из начального и конечного значений, разделённых двумя точками (..). Первое значение, как и прежде, указывает начальный индекс подстроки, а второе — конечный индекс (а не длину). Теперь не нужно повторять начальный индекс в вычислении, поскольку конечный индекс равен длине минус два (один для конечного индекса и один, чтобы не включать последний символ).
Можно ещё проще: отрицательные значения индексов автоматически отсчитываются от конца строки, поэтому вычислять конечный индекс на основе размера строки явно не требуется.
message = "Hello World!"
p! message[6..(message.size - 2)],
message[6..-2]
Замена
Похожим образом можно изменить строку. Давайте убедимся, что мы правильно приветствуем Crystal и никого больше. Вместо обращения к подстроке вызовем #sub. Первый аргумент снова представляет собой диапазон, задающий участок, который будет заменён значением второго аргумента.
message = "Hello World!"
p! message.sub(6..-2, "Crystal")
Метод #sub очень гибкий, и использовать его можно по-разному. В качестве первого аргумента также можно передать строку для поиска — тогда он заменит эту подстроку значением второго аргумента.
message = "Hello World!"
p! message.sub("World", "Crystal")
#sub заменяет только первое вхождение искомой строки. Его расширенная версия, #gsub, заменяет все вхождения.
message = "Hello World! How are you, World?"
p! message.sub("World", "Crystal"),
message.gsub("World", "Crystal")
Совет
Более подробную информацию можно найти в справочнике по строковым литералам и документации по API String.
To the extent possible under law, the persons who contributed to this workhave waived
all copyright and related or neighboring rights to this workby associating CC0 with it.
https://crystal-lang.org/reference/1.2/tutorials/basics/40_strings.html