Что нового в Swift 5.8

API с обратным развертыванием, больше неявных самостоятельных обновлений, улучшенные построители результатов и многое другое!

Несмотря на то, что многие важные изменения Swift в настоящее время просачиваются через Swift Evolution, сам Swift 5.8 представляет собой скорее чистую версию: да, есть дополнения, но есть и другие улучшения, улучшающие функциональность, которая уже широко использовалась. Надеюсь, это должно значительно облегчить внедрение, особенно после гигантского набора изменений, которые были втиснуты в Swift 5.7!

В этой статье я расскажу вам о наиболее важных изменениях на этот раз, предоставив примеры кода и пояснения, чтобы вы могли попробовать все сами. Вам понадобится Xcode 14.3 или более поздняя версия, чтобы использовать это, хотя некоторые изменения требуют определенного флага компилятора, прежде чем Swift 6 наконец появится.

Снять все ограничения на переменные в построителях результатов

SE-0373 ослабляет некоторые ограничения на переменные при использовании внутри построителей результатов, позволяя нам писать код, который ранее был бы запрещен компилятором.

Например, в Swift 5.8 мы можем использовать ленивые переменные непосредственно внутри построителей результатов, например так:

struct ContentView: View {
    var body: some View {
        VStack {
            lazy var user = fetchUsername()
            Text("Hello, \(user).")
        }
        .padding()
    }

    func fetchUsername() -> String {
        "@twostraws"
    }
}

Это показывает концепцию, но не дает никакой пользы, потому что всегда используется ленивая переменная — нет никакой разницы между использованием lazy var и let в этом коде. Чтобы увидеть, где это действительно полезно, рассмотрим более длинный пример кода, например этот:

// The user is an active subscriber, not an active subscriber, or we don't know their status yet.
enum UserState {
    case subscriber, nonsubscriber, unknown
}

// Two small pieces of information about the user
struct User {
    var id: UUID
    var username: String
}

struct ContentView: View {
    @State private var state = UserState.unknown

    var body: some View {
        VStack {
            lazy var user = fetchUsername()

            switch state {
            case .subscriber:
                Text("Hello, \(user.username). Here's what's new for subscribers…")
            case .nonsubscriber:
                Text("Hello, \(user.username). Here's why you should subscribe…")
                Button("Subscribe now") {
                    startSubscription(for: user)
                }
            case .unknown:
                Text("Sign up today!")
            }
        }
        .padding()
    }

    // Example function that would do complex work
    func fetchUsername() -> User {
        User(id: UUID(), username: "Anonymous")
    }

    func startSubscription(for user: User) {
        print("Starting subscription…")
    }
}

Этот подход решает проблемы, которые могут появиться в альтернативах:

  • Если бы мы не использовали lazy, то fetchUsername() вызывалась бы во всех трех случаях state, даже если она не используется ни в одном из них.
  • Если бы мы удалили lazy и поместили вызов fetchUsername() в два случая, то мы бы дублировали код — не такая уж большая проблема для простого однострочного кода, но вы можете себе представить, как это вызовет проблемы в более сложном коде.
  • Если мы переместим user в вычисляемое свойство, оно будет вызвано во второй раз, когда пользователь нажмет кнопку «Подписаться сейчас».

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

struct ContentView: View {
    var body: some View {
        @AppStorage("counter") var tapCount = 0

        Button("Count: \(tapCount)") {
            tapCount += 1
        }
    }
}

Однако, хотя это приведет к изменению базового значения UserDefaults при каждом касании, использование @AppStorage таким образом не приведет к повторному вызову свойства body при каждом изменении tapCount — наш пользовательский интерфейс не будет автоматически обновляться, чтобы отразить изменение.

Обратное развертывание функции

SE-0376 добавляет новый атрибут @backDeployed, который позволяет разрешить использование новых API в более старых версиях фреймворков. Он работает, записывая код функции в двоичный файл вашего приложения, а затем выполняя проверку во время выполнения: если ваш пользователь использует подходящую новую версию операционной системы, тогда будет использоваться собственная версия функции системы, в противном случае версия, скопированная в ваш вместо этого будет использоваться двоичный файл приложения.

На первый взгляд это звучит как фантастический способ для Apple сделать некоторые новые функции задним числом доступными в более ранних операционных системах, но я не думаю, что это какая-то серебряная пуля — @backDeployed применяется только к функциям, методам, индексам и вычисляемым свойствам. , поэтому, хотя он может отлично работать для небольших изменений API, таких как модификатор fontDesign(), представленный в iOS 16.1, он не будет работать для любого кода, требующего использования новых типов, такого как новый модификатор scrollBounceBehavior(), который зависит от новой структуры ScrollBounceBehavior.

Например, в iOS 16.4 появился monospaced(_ isActive:) вариант для Text. Если бы это использовало @backDeployed, команда SwiftUI могла бы убедиться, что модификатор доступен для любой самой ранней версии SwiftUI, поддерживающей код реализации, который им действительно нужен, например:

extension Text {
    @backDeployed(before: iOS 16.4, macOS 13.3, tvOS 16.4, watchOS 9.4)
    @available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *)
    public func monospaced(_ isActive: Bool) -> Text {
        fatalError("Implementation here")
    }
}

Если бы модификатор был реализован таким образом, во время выполнения Swift использовал бы системную копию SwiftUI, если он уже имеет этот модификатор, в противном случае использовал бы версию, развернутую обратно в iOS 14.0 и аналогичную. На практике, несмотря на то, что новые типы не раскрываются публично и поэтому кажется простым выбором для обратного развертывания, мы не знаем, какие типы SwiftUI использует внутри, поэтому непросто предсказать, что можно, а что нельзя обратно развернуть.

Разрешить неявное self для слабых захватов self после того, как self развернуто

SE-0365 делает еще один шаг к тому, чтобы позволить нам удалить self из замыканий, разрешив неявное self в местах, где был развернут weak self.

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

class TimerController {
    var timer: Timer?
    var fireCount = 0

    init() {
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
            guard let self else { return }
            print("Timer has fired \(fireCount) times")
            fireCount += 1
        }
    }
}

Этот код не был бы скомпилирован до Swift 5.8, потому что оба экземпляра fireCount в замыкании должны были быть записаны как self.fireCount.

Лаконичные волшебные имена файлов

SE-0274 настраивает магический идентификатор #file для использования формата Module/Filename, например. MyApp/ContentView.swift. Раньше #file содержал полный путь к файлу Swift, например /Users/twostraws/Desktop/WhatsNewInSwift/WhatsNewInSwift/ContentView.swift, который излишне длинный и также может содержать вещи, которые вы не хотели бы раскрывать.

Важно: сейчас это поведение не включено по умолчанию. SE-0362 добавляет новый флаг компилятора -enable-upcoming-feature, предназначенный для того, чтобы разработчики могли выбирать новые функции до того, как они будут полностью включены в язык, поэтому, чтобы включить новое поведение #file, вы должны добавить -enable-upcoming-feature ConciseMagicFile в Другие флаги Swift в Xcode.

Если вы хотите сохранить старое поведение после включения этого флага, вам следует вместо этого использовать #filePath:

// New behavior, when enabled
print(#file)

// Old behavior, when needed
print(#filePath)

Предложение Swift Evolution по этому изменению стоит прочитать, потому что в нем упоминаются удивительно большие улучшения в размере двоичного файла и производительности выполнения, а также из-за этого довольно блестящего абзаца, объясняющего, почему использование полного пути часто является плохой идеей:

«Полный путь к исходному файлу может содержать имя пользователя разработчика, подсказки о конфигурации фермы сборки, проприетарные версии или идентификаторы или Sailor Scout, в честь которого вы назвали внешний диск».

Открытие экзистенциальных аргументов для необязательных параметров

SE-0375 расширяет функцию Swift 5.7, которая позволяла нам вызывать универсальные функции с использованием протокола, устраняя небольшое, но раздражающее несоответствие: Swift 5.7 не допускал такого поведения с необязательными параметрами, тогда как Swift 5.8 разрешает.

Например, этот код отлично работал в Swift 5.7, потому что он использует необязательный параметр T:

func double<T: Numeric>(_ number: T) -> T {
    number * 2
}

let first = 1
let second = 2.0
let third: Float = 3

let numbers: [any Numeric] = [first, second, third]

for number in numbers {
    print(double(number))
}

В Swift 5.8 этот же параметр теперь может быть необязательным, например:

func optionalDouble<T: Numeric>(_ number: T?) -> T {
    let numberToDouble = number ?? 0
    return  numberToDouble * 2
}

let first = 1
let second = 2.0
let third: Float = 3

let numbers: [any Numeric] = [first, second, third]

for number in numbers {
    print(optionalDouble(number))
}

В Swift 5.7 это выдавало бы довольно непонятное сообщение об ошибке «Тип ‘any Numeric’ не может соответствовать ‘Numeric’», поэтому приятно видеть, что это несоответствие устранено.

Теперь поддерживаются понижения коллекции в шаблонах приведения.

Это устраняет еще одно небольшое, но потенциально раздражающее несоответствие в Swift, когда понижение коллекции — например. приведение массива ClassA к массиву другого типа, наследуемого от ClassA, в некоторых случаях не допускается.

Например, этот код теперь действителен в Swift 5.8, тогда как раньше он не работал:

class Pet { }
class Dog: Pet {
    func bark() { print("Woof!") }
}

func bark(using pets: [Pet]) {
    switch pets {
    case let pets as [Dog]:
        for pet in pets {
            pet.bark()
        }
    default:
        print("No barking today.")
    }
}

До Swift 5.8 это приводило к сообщению об ошибке: “Collection downcast in cast pattern is not implemented; use an explicit downcast to ‘[Dog]’ instead.”. На практике такой синтаксис, как если бы собаки = домашние животные как? [Собака] { работала нормально, так что я полагаю, что эта ошибка встречается редко. Однако это изменение означает, что несоответствие другого языка устранено, что всегда приветствуется.

И это еще не все!

Есть два дополнительных изменения, о которых стоит кратко упомянуть.

Во-первых, в SE-0368 представлен новый тип StaticBigInt, который должен облегчить добавление новых, более крупных целочисленных типов в будущем.

И, во-вторых, SE-0372 корректирует документацию функций сортировки Swift, чтобы пометить их как stable, что означает, что если два элемента в массиве считаются равными, они останутся в том же относительном порядке в отсортированном массиве — они останутся вместе в отсортированного массива, а также в исходном порядке, который у них был. Сортировка Swift некоторое время была стабильной, но теперь она официальная.

Кроме того, в Swift 5.8 запланированы две дополнительные функции, которые в настоящее время недоступны в текущих бета-версиях Xcode — вполне вероятно, что они будут включены в финальной версии Xcode 14.3, но нам нужно немного подождать, чтобы узнать наверняка.

Вот они:

  • SE-0369: добавьте соответствие CustomDebugStringConvertible к AnyKeyPath, что означает, что выходные данные отладки для пути к ключу будут записаны как \ParentTypeName.PropertyName, а не текущий KeyPath<ParentTypeName, PropertyTypeName>.
  • SE-0381: DiscardingTaskGroups, который создает группы задач, которые отбрасывают свои дочерние задачи, как только они завершаются, вместо того, чтобы нам приходилось ждать их явно.