Scala 3 — Book

Интерполяция строк

Language

Введение

Интерполяция строк позволяет использовать внутри строк переменные. Например:

val name = "James"
val age = 30
println(s"$name is $age years old")   // "James is 30 years old"

Использование интерполяции строк заключается в том, что перед строковыми кавычками ставится символ s, а перед любыми именами переменных ставится символ $.

Другие интерполяторы

То s, что вы помещаете перед строкой, является лишь одним из возможных интерполяторов, предоставляемых Scala.

Scala по умолчанию предоставляет три метода интерполяции строк: s, f и raw. Кроме того, строковый интерполятор — это всего лишь специальный метод, и вы можете определить свой собственный. Например, некоторые библиотеки баз данных определяют интерполятор sql, возвращающий запрос к базе данных.

Интерполятор s (s-строки)

Добавление s перед любым строковым литералом позволяет использовать переменные непосредственно в строке. Вы уже здесь видели пример:

val name = "James"
val age = 30
println(s"$name is $age years old")   // "James is 30 years old"

Здесь переменные $name и $age заменяются в строке результатами вызова name.toString и age.toString соответственно. s-строка будет иметь доступ ко всем переменным, в настоящее время находящимся в области видимости.

Хотя это может показаться очевидным, важно здесь отметить, что интерполяция строк не будет выполняться в обычных строковых литералах:

val name = "James"
val age = 30
println("$name is $age years old")   // "$name is $age years old"

Строковые интерполяторы также могут принимать произвольные выражения. Например:

println(s"2 + 2 = ${2 + 2}")   // "2 + 2 = 4"
val x = -1
println(s"x.abs = ${x.abs}")   // "x.abs = 1"

Любое произвольное выражение может быть встроено в ${}.

Некоторые специальные символы необходимо экранировать при встраивании в строку. Чтобы указать символ “знак доллара”, вы можете удвоить его $$, как показано ниже:

println(s"New offers starting at $$14.99")   // "New offers starting at $14.99"

Двойные кавычки также необходимо экранировать. Это можно сделать с помощью тройных кавычек, как показано ниже:

println(s"""{"name":"James"}""")     // `{"name":"James"}`

Наконец, все многострочные строковые литералы также могут быть интерполированы.

println(s"""name: "$name",
           |age: $age""".stripMargin)

Строка будет напечатана следующим образом:

name: "James"
age: 30

Интерполятор f (f-строки)

Добавление f к любому строковому литералу позволяет создавать простые отформатированные строки, аналогичные printf в других языках. При использовании интерполятора f за всеми ссылками на переменные должна следовать строка формата в стиле printf, например %d. Давайте посмотрим на пример:

val height = 1.9d
val name = "James"
println(f"$name%s is $height%2.2f meters tall")  // "James is 1.90 meters tall"

Интерполятор f типобезопасен. Если вы попытаетесь передать в строку формата, который работает только для целых чисел, значение double, компилятор выдаст ошибку. Например:

val height: Double = 1.9d

scala> f"$height%4d"
<console>:9: error: type mismatch;
  found   : Double
  required: Int
            f"$height%4d"
              ^
val height: Double = 1.9d

scala> f"$height%4d"
-- Error: ----------------------------------------------------------------------
1 |f"$height%4d"
  |   ^^^^^^
  |   Found: (height : Double), Required: Int, Long, Byte, Short, BigInt
1 error found

Интерполятор f использует утилиты форматирования строк, доступные в Java. Форматы, разрешенные после символа %, описаны в Formatter javadoc. Если после определения переменной нет символа %, предполагается форматирование %s (String).

Наконец, как и в Java, используйте %% для получения буквенного символа % в итоговой строке:

println(f"3/19 is less than 20%%")  // "3/19 is less than 20%"

Интерполятор raw

Интерполятор raw похож на интерполятор s, за исключением того, что он не выполняет экранирование литералов внутри строки. Вот пример обработанной строки:

scala> s"a\nb"
res0: String =
a
b

Здесь строковый интерполятор s заменил символы \n символом переноса строки. Интерполятор raw этого не делает.

scala> raw"a\nb"
res1: String = a\nb

Интерполятор raw полезен тогда, когда вы хотите избежать преобразования таких выражений, как \n, в символ переноса строки.

В дополнение к трем строковым интерполяторам пользователи могут определить свои собственные.

Расширенное использование

Литерал s"Hi $name" анализируется Scala как обрабатываемый строковый литерал. Это означает, что компилятор выполняет некоторую дополнительную работу с этим литералом. Особенности обработанных строк и интерполяции строк описаны в SIP-11. Вот краткий пример, который поможет проиллюстрировать, как они работают.

Пользовательские интерполяторы

В Scala все обрабатываемые строковые литералы представляют собой простые преобразования кода. Каждый раз, когда компилятор встречает обрабатываемый строковый литерал вида:

id"string content"

он преобразует его в вызов метода (id) для экземпляра StringContext. Этот метод также может быть доступен в неявной области видимости. Чтобы определить собственную интерполяцию строк, нужно создать неявный класс (Scala 2) или метод расширения (Scala 3), который добавляет новый метод для StringContext.

В качестве простого примера предположим, что у нас есть простой класс Point и мы хотим создать собственный интерполятор, который преобразует p"a,b" в объект Point.

case class Point(x: Double, y: Double)

val pt = p"1,-2"     // Point(1.0,-2.0)

Мы бы создали собственный интерполятор p, сначала внедрив расширение StringContext, например, так:

implicit class PointHelper(val sc: StringContext) extends AnyVal {
  def p(args: Any*): Point = ???
}

Примечание. Важно расширить AnyVal в Scala 2.x, чтобы предотвратить создание экземпляра класса во время выполнения при каждой интерполяции. Дополнительную информацию см. в документации по value class.

extension (sc: StringContext)
  def p(args: Any*): Point = ???

Как только это расширение окажется в области видимости и компилятор Scala обнаружит p"some string", то превратит some string в токены String, а каждую встроенную переменную в аргументы выражения.

Например, p"1, $someVar" превратится в:

new StringContext("1, ", "").p(someVar)

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

new PointHelper(new StringContext("1, ", "")).p(someVar)
StringContext("1, ", "").p(someVar)

В результате каждый из фрагментов обработанной строки отображается в элементе StringContext.parts, а любые значения выражений в строке передаются в параметр метода args.

Пример реализации

Простая реализация метода интерполяции для нашего Point может выглядеть примерно так, как показано ниже, хотя более детализированный метод может иметь более точный контроль над обработкой строки parts и выражения args вместо повторного использования интерполятора s.

implicit class PointHelper(val sc: StringContext) extends AnyVal {
  def p(args: Double*): Point = {
    // переиспользование интерполятора `s` и затем разбиение по ','
    val pts = sc.s(args: _*).split(",", 2).map { _.toDoubleOption.getOrElse(0.0) }
    Point(pts(0), pts(1))
  }
}

val x=12.0

p"1, -2"        // Point(1.0, -2.0)
p"${x/5}, $x"   // Point(2.4, 12.0)
extension (sc: StringContext)
  def p(args: Double*): Point = {
    // переиспользование интерполятора `s` и затем разбиение по ','
    val pts = sc.s(args: _*).split(",", 2).map { _.toDoubleOption.getOrElse(0.0) }
    Point(pts(0), pts(1))
  }

val x=12.0

p"1, -2"        // Point(1.0, -2.0)
p"${x/5}, $x"   // Point(2.4, 12.0)

Хотя строковые интерполяторы изначально использовались для создания нескольких строковых форм, использование пользовательских интерполяторов, как указано выше, может обеспечить более мощное синтаксическое сокращение, и сообщество уже использует этот синтаксис для таких вещей, как расширение цвета терминала ANSI, выполнение SQL-запросов, магические представления $"identifier" и многие другие.

Contributors to this page: