top of page
Фото автораБогдан Архипчук

Що нового у Swift 6: Race Condition, методи throw, Swift Testing та інші зміни



У вересні 2024 року відбувся реліз нової версії Swift 6. Богдан Архипчук, iOS Developer у компанії OBRIO з екосистеми Genesis поділився враженнями про ключові оновлення та зміни. 


Цього року Apple сфокусувалися на підвищенні Data Race Safety своєї мови, але також не забули про інші фічі — менш глобальні, але важливі для інтуїтивної роботи зі Swift. Головні оновлення, представлені на WWDC: 


  • режим для запобігання race condition у паралельному коді;

  • типізовані throw-методи для детальнішої інформації про помилки та роботи з ними;

  • ітерація Parameter Packs для кращої роботи з загальними параметрами;

  • кастомні самарі LLDB за допомогою @DebugDescription для кращого дебагінгу;

  • Swift Testing для простішого тестування написаного коду.


Реліз містить досить багато вагомих змін, тому розглянемо їх детальніше.



Режим для запобігання race condition


Race condition — це вічна проблема багатьох мов програмування, і Swift не є виключенням. Ці перегони можуть спричинити неочевидну та часом некоректну поведінку коду. Це досить важко виявити та дебажити, якщо не знати, де вони відбуваються. Apple вирішили зробити життя розробників менш стресовим і додали концепцію Async/Await у Swift 5.5. Це дало змогу керувати асинхронними операціями та зменшувати ризик перегонів. 


Цього року вони пішли далі, і Swift 6 стає офіційно Data Race Safe. Це означає, що починаючи з цієї версії, Swift видаватиме помилки під час компіляції про потенційні стани race condition. Раніше вони максимум позначалися застереженням, на яке можна було закрити очі. Також завдяки покращенням протоколу Sendable, Swift потенційно має краще розуміти місця, де можуть виникнути такі ситуації, і убезпечує вашу логіку у такий спосіб.


Також додана можливість міграції за модулями. Тепер немає потреби переводити весь проєкт одразу, а можна обирати, які фреймворки всередині будуть використовувати даний функціонал, а які — ні.



Типізовані throw методи


З додаванням асинхронності у Swift 5.5 ми також отримали методи, які можуть викидати помилки у разі невдалого виконання коду. Проте проблема з традиційним throws полягала в тому, що ми завжди могли кидати лише тип, який підпадає під загальний протокол Error. З одного боку, це протокол, який можна додати до будь-якого іншого типу як наслідування, але коли доходить до роботи з Error, єдине, що ми могли зробити — дістати локалізований опис.

func parseRecord(from string: String) throws -> Record { 
  // ... 
}

do { 
     let record = await parseRecord(from: “Record Name”)
} catch { 
     error.localizedDescription // єдина корисна проперті.
}

Зі Swift 6, у нас зʼявляється можливість вказати конкретний тип помилки, який ми очікуємо отримати.  Тепер наш код вище може виглядати так: 

struct MyCustomError: Error { 

      let description: String 
      let recordName: String 
} 

func parseRecord(from string: String) throws(MyCustomError) -> Record { 
  // ... 
}

do { 
     let record = await parseRecord(from: “Record Name”)
} catch { 
     error.recordName
}

Це неймовірно крута фіча, яка дозволить більш точно обробляти помилки, а також навіть будувати додаткову логіку для їхнього хендлінгу. Цікавий факт реалізації: зі Swift 6 всі методи будуть фактично «кидати» щось. 

func parseRecord(from string: String) -> Record { 
  // ... 
}

Тепер такий метод — те саме, що:

func parseRecord(from string: String) throws(Never) -> Record { 
  // ... 
}

Ітерація Parameter Packs


Пакети параметрів, запроваджені у Swift 5.9, дають змогу писати узагальнені функції, які абстрагуються від кількості аргументів. Це усуває необхідність мати перевантажені копії однієї і тієї ж загальної функції для одного аргументу, двох чи трьох тощо. У Swift 6.0 ітерація пакетів робить роботу з пакетами параметрів простішою, ніж будь-коли.


Розглянемо коди нижче: 

struct Request<Payload> {

     let payload: Payload
}

func handleBatch<Payload1>(_ request1: Request<Payload1>) {
   // ..
}

 func handleBatch<Payload1, Payload2>(_ request1: Request<Payload1>, _ request2: Request<Payload2>) {
   //…
}


func  handleBatch<Payload1, Payload2, Payload3>(_ request1: Request<Payload1>, _ request2: Request<Payload2>, _ request3: Request<Payload3>) {
   // ...
}

// Для виклику такого потрібен вже новий метод
handleBatch (
   Request (payload: 123),
   Request (payload:"TEST"),
   Request (payload: true)
   Request (payload: Data((16, 32, 641))
)

Замість того, щоби писати новий метод щоразу, з 5.9 версії зʼявився такий спосіб: 

func handleBatch<each Payload>(_ requests: repeat Request<each Payload>) {
  // ...
}


handleBatch (
   Request (payload: 123),
   Request (payload:"TEST"),
   Request (payload: true)
   Request (payload: Data((16, 32, 641))
)

Але проблема такого виклику у відсутності на той момент можливості зручно працювати з повторюваними загальними змінними, які ми передаємо в метод. У Swift 6 зʼявилася можливість використовувати звичний для всіх for loop:

func handleBatch<each Payload>(_ requests: repeat Request<each Payload>) {
  for request in requests each request { 
    // ... 
  }
}




Кастомні самарі LLDB за допомогою @DebugDescription


Типи, що відповідають CustomDebugStringConvertible, мають властивість debugDescription. Вона повертає рядок, що описує об'єкт. У LLDB команда po викликає цю обчислювану властивість для об'єкта. На відміну від цього, команда p використовує форматори зведення типів LLDB для безпосереднього форматування об'єкта за допомогою збережених значень його властивостей.@DebugDescription — це новий макрос у стандартній бібліотеці, який дозволяє вам визначати зведення типів LLDB для ваших власних типів безпосередньо у коді. Він обробляє властивість debugDescription, перетворюючи прості інтерполяції рядків зі збереженими властивостями у зведення типів LLDB. Це дозволяє LLDB використовувати ваше власне форматування навіть при використанні p. Макрос може використовувати наявну відповідність до CustomDebugStringConvertible, або ви можете надати окремий опис інтерполяції рядка лише для використання у команді p LLDB. 


Надання окремого рядка опису LLDB може бути корисним, якщо ваша реалізація CustomDebugStringConvertible не відповідає вимогам макросу @DebugDescription, або якщо ви знайомі із синтаксисом рядка опису LLDB і хочете використовувати безпосередньо його.


Наприклад:

@DebugDescription 
struct Organization: CustomDebugStringConvertible { 
var id: String 
var name: String 
var manager: Person 

// ... 

var debugDescription: String { "#\(id) \(name) [\(manager.name)]" } }

В дебазі виглядатиме ось так:

(lldb) p myOrg 
(Organization) myOrg = "`#100 Worldwide Travel [Jonathan Swift]`"

Це невелика, але дуже корисна зміна для покращення процесу дебагінгу, який розробники проходять ледь не щодня, намагаючись знайти помилки. 



Swift Testing


На останок, Apple вирішили зробити автоматизоване тестування ще простішим і створило нову бібліотеку Swift Testing. Вона повністю орієнтована на потреби розробників, які працюють з Swift, і включає описове API, яке дозволяє легко організувати будь-які тести. Бібліотека активно використовує нову технологію макросів, яка дозволяє на рівні компіляції генерувати додаткові методи і одразу використовувати їх в тестах. 


Є основний макрос @Test, який дає зрозуміти, що даний метод є тестом, а також макрос #expect, який виконує функцію старого XCTestExpectation.  

@Test
func helloWorld() {
  let greeting = "Hello, world!"
  #expect(greeting == "Hello") // Expectation failed: (greeting → "Hello, world!") == "Hello"
}

Також можна налаштувати поведінку тестів або наборів тестів за допомогою ознак, вказаних у вашому коді. Ознаки можуть описувати умови виконання тесту, наприклад, на якому пристрої він повинен виконуватися, або обмежувати тест певними версіями операційних систем. Ознаки також можуть допомогти ефективно використовувати безперервну інтеграцію, вказуючи обмеження часу виконання тестів.

@Test(.enabled(if: AppFeatures.isCommentingEnabled))
func videoCommenting() async throws {
    let video = try #require(await videoLibrary.video(named: "A Beach"))
    #expect(video.comments.contains("So picturesque!"))
}

Або навіть додати опис та набір базових кейсів, аби не перевикористовувати один і той самий метод декілька разів:

@Test("Continents mentioned in videos", arguments: [
    "A Beach",
    "By the Lake",
    "Camping in the Woods"
])
func mentionedContinents(videoName: String) async throws {
    let videoLibrary = try await VideoLibrary()
    let video = try #require(await videoLibrary.video(named: videoName))
    #expect(video.mentionedContinents.count <= 3)
}

У цілому новий фреймворк виглядає цікавим та інтуїтивно зрозумілим, для тих хто хоче почати писати тести, а також дає змогу робити це швидко і лаконічно.


Отже, Swift 6 приніс значні покращення та нові можливості, що роблять мову ще більш зручною для розробників. Проте нова версія також вимагає глибшого розуміння асинхронного програмування, оскільки Apple робить великий акцент саме на цьому. Як завжди, при міграції на нову версію можуть виникнути певні складнощі, але вони безумовно варті зусиль, адже це значно підвищить безпеку коду. Детальніше з оновленням можна ознайомитись в документації


Чи пробували ви користуватись новими фічами? Буду радий, якщо поділитесь своїм досвідом зі мною в LinkedIn.



© 2035 by Business Name. Made with Wix Studio™

bottom of page