[Перевод] Топ-5 распространенных практик написания хорошего Swift-кода
Повышаем производительность и читабельность вашего Swift-кода
Эффективность при написании кода заключается не только в достижении желаемой функциональности, но и в создании кода, который будет производительным, удобным в сопровождении и легко читаемым. В Swift то, как вы пишете код, может оказывать сильное влияние на общую производительность и user experience ваших приложений. В этой статье мы рассмотрим некоторые ключевые сравнения и практики программирования на Swift, которые могут значительно повысить эффективность вашего кода.
Благодаря этим сравнениям вы получите более глубокое понимание того, как определенные методы написания кода в Swift могут привести к более оптимизированным, элегантным и эффективным решениям. Независимо от того, являетесь ли вы опытным разработчиком или только начинаете свой путь, эти знания помогут вам писать Swift-код, который не только хорошо работает, но и достигает оптимальной производительности, читабельности и удобства сопровождения.
№1: Циклы for и forEach
Описание и синтаксис
Цикл for: Цикл for-in — это классическая конструкция цикла в Swift, используемая для итерации по последовательностям, например массивам, диапазонам или строкам. Вот пример его использования:
let numbers = [1, 2, 3, 4, 5]
for number in numbers {
print(number)
}
В этом примере цикл for-in
проходит по каждому элементу массива numbers
.
Замыкание forEach
: метод forEach
— это функция высшего порядка, которая принимает замыкание и применяет его к каждому элементу коллекции. Пример:
let numbers = [1, 2, 3, 4, 5]
numbers.forEach { number in
print(number)
}
Это замыкание forEach
выдает нам тот же результат, что и цикл for-in
выше.
Анализ производительности
В большинстве случаев разница в производительности между циклами for-in
и forEach
незначительна, особенно для коллекций небольшого или среднего размера. Однако циклы for-in
могут иметь небольшое преимущество в производительности за счет прямого доступа к элементам коллекции, в то время как forEach
предполагает вызов функции для каждого элемента.
Разница в производительности может стать более заметной в сценариях, предполагающих сложные операции в каждой итерации или при работе с большими наборами данных.
Читабельность и примеры использования
Читабельность:
forEach
часто более лаконичен, и код с его использованием может выглядеть чище, особенно при использовании парадигм функционального программирования.Циклы
for-in
представляют из себя классические циклы и могут быть более читабельны для разработчиков, знакомых с императивным стилем программирования.
Примеры использования:
Используйте циклы
for-in
, когда вам нужно больше контроля над итерацией, например, чтобы выйти из цикла раньше или пропустить некоторые элементы.forEach
идеально подходит для применения определенной операции к каждому элементу коллекции без необходимости задействовать дополнительную логику с применением управляющих конструкций.
Лучшие практики
Если вам нужно выполнить простую операцию над каждым элементом и вам не нужны управляющие конструкции (например,
break
илиcontinue
), отдайте предпочтениеforEach
.В ситуациях, когда необходимо изменить ход выполнения цикла (например, использовать
break
,continue
или изменить переменную цикла), используйте циклfor-in
.Для сложных операций, в которых производительность имеет большое значение, рекомендуется проводить бенчмарки как с
for-in
, так и сforEach
, чтобы принять обоснованное решение.
№2: Проверка диапазона
Описание и синтаксис
Традиционная проверка диапазона: Традиционный способ проверки того, попадает ли значение в диапазон, предполагает использование операторов <=
и >=
. Вот пример:
let x = 25
if 0 <= x && x <= 30 {
print("x is within the range 0 to 30")
}
В этом примере мы проверяем x на предмет того, что он больше или равен 0 и меньше или равен 30.
Использование метода .contains
: Swift предоставляет более лаконичный синтаксис для проверки того, находится ли значение в диапазоне, с помощью метода .contains
:
let x = 25
if (0...30).contains(x) {
print("x is within the range 0 to 30")
}
Здесь мы также проверяем, попадает ли x
в закрытый диапазон от 0 до 30.
Анализ производительности
Для простых проверок диапазона разница в производительности между двумя методами обычно минимальна или даже пренебрежимо мала для повседневных сценариев.
Однако в критически важных для производительности частях приложения, например, в больших циклах или при обработке больших массивов данных, традиционный метод (
if 0 <= x && x <= 30
) может иметь небольшое преимущество в производительности благодаря прямому сравнению, что может быть быстрее, чем вызов метода типа .contains.
Читабельность и контекст
Метод
.contains
предлагает более лаконичную и удобочитаемую форму, особенно для тех, кто знаком с синтаксисом диапазонов Swift. Это может облегчить чтение и сделать код понятным с первого взгляда.Традиционный метод может быть более привычным для разработчиков, пришедших из других языков программирования, и может быть более наглядным в своих намерениях, что некоторые могут предпочесть из соображений выразительности.
Лучшие практики
Для общего использования и в тех случаях, когда читабельность является первостепенным приоритетом, используйте метод
.contains
. Он использует возможности языка Swift и приводит к более чистому коду.В сценариях, где производительность критически важна и каждая миллисекунда на счету, например при обработке больших массивов или сложных алгоритмов, традиционный метод проверки диапазона может оказаться более подходящим.
При выборе между этими методами учитывайте общий контекст кода и уровень знания возможностей Swift вашей команды. Последовательность в стиле написания кода также может быть важным фактором для вашей команды.
№3: Использование Map и циклов for для преобразований
Описание и синтаксис
Цикл for
и преобразования: Традиционный цикл for
можно использовать для преобразования элементов коллекции, перебирая каждый элемент и применяя преобразование (transformation). Вот как это выглядит:
let numbers = [1, 2, 3, 4, 5]
var squaredNumbers = Int
for number in numbers {
squaredNumbers.append(number * number)
}
В этом примере каждое число в массиве numbers
возводится в квадрат и добавляется в массив squaredNumbers
.
Использование map
для преобразования: Функция map
— это функция высшего порядка, которая применяет заданное преобразование к каждому элементу коллекции и возвращает новый массив. Пример:
let numbers = [1, 2, 3, 4, 5]
let squaredNumbers = numbers.map { $0 * $0 }
Эта строка кода достигает того же результата, что и цикл for
, но в более лаконичной форме.
Анализ производительности
Производительность
map
и традиционных цикловfor
в целом сопоставима, особенно для коллекций малого и среднего размера.map
может иметь небольшие накладные расходы из-за того, что является функцией высшего порядка, но обычно они незначительны.Пара бенчмарков, на которые я рекомендовал бы вам взглянуть
Читабельность и функциональное программирование
map
приводит к более лаконичному и читаемому коду, сокращая количество рутинного кода и фокусируясь на операции, выполняемой над каждым элементом.Она хорошо согласуется с принципами функционального программирования, продвигая иммутабельности и stateless-операций.
Циклы
for
могут быть более наглядными и предпочтительнее для более сложных преобразований, где требуется какая-нибудь дополнительная логика.
Лучшие практики
map
предпочтительнее при применении одного преобразования к каждому элементу коллекции, так как это приводит к более лаконичному и декларативному коду.Используйте циклы
for
, когда логика преобразования сложна или когда требуется какие-нибудь дополнительные управляющие конструкции (например,break
или условные операторы).Учитывайте читаемость и сопровождаемость кода как основные факторы, особенно в команде, где ясность намерений имеет решающее значение.
№4: Ленивые свойства и немедленная инициализация
Описание и концепция
Немедленная инициализация: Немедленная инициализация означает, что свойство класса или структуры полностью инициализируется сразу же после создания экземпляра этого класса или структуры. Пример:
class ImageGallery {
var images: [Image]
init() {
// Предполагается, что loadHighResolutionImages() — это функция, которая загружает изображения с диска или из сети
images = loadHighResolutionImages() // Изображения загружаются сразу при инициализации
print("Images are loaded!")
}
func loadHighResolutionImages() -> [Image] {
// Сложная логика загрузки
// ...
return [Image(named: "image1"), Image(named: "image2"), Image(named: "image3")]
}
// ... остальные свойства и методы ...
}
Использование:
let gallery = ImageGallery() // "Images are loaded!" выводится немедленно
В этом примере изображения загружаются в память, как только создается экземпляр ImageGallery
.
Ленивая инициализация: lazy
свойства в Swift — это свойства, которые инициализируются только при первом обращении к ним. Это полезно для свойств, изначальные значения которых требуют больших вычислительных затрат или не нужны сразу же после создания экземпляра. Пример:
class ImageGallery {
lazy var images: [Image] = {
// Этот блок выполняется только при первом обращении к 'images'.
let loadedImages = loadHighResolutionImages()
print("Images are lazily loaded!")
return loadedImages
}()
func loadHighResolutionImages() -> [Image] {
// Сложная логика загрузки
// ...
return [Image(named: "image1"), Image(named: "image2"), Image(named: "image3")]
}
// ... остальные свойства и методы ...
}
Использование:
let gallery = ImageGallery() // Изображения в этот момент еще НЕ начинают загружаться
print("Gallery created")
gallery.images // При первом обращении к 'images' происходит загрузка: выводится "Images are lazily loaded!".
Здесь изображения загружаются только при первом обращении к ним, что позволяет экономить ресурсы, если они в конечном итоге окажутся невостребованными.
Анализ производительности
Время начальной загрузки: использование ленивых свойств может значительно сократить время начальной загрузки приложения, поскольку дорогостоящие вычисления откладываются до тех пор, пока они не понадобятся.
Использование памяти: ленивые свойства могут сократить использование памяти, особенно в случаях, когда свойство может не требоваться немедленно для каждого экземпляра.
Общая производительность: Хотя ленивые свойства могут повысить производительность в определенных сценариях, они потенциально могут создавать накладные расходы при первом обращении к ним.
Примеры использования и управление памятью
Ресурсоемкие свойства: Используйте
lazy
для свойств, инициализация которых требует значительных ресурсов, например, для загрузки больших наборов данных, изображений или сложных вычислений.Необязательные свойства:
lazy
также полезно для свойств, которые могут использоваться не в каждом сеансе или экземпляре, например, необязательные компоненты пользовательского интерфейса.Управление памятью: Поскольку ленивые свойства инициализируются только при обращении к ним, они могут помочь более эффективно управлять памятью, особенно в средах с ограниченным объемом памяти.
Лучшие практики
Используйте ленивые свойства, когда стоимость инициализации высока и не все экземпляры класса нуждаются в этом свойстве.
Избегайте использования дескриптора
lazy
для свойств, которые требуются немедленно или для всех экземпляров, так как это добавляет ненужную сложность.Будьте внимательны к потокобезопасности при обращении к ленивым свойствам из нескольких потоков; при необходимости рассмотрите механизмы синхронизации.
Оцените, соответствует ли использование дескриптора
lazy
для конкретного свойства жизненному циклу приложения и потребностям пользователей.
№5: Структуры и классы: Выбираем между ссылочными и значимыми типами
Описание и синтаксис
Struct
(значимый тип): В Swift struct
— это значимый тип. Когда вы присваиваете значимый тип переменной, константе или передаете его в функцию, он копируется. Вот пример:
struct Point {
var x: Int
var y: Int
}
var point1 = Point(x: 0, y: 0)
var point2 = point1 // point2 является копией point1
point2.x = 5 // Изменение point2 не влияет на point1
В этом примере point2
является отдельной копией point1
. Изменения в point2
не влияют на point1
.
Класс (ссылочный тип): class
— это ссылочный тип. В отличие от значимых типов, ссылочные типы не копируются при присвоении переменной или константе, а также при передаче в функцию. Вместо этого используется ссылка на один и тот же существующий экземпляр. Пример:
class Box {
var width: Int
var height: Int
init(width: Int, height: Int) {
self.width = width
self.height = height
}
}
var box1 = Box(width: 10, height: 20)
var box2 = box1 // box2 ссылается на тот же экземпляр, что и box1
box2.width = 50 // Изменение box2 также влияет на box1
Здесь box2
и box1
ссылаются на один и тот же экземпляр, поэтому изменения в box2
отражаются и в box1
.
Анализ производительности
Управление памятью: структуры, как правило, более эффективны с точки зрения управления памятью, поскольку они выделяются в стеке, в то время как классы выделяются в куче.
Скорость доступа: Доступ к значимым типам может быть быстрее, чем к ссылочным типам, благодаря хранению в стеке и отсутствию операций подсчета ссылок.
Поведение при копировании: Для больших структур копирование может стать дорогостоящим. В таких случаях классы могут быть более эффективными, поскольку они передают ссылки, а не копируют целые структуры.
Мутабельность и безопасность потоков
Мутабельность: В структурах каждый экземпляр владеет своими данными, поэтому изменения не влияют на другие экземпляры, что делает код более предсказуемым. Классы же разделяют один экземпляр, поэтому изменения в одном месте могут повлиять на другие части вашей программы.
Потокобезопасность: структуры по своей природе более безопасны в параллельной среде, поскольку они не разделяют память. Для обеспечения потокобезопасности классам могут потребоваться дополнительные механизмы синхронизации (например, блокировки).
Лучшие практики
Предпочитайте структуры для небольших, простых структур данных, которые копируются, а не используются совместно.
Используйте классы, когда вам нужно наследование, идентичность (например, синглтоны) или когда вы имеете дело с большими и сложными данными, которые не должны копироваться.
Рассмотрите возможность использования структур для повышения потокобезопасности и производительности при выполнении параллельных операций.
Оцените, какая семантика — семантика ссылок (предлагаемая классами) или семантика значений (предлагаемая структурами) — больше подходит для вашего конкретного сценария использования.
Заключение: Повышение качества вашего Swift-кода
В этом разборе азов написания кода на Swift мы увидели, как выбор правильных инструментов и подходов может значительно повысить производительность, читабельность и удобство сопровождения кода. От понимания нюансов циклов for
и forEach
до эффективного использования map
, ленивых свойств и выбора между структурами и классами — каждое решение играет ключевую роль в создании эффективного и элегантного кода. Продолжая свой путь в разработке на Swift, помните об этих знаниях. Это не просто интересные приемы, а ступеньки на пути к овладению искусством написания элегантного Swift-кода.
В заключение приглашаем на вебинар, на котором мы напишем небольшое приложений с использованием MusicKit.
На WWDC 2023 Apple представили новые интерактивные виджеты с поддержкой расширенного функционала по работе с сервисами Apple. Виджеты теперь не только красивые, но и действительно полезные.
В процессе нашего вебинара мы создадим приложение с использованием музыкального сервиса MusicKit и интерактивного виджета к нему. У вас будет возможность ознакомиться со следующими аспектами: SwiftUI для создания виджетов, WidgetKit и AppIntents для обеспечения интерактивности. Урок состоится уже сегодня вечером — успевайте записаться на странице онлайн-курса OTUS «iOS Developer. Professional».