Ошибки, о которых большинство разработчиков Swift не подозревают
Опубликовано: 2022-03-11Исходя из опыта работы с Objective-C, вначале я чувствовал, что Swift меня сдерживает. Swift не давал мне прогресса из-за своей строго типизированной природы, которая временами приводила в бешенство.
В отличие от Objective-C, Swift применяет многие требования во время компиляции. Вещи, которые смягчены в Objective-C, такие как тип id
и неявные преобразования, отсутствуют в Swift. Даже если у вас есть Int
и Double
и вы хотите их сложить, вам придется явно преобразовать их в один тип.
Кроме того, необязательные параметры являются фундаментальной частью языка, и, хотя они представляют собой простую концепцию, требуется некоторое время, чтобы к ним привыкнуть.
В начале вы можете принудительно развернуть все, но это в конечном итоге приведет к сбоям. Когда вы знакомитесь с языком, вам начинает нравиться, что у вас почти нет ошибок во время выполнения, поскольку многие ошибки обнаруживаются во время компиляции.
Большинство программистов Swift уже имеют значительный предыдущий опыт работы с Objective-C, что, среди прочего, может привести к тому, что они будут писать код Swift, используя те же методы, с которыми они знакомы в других языках. И это может привести к некоторым неприятным ошибкам.
В этой статье мы расскажем о наиболее распространенных ошибках, которые допускают разработчики Swift, и о том, как их избежать.
1. Опции принудительной распаковки
Переменная необязательного типа (например, String?
) может содержать или не содержать значение. Когда они не содержат значения, они равны nil
. Чтобы получить значение опционала, сначала вам нужно развернуть его, а это можно сделать двумя разными способами.
Одним из способов является необязательная привязка с использованием if let
или guard let
, то есть:
var optionalString: String? //... if let s = optionalString { // if optionalString is not nil, the test evaluates to // true and s now contains the value of optionalString } else { // otherwise optionalString is nil and the if condition evaluates to false }
Во-вторых, принудительно развернуть с помощью !
оператор или использование неявно развернутого необязательного типа (например, String!
). Если необязательное значение равно nil
, принудительное развертывание вызовет ошибку времени выполнения и завершит приложение. Кроме того, попытка доступа к значению неявно развернутого опционала вызовет то же самое.
Иногда у нас есть переменные, которые мы не можем (или не хотим) инициализировать в инициализаторе класса/структуры. Таким образом, мы должны объявить их как необязательные. В некоторых случаях мы предполагаем, что они не будут nil
в определенных частях нашего кода, поэтому мы принудительно разворачиваем их или объявляем их как неявно развернутые необязательные параметры, потому что это проще, чем постоянно выполнять необязательную привязку. Это следует делать с осторожностью.
Это похоже на работу с IBOutlet
s, которые являются переменными, которые ссылаются на объект в пере или раскадровке. Они не будут инициализированы при инициализации родительского объекта (обычно это контроллер представления или пользовательский UIView
), но мы можем быть уверены, что они не будут равны nil
при viewDidLoad
(в контроллере представления) или awakeFromNib
(в представлении), и поэтому мы можем безопасно получить к ним доступ.
В общем, лучше всего избегать принудительной распаковки и использования неявно распаковываемых опций. Всегда учитывайте, что необязательный параметр может быть nil
, и обрабатывайте его соответствующим образом, либо используя необязательную привязку, либо проверяя, не является ли он nil
, прежде чем принудительно разворачивать, или обращаясь к переменной в случае неявно развернутого необязательного.
2. Незнание подводных камней циклов сильных ссылок
Цикл сильной ссылки существует, когда пара объектов сохраняет сильную ссылку друг на друга. Это не является чем-то новым для Swift, поскольку у Objective-C та же проблема, и ожидается, что опытные разработчики Objective-C должным образом справятся с ней. Важно обращать внимание на сильные ссылки и на то, что на что ссылается. В документации Swift есть раздел, посвященный этой теме.
Особенно важно управлять ссылками при использовании замыканий. По умолчанию замыкания (или блоки) сохраняют сильную ссылку на каждый объект, на который есть ссылка внутри них. Если какой-либо из этих объектов имеет сильную ссылку на само замыкание, у нас есть цикл сильной ссылки. Необходимо использовать списки захвата , чтобы правильно управлять захватом ссылок.
Если существует вероятность того, что экземпляр, захваченный блоком, будет освобожден до вызова блока, вы должны захватить его как слабую ссылку , которая будет необязательной, поскольку может быть nil
. Теперь, если вы уверены, что захваченный экземпляр не будет освобожден в течение времени жизни блока, вы можете захватить его как бесхозную ссылку . Преимущество использования unowned
вместо weak
заключается в том, что ссылка не будет необязательной, и вы можете использовать значение напрямую, без необходимости его разворачивать.
В следующем примере, который вы можете запустить в Xcode Playground, класс Container
имеет массив и необязательное закрытие, которое вызывается при каждом изменении его массива (для этого используются наблюдатели свойств). Класс Whatever
имеет экземпляр Container
, и в его инициализаторе он назначает замыкание для arrayDidChange
и это замыкание ссылается на self
, тем самым создавая сильную связь между экземпляром Whatever
и замыканием.
struct Container<T> { var array: [T] = [] { didSet { arrayDidChange?(array: array) } } var arrayDidChange: ((array: [T]) -> Void)? } class Whatever { var container: Container<String> init() { container = Container<String>() container.arrayDidChange = { array in self.f(array) } } deinit { print("deinit whatever") } func f(s: [String]) { print(s) } } var w: Whatever! = Whatever() // ... w = nil
Если вы запустите этот пример, вы заметите, что deinit whatever
никогда не печатается, что означает, что наш экземпляр w
не освобождается из памяти. Чтобы исправить это, мы должны использовать список захвата, чтобы сильно не захватывать self
:
struct Container<T> { var array: [T] = [] { didSet { arrayDidChange?(array: array) } } var arrayDidChange: ((array: [T]) -> Void)? } class Whatever { var container: Container<String> init() { container = Container<String>() container.arrayDidChange = { [unowned self] array in self.f(array) } } deinit { print("deinit whatever") } func f(s: [String]) { print(s) } } var w: Whatever! = Whatever() // ... w = nil
В этом случае мы можем использовать unowned
, потому что self
никогда не будет равно nil
во время жизни замыкания.
Хорошей практикой является почти всегда использование списков захвата, чтобы избежать циклов ссылок, что уменьшит утечки памяти и, в конце концов, сделает код более безопасным.
3. Использование self
везде
В отличие от Objective-C, в Swift нам не требуется использовать self
для доступа к свойствам класса или структуры внутри метода. Мы должны сделать это только в замыкании, потому что оно должно захватить self
. Использование self
там, где это не требуется, не является ошибкой, оно работает нормально, ошибок и предупреждений не будет. Однако зачем писать больше кода, чем нужно? Кроме того, важно, чтобы ваш код был последовательным.
4. Незнание типа ваших типов
Swift использует типы значений и ссылочные типы . Кроме того, экземпляры типа значения демонстрируют несколько иное поведение экземпляров ссылочных типов. Незнание того, к какой категории относится каждый из ваших экземпляров, приведет к ложным ожиданиям в отношении поведения кода.
В большинстве объектно-ориентированных языков, когда мы создаем экземпляр класса и передаем его другим экземплярам и в качестве аргумента методам, мы ожидаем, что этот экземпляр будет везде одинаковым. Это означает, что любое изменение в нем будет отражено везде, потому что, по сути, у нас есть просто набор ссылок на одни и те же данные. Объекты, демонстрирующие такое поведение, являются ссылочными типами, а в Swift все типы, объявленные как class
, являются ссылочными типами.
Далее у нас есть типы значений, которые объявляются с помощью struct
или enum
. Типы значений копируются, когда они присваиваются переменной или передаются в качестве аргумента функции или методу. Если вы что-то измените в скопированном экземпляре, исходный не изменится. Типы значений являются неизменяемыми . Если вы присваиваете новое значение свойству экземпляра типа значения, например CGPoint
или CGSize
, с изменениями создается новый экземпляр. Вот почему мы можем использовать наблюдатели свойств в массиве (как в приведенном выше примере в классе Container
), чтобы уведомлять нас об изменениях. Что на самом деле происходит, так это то, что с изменениями создается новый массив; он присваивается свойству, а затем вызывается didSet
.

Таким образом, если вы не знаете, что объект, с которым вы имеете дело, имеет тип ссылки или значения, ваши ожидания относительно того, что будет делать ваш код, могут быть совершенно неверными.
5. Не использовать весь потенциал перечислений
Когда мы говорим о перечислениях, мы обычно думаем об основном перечислении C, которое представляет собой просто список связанных констант, которые являются целыми числами внизу. В Swift перечисления намного мощнее. Например, вы можете присвоить значение каждому случаю перечисления. У перечислений также есть методы и доступные только для чтения/вычисляемые свойства, которые можно использовать для обогащения каждого случая дополнительной информацией и деталями.
Официальная документация по перечислениям очень интуитивно понятна, а документация по обработке ошибок представляет несколько вариантов использования дополнительных возможностей перечислений в Swift. Кроме того, ознакомьтесь со следующим обширным исследованием перечислений в Swift, чтобы узнать почти все, что вы можете с ними делать.
6. Неиспользование функциональных возможностей
Стандартная библиотека Swift предоставляет множество методов, которые являются фундаментальными в функциональном программировании и позволяют нам делать многое с помощью всего одной строки кода, например, отображать, уменьшать и фильтровать.
Рассмотрим несколько примеров.
Скажем, вам нужно рассчитать высоту табличного представления. Учитывая, что у вас есть подкласс UITableViewCell
, например:
class CustomCell: UITableViewCell { // Sets up the cell with the given model object (to be used in tableView:cellForRowAtIndexPath:) func configureWithModel(model: Model) // Returns the height of a cell for the given model object (to be used in tableView:heightForRowAtIndexPath:) class func heightForModel(model: Model) -> CGFloat }
Предположим, у нас есть массив экземпляров модели modelArray
; мы можем вычислить высоту представления таблицы с помощью одной строки кода:
let tableHeight = modelArray.map { CustomCell.heightForModel($0) }.reduce(0, combine: +)
map
выведет массив CGFloat
, содержащий высоту каждой ячейки, а reduce
добавит их.
Если вы хотите удалить элементы из массива, вы можете сделать следующее:
var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } for s in supercars { if !isSupercar(s), let i = supercars.indexOf(s) { supercars.removeAtIndex(i) } }
Этот пример не выглядит элегантным и не очень эффективным, поскольку мы вызываем indexOf
для каждого элемента. Рассмотрим следующий пример:
var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } for (i, s) in supercars.enumerate().reverse() { // reverse to remove from end to beginning if !isSupercar(s) { supercars.removeAtIndex(i) } }
Теперь код более эффективен, но его можно улучшить с помощью filter
:
var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } supercars = supercars.filter(isSupercar)
В следующем примере показано, как можно удалить все UIView
, соответствующие определенным критериям, например фрейм, пересекающий определенный прямоугольник. Вы можете использовать что-то вроде:
for v in view.subviews { if CGRectIntersectsRect(v.frame, rect) { v.removeFromSuperview() } } ``` We can do that in one line using `filter` ``` view.subviews.filter { CGRectIntersectsRect($0.frame, rect) }.forEach { $0.removeFromSuperview() }
Тем не менее, мы должны быть осторожны, потому что у вас может возникнуть соблазн связать пару вызовов этих методов, чтобы создать причудливую фильтрацию и преобразование, что может закончиться одной строкой нечитаемого спагетти-кода.
7. Оставаться в зоне комфорта и не пытаться программировать на основе протокола
Swift считается первым протокольно-ориентированным языком программирования , как упоминалось в сессии WWDC Protocol-Oriented Programming in Swift. По сути, это означает, что мы можем моделировать наши программы на основе протоколов и добавлять поведение к типам, просто приспосабливаясь к протоколам и расширяя их. Например, если у нас есть протокол Shape
, мы можем расширить CollectionType
(который соответствует таким типам, как Array
, Set
, Dictionary
) и добавить к нему метод, который вычисляет общую площадь с учетом пересечений.
protocol Shape { var area: Float { get } func intersect(shape: Shape) -> Shape? } extension CollectionType where Generator.Element: Shape { func totalArea() -> Float { let area = self.reduce(0) { (a: Float, e: Shape) -> Float in return a + e.area } return area - intersectionArea() } func intersectionArea() -> Float { /*___*/ } }
Оператор, where Generator.Element: Shape
— это ограничение, указывающее, что методы в расширении будут доступны только в экземплярах типов, соответствующих CollectionType
, которые содержат элементы типов, соответствующих Shape
. Например, эти методы можно вызывать для экземпляра Array<Shape>
, но не для экземпляра Array<String>
. Если у нас есть класс Polygon
, соответствующий протоколу Shape
, то эти методы будут доступны и для экземпляра Array<Polygon>
.
С расширениями протокола вы можете дать реализацию по умолчанию для методов, объявленных в протоколе, которые затем будут доступны во всех типах, соответствующих этому протоколу, без необходимости вносить какие-либо изменения в эти типы (классы, структуры или перечисления). Это делается широко во всей стандартной библиотеке Swift, например, map
и reduce
определены в расширении CollectionType
, и эта же реализация используется такими типами, как Array
и Dictionary
, без какого-либо дополнительного кода.
Это поведение похоже на миксины из других языков, таких как Ruby или Python. Просто согласовывая протокол с реализациями методов по умолчанию, вы добавляете функциональность к своему типу.
Программирование, ориентированное на протоколы, на первый взгляд может показаться довольно неудобным и бесполезным, что может заставить вас игнорировать его и даже не попробовать. Этот пост дает хорошее представление об использовании протоколо-ориентированного программирования в реальных приложениях.
Как мы узнали, Swift — не игрушечный язык
Первоначально Swift был встречен с большим скептицизмом; люди, похоже, думали, что Apple собирается заменить Objective-C игрушечным языком для детей или чем-то для непрограммистов. Тем не менее, Swift оказался серьезным и мощным языком, который делает программирование очень приятным. Поскольку он строго типизирован, в нем трудно сделать ошибки, и поэтому трудно перечислить ошибки, которые вы можете допустить в языке.
Когда вы привыкнете к Swift и вернетесь к Objective-C, вы заметите разницу. Вы пропустите приятные функции, которые предлагает Swift, и вам придется писать утомительный код на Objective-C, чтобы добиться того же эффекта. В других случаях вы столкнетесь с ошибками времени выполнения, которые Swift поймал бы во время компиляции. Это отличное обновление для программистов Apple, и многое еще предстоит сделать по мере развития языка.