[Перевод] Продвинутая интерполяция строк в Swift 5.0

l10vsmiyatuiropglzgx5l6ggj4.png

Интерполяция строк была в Swift с ранних версий, но в Swift 5.0 этот функционал был расширен, стал быстрее и значительно мощнее.

В этой статье мы пройдёмся по новым возможностям интерполяции строк и рассмотрим, как это можно применить в нашем собственном коде. Вы также можете загрузить исходники к этой статье здесь.

Основы


Мы используем базовую интерполяцию строк вот так:

let age = 38
print("You are \(age)")


Мы считаем это само собой разумеющимся, но в своё время это стало значительным облегчением по сравнению с тем, с чем приходилось иметь дело раньше:

[NSString stringWithFormat:@"%ld", (long)unreadCount];


Здесь также значимый выигрыш в производительности, так как альтернативой было:

let all = s1 + s2 + s3 + s4


Да, конечный результат был бы тем же, но Swift пришлось бы добавить s1 к s2 чтобы получить s5, добавить s5 к s3 чтобы получить s6, и добавить s6 к s4 чтобы получить s7, перед присвоением all.

Интерполяция строк практически не менялась со Swift 1.0, единственное значимое изменение пришло вместе с Swift 2.1, где мы получили возможность использовать в интерполяции string literals:

print("Hi, \(user ?? "Anonymous")")


Как вы знаете, Swift развивается во многом благодаря предложениям сообщества. Идеи обсуждаются, разрабатываются — и либо принимаются, либо отвергаются.

Итак, спустя пять лет, развитие Swift добралось до интерполяции строк. В Swift 5.0 появились новые супервозможности, которые дают нам возможность контролировать процесс интерполяции строк.

Чтобы попробовать, рассмотрим следующий сценарий. Если мы задаём новую целую переменную вот так:

let age = 38


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

print("Hi, I'm \(age).")


Но что, если мы хотим отформатировать результат как-то по-другому?

Используя новую систему интерполяции строк в in Swift 5.0 мы можем написать экстеншн String.StringInterpolation, чтобы добавить свой собственный метод интерполяции:

extension String.StringInterpolation {
    mutating func appendInterpolation(_ value: Int) {
        let formatter = NumberFormatter()
        formatter.numberStyle = .spellOut

        if let result = formatter.string(from: value as NSNumber) {
            appendLiteral(result)
        }
    }
}


Теперь код выведет целую переменную как текст: «Hi, I«m thirty-eight.»

Мы можем использовать похожую технику чтобы поправить форматирование даты, поскольку вид даты по умолчанию в виде string не слишком привлекателен:

print("Today's date is \(Date()).")


Вы увидите, что Swift выведет текущую дату в виде что-то вроде:»2019–02–21 23:30:21 +0000». Мы можем сделать это красивее используя своё собственное форматирование даты:

mutating func appendInterpolation(_ value: Date) {
    let formatter = DateFormatter()
    formatter.dateStyle = .full

    let dateString = formatter.string(from: value)
    appendLiteral(dateString)
}


Теперь результат выглядит гораздо лучше, что-то вроде: «February 21, 2019 23:30:21».

Замечание: чтобы избежать возможной путаницы при совместной работе в команде, вам, вероятно, не стоит перекрывать методы Swift по умолчанию. Поэтому дайте параметрам имена на свой выбор, чтобы избежать путаницы:

mutating func appendInterpolation(format value: Int) {


Теперь мы вызовем этот метод с именованным параметром:

print("Hi, I'm \(format: age).")


Теперь будет ясно видно, что мы используем собственную реализацию метода.

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


Это изменение показывает, что теперь у нас есть полный контроль над тем, каким образом происходит интерполяция строк.

Например, мы можем переписать код, чтобы обрабатывать сообщения в Twitter:

mutating func appendInterpolation(twitter: String) {
    appendLiteral("@\(twitter)")
}


Теперь мы можем написать так:

print("You should follow me on Twitter: \(twitter: "twostraws").")


Но зачем нам ограничиваться одним параметром? Для нашего примера форматирования числа нет никакого смысла принуждать пользователей использовать один параметр конвертирования (.spellOut) — так что мы изменим метод, добавив второй параметр:

mutating func appendInterpolation(format value: Int, using style: NumberFormatter.Style) {
    let formatter = NumberFormatter()
    formatter.numberStyle = style

    if let result = formatter.string(from: value as NSNumber) {
        appendLiteral(result)
    }
}


И используем его так:

print("Hi, I'm \(format: age, using: .spellOut).")


У вас может быть сколько угодно параметров, любого типа. Пример с использованием @autoclosure для значения по умолчанию:

extension String.StringInterpolation {
    mutating func appendInterpolation(_ values: [String], empty defaultValue: @autoclosure () -> String) {
        if values.count == 0 {
            appendLiteral(defaultValue())
        } else {
            appendLiteral(values.joined(separator: ", "))
        }
    }
}

let names = ["Malcolm", "Jayne", "Kaylee"]
print("Crew: \(names, empty: "No one").")


Применение атрибута @autoclosure означает, что для значения по умолчанию мы можем использовать простые значения или вызывать сложные функции. В методе они станут замыканием.

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

extension Array where Element == String {
    func formatted(empty defaultValue: @autoclosure () -> String) -> String {
        if count == 0 {
            return defaultValue()
        } else {
            return self.joined(separator: ", ")
        }
    }
}

print("Crew: \(names.formatted(empty: "No one")).")


Но теперь мы усложнили вызов — ведь мы же очевидно пытаемся отформатировать что-то, в этом и есть смысл интерполяции. Помните правило Swift — избегайте ненужных слов.

Erica Sadun предложила по-настоящему короткий и красивый пример того, как можно упростить код:

extension String.StringInterpolation {
    mutating func appendInterpolation(if condition: @autoclosure () -> Bool, _ literal: StringLiteralType) {
        guard condition() else { return }
        appendLiteral(literal)
    }
}

let doesSwiftRock = true
print("Swift rocks: \(if: doesSwiftRock, "(*)")")
print("Swift rocks \(doesSwiftRock ? "(*)" : "")")


Добавление интерполяции строк для пользовательских типов


Мы можете использовать интерполяцию строк для своих собственных типов:

struct Person {
    var type: String
    var action: String
}

extension String.StringInterpolation {
    mutating func appendInterpolation(_ person: Person) {
        appendLiteral("I'm a \(person.type) and I'm gonna \(person.action).")
    }
}

let hater = Person(type: "hater", action: "hate")
print("Status check: \(hater)")


Интерполяция строк полезна тем, что мы не касаемся отладочной информации об объекте. Если мы посмотрим его в дебаггере или выведем его, то мы увидим нетронутые данные:

print(hater)


Мы можем комбинировать пользовательский тип с несколькими параметрами:

extension String.StringInterpolation {
    mutating func appendInterpolation(_ person: Person, count: Int) {
        let action = String(repeating: "\(person.action) ", count: count)
        appendLiteral("\n\(person.type.capitalized)s gonna \(action)")
    }
}

let player = Person(type: "player", action: "play")
let heartBreaker = Person(type: "heart-breaker", action: "break")
let faker = Person(type: "faker", action: "fake")

print("Let's sing: \(player, count: 5) \(hater, count: 5) \(heartBreaker, count: 5) \(faker, count: 5)")


Конечно же, вы можете использовать все возможности Swift, чтобы создать своё собственное форматирование. Например, мы можем написать реализацию, которая принимает любой объект Encodable и выводит его в JSON:

mutating func appendInterpolation(debug value: T) {
    let encoder = JSONEncoder()
    encoder.outputFormatting = .prettyPrinted

    if let result = try? encoder.encode(value) {
        let str = String(decoding: result, as: UTF8.self)
        appendLiteral(str)
    }
}

Если мы сделаем Person соответствующим протоколу Encodable, то мы можем сделать так:

print("Here's some data: \(debug: faker)")


Вы можете использовать возможности вроде переменного числа параметров иди даже пометить вашу реализацию интерполяции как throwing. Например, наша система форматирования JSON в случае ошибки кодирования никак не реагирует, но мы может это исправить для анализа произошедшей ошибки в дальнейшем:

mutating func appendInterpolation(debug value: T) throws {
    let encoder = JSONEncoder()
    encoder.outputFormatting = .prettyPrinted

    let result = try encoder.encode(value)
    let str = String(decoding: result, as: UTF8.self)
    appendLiteral(str)
}

print(try "Status check: \(debug: hater)")


Всё, что мы до сих пор рассматривали — это просто модификации способов интерполяции строк.

Создание собственных типов при помощи интерполяции


Как вы видели, речь шла о способе форматирования данных в вашем приложении по-настоящему удобным способом, но мы также можем создавать свои собственные типы используя интерполяцию строк.

Чтобы продемонстрировать это, мы создадим новый тип, который инициализируется из string при помощи интерполяции строк.

struct ColoredString: ExpressibleByStringInterpolation {
    // это вложенная структура - черновик для хранения атрибутов-строк из нескольких интерполяций 
    struct StringInterpolation: StringInterpolationProtocol {
        // здесь храним атрибут-строку по мере создания
        var output = NSMutableAttributedString()

        // несколько атрибутов по умолчанию 
        var baseAttributes: [NSAttributedString.Key: Any] = [.font: UIFont(name: "Georgia-Italic", size: 64) ?? .systemFont(ofSize: 64), .foregroundColor: UIColor.black]

        // этот инициалайзер необходим, также может быть использован в оптимизации производительности
        init(literalCapacity: Int, interpolationCount: Int) { }

        // вызывается, когда нам необходимо добавить текст
        mutating func appendLiteral(_ literal: String) {
            // выводится на печать, так что можно видеть процесс выполнения
            print("Appending \(literal)")

            // получаем базовое оформление
            let attributedString = NSAttributedString(string: literal, attributes: baseAttributes)

            // добавляем к черновой строке
            output.append(attributedString)
        }

        // вызывается, когда нам нужно добавить цветное сообщение к нашей строке
        mutating func appendInterpolation(message: String, color: UIColor) {
            // снова выводим на печать
            print("Appending \(message)")

            // берем копию базового атрибута и применяем цвет
            var coloredAttributes = baseAttributes
            coloredAttributes[.foregroundColor] = color

            // заворачиваем в новый атрибут-строку и добавляем в черновик
            let attributedString = NSAttributedString(string: message, attributes: coloredAttributes)
            output.append(attributedString)
        }
    }

    // окончательная строка с атрибутами, когда все интерполции выполнены
    let value: NSAttributedString

    // создаем экземпляр из строки литералов
    init(stringLiteral value: String) {
        self.value = NSAttributedString(string: value)
    }

    // создаем экземпляр из интреполяции
    init(stringInterpolation: StringInterpolation) {
        self.value = stringInterpolation.output
    }
}

let str: ColoredString = "\(message: "Red", color: .red), \(message: "White", color: .white), \(message: "Blue", color: .blue)"


На самом деле, под капотом тут один синтаксический сахар. Заключительную часть мы вполне могли бы написать вручную:

var interpolation = ColoredString.StringInterpolation(literalCapacity: 10, interpolationCount: 1)

interpolation.appendLiteral("Hello")
interpolation.appendInterpolation(message: "Hello", color: .red)
interpolation.appendLiteral("Hello")

let valentine = ColoredString(stringInterpolation: interpolation)


Заключение


Как вы видели, пользовательская интерполяция строк позволяет нам разместить форматирование в одном месте, так что вызовы методов становятся проще и яснее. Также это предоставляет нам отличную гибкость для создания требуемых типов максимально естественным образом.

Помните, что это только одна из возможностей — и не единственная. Это означает, что иногда мы используем интерполяцию, иногда функции или что-то ещё. Как и многое в разработке, всегда нужно выбирать оптимальный способ решения задачи.

© Habrahabr.ru