Swift Pattern Matching

 
 

Статья перевод, оригинал тут. Далее повествование идет от лица автора, я лишь в меру своих сил попытался литературно перевести на русский. Также автор судя по всему использует Swift 2, по мере возможности я адаптировал примеры для Swift 3.

Среди всех новых конструкций языка Swift по отношению к Objective-C выделяется конструкция switch. Которая, в отличии от языка Objective-C не просто альтернатива набору последовательных операций if-elseif-else, но предоставляет расширенный набор вариантов выбора.

Конструкция switch в swift позволяет делать гораздо больше. И в этой статье я попытаюсь рассказать про эти новые возможности более подробно. Я буду игнорировать те аспекты, которые не несут ничего нового по отношению к Objective-C. Базовые идеи для этой статьи появились в Июле 2014, но большинство вариантов приводило к падению компилятора, поэтому я откладывал написание этой статьи.

Погружаемся

Основная функция оператора switch конечно pattern matching (устоявшийся термин, переводить не стал, прим. переводчика), способность классифицировать значения и сопоставлять их вариантам выбора.

// Пример наихудшего конвертера binary -> decimal в истории
let bool1 = 1
let bool2 = 0
switch (bool1, bool2) {
   case (0, 0): print("0")
   case (0, 1): print("1")
   case (1, 0): print("2")
   case (1, 1): print("3")
}

Pattern matching (PM) давно существует в таких языках программирования, как Haskell, Erlang, Scala или Prolog. Это замечательно, так как позволяет нам увидеть, как PM используется в этих языках для решения практических проблем, и перенять хорошие для решения собственных задач.

Торговый движок

К вам обратились дельцы с Wall Street (ну или хотя бы с ММВБ), им нужна торговая платформа на iOS устройствах. Так как это торгвая платформа, вы определяете перечисление для сделок.

Первый черновик

enum Trades {
    case buy(stock: String, amount: Int, stockPrice: Float)
    case sell(stock: String, amount: Int, stockPrice: Float)
}

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

/**
 - parameter stock: The stock name
 - parameter amount: The amount, negative number = sell, positive = buy
*/
func process(stock: String, _ amount: Int) {
    print ("\(amount) of \(stock)")
}

Следующий шаг - обработать приказы. И тут виден потенциал для применения pattern matching, поэтому напишем так:

let trade = Trades.buy(stock: "APPL", amount: 200, stockPrice: 115.5)

switch trade {
case .buy(let stock, let amount, _):
    process(stock, amount)
case .sell(let stock, let amount, _):
    process(stock, amount * -1)
}

// Prints "buy 200 of APPL"

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

Отлично, оправляемся на Wall Street для того, чтобы продемонстрировать нашу замечательную торговую платформу. Однако как обычно, на практике все не так, как в теории. Сделка не так проста, как вам рассказывают.

  • Необходимо рассчитать комиссию, которая отличается в зависимости от типа трейдера.
  • Чем меньше игрок, тем больше комиссия
  • Большие компании имеют больший приоритет

Поэтому было решено выдать новый API для решения этих проблем:

func processSlow(_ stock: String, _ amount: Int, _ fee: Float) { print("slow") }
func processFast(_ stock: String, _ amount: Int, _ fee: Float) { print("fast") }

Типы трейдоров

Поэтому возвращаемся и добавляем еще одно перечисление. Тип трейдера будет частью всех сделок.

enum TraderType {
case person
case company
}

enum Trades {
    case buy(stock: String, amount: Int, stockPrice: Float, type: TraderType)
    case sell(stock: String, amount: Int, stockPrice: Float, type: TraderType)
}

Чтож, как лучше всего имплементировать это новое ограничение? Можно просто добавить каскадную конструкцию if-else для .buy и для .sell, но это приведет к каскадному коду, и неминуемо потеряет выразительность, и кто знает, что акулы с Wall Street придумают для нас завтра. Поэтому реализуем эти новые ограничения с применением pattern matching:

let trade = Trades.sell(stock: "GOOG", amount: 100, stockPrice: 666.0, type: TraderType.company)

switch trade {
case let .buy(stock, amount, _, TraderType.person):
    processSlow(stock, amount, 5.0)
case let .sell(stock, amount, _, TraderType.person):
    processSlow(stock, -1 * amount, 5.0)
case let .buy(stock, amount, _, TraderType.company):
    processFast(stock, amount, 2.0)
case let .sell(stock, amount, _, TraderType.company):
    processFast(stock, -1 * amount, 2.0)
}

Достаточно выразительно, не так ли? Кроме того, для краткости мы изменили .buy(let stock, let amount) на let .buy(stock, amount). Суть та же, букв меньше.

Охрана! Охрана!

Тут видимо имелось ввиду игра слов, синтаксическая конструкция guard также означает и охрану, прим. переводчика

Вы снова презентуете ваше творение дельцам с Wall Street, и снова появляется новая хотелка (вам все-таки стоило изначально попросить более детальное ТЗ).

  • Приказы на продажу общим объемом более $ 1 000 000 всегда обрабатываются быстрым процессингом, даже если этот приказ поступил от частного лица
  • Приказы на покупку объемом менее $ 1 000 всегда обрабатываются медленным процессингом.

Это стало бы адом (и лапшой наверное), если бы мы попытались реализовать это через традиционное дерево if’ов. Но у нас есть Swift и конструкция switch. Swift предоставляет дополнительные конструкции для ограничения диапазонов действия переменных.

Поэтому наш код нужно поправить всего чуть-чуть для отражения новых требований.

let trade = Trades.buy(stock: "GOOG", amount: 1000, stockPrice: 666.0, type: TraderType.person)

switch trade {
case let .buy(stock, amount, _, TraderType.person):
    processSlow(stock, amount, 5.0)

case let .sell(stock, amount, price, TraderType.person)
    where price * Float(amount) > 1000000:
    processFast(stock, -1 * amount, 5.0)

case let .sell(stock, amount, _, TraderType.person):
    processSlow(stock, -1 * amount, 5.0)

case let .buy(stock, amount, price, TraderType.company)
    where price * Float(amount) < 1000:
    processSlow(stock, amount, 2.0)

case let .buy(stock, amount, _, TraderType.company):
    processFast(stock, amount, 2.0)

case let .sell(stock, amount, _, TraderType.company):
    processFast(stock, -1 * amount, 2.0)
}

Этот код по прежнему остается как достаточно простым для понимания, так и открытым к новым изменениям.

Ура-ура, мы успешно реализовали нашу торговую платформу. Однако, полученное решение все-таки содержит небольшое количество повторяющегося кода. Но может быть есть еще какие-то магические заклинания, которые помогут нам его уменьшить еще? Давайте смотреть дальше.

Продвинутый Pattern Matching

Мы уже попробовали несколько шаблонов в действии. Существуют ли другие варианты? Swift различает 7 типов шаблонов. Давайте посмотрим на все.

Все рассмотренные далее паттерны могут быть использованы не только в конструкции switch, но и с if, guard и даже циклом for.

1. Шаблонный паттерн

Шаблонный паттерн игнорирует значение, к которому сопоставляется. В этом случае допустимо любое значение. Вы его уже встречали, это тоже самое что и, например, let _ = fnt(), где символ “_” означает, что вам безразлично что будет возвращено из функции. Самое интересное, что будет сопоставлено не только значение, но и его отстутствие (nil). Можно даже сопоставлять Optional, достаточно лишь добавить “?”.

let p: String? = nil
switch p {
case _?: print ("Has String")
case nil: print ("No String")
}

И как мы уже успели заметить, когда реализовывали торговую платформу, этот паттерн позволяет нам опускать данные, которые в данном сопоставлении не используются:

switch (15, "example", 3.14) {
    case (_, _, let pi): print ("pi: \(pi)")
}

2. Паттер по идентификатору

Сопоставляет конкретное значение. Это стандартный механизм того, как работает switch в реализации Objective-C:

switch 5 {
  case 5: print("5")
}

3. Паттерн с присвоением значения

Аналогичен с присвоением значения переменным через let или var, но только внутри конструкции switch. Мы уже чуть выше встречались с этим видом:

switch (4, 5) {
  case let (x, y): print("\(x) \(y)")
}

4. Паттерн кортеж

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

let age = 23
let job: String? = "Operator"
let payload: AnyObject = NSDictionary()

switch (age, job, payload) {
  case (let age, _?, _ as NSDictionary):
  print(age)
  default: ()
}

В этом примере мы объединяем три значения в кортеж (например, мы их получили из нескольких разных запросов к API) и сопоставляем их. Важно, что здесь мы выполнили три задачи:

  1. Извлекли возраст (age)
  2. Убедились, что есть работа (job), даже несмотря на то, что она нам в общем не нужна
  3. Убедились что привязанные данные (payload) являются наследником класса NSDictionary, даже не смотря на то, что они нам в общем также не нужны.

5. Паттерн выбора из перечисления

Как уже было замечено выше в примере торговой платформы, pattern matching замечательно работает в паре с перечислениями Swift. Это потому, что перечисления в Swift это закрытые, иммутабельные объекты пригодные для разбора. Также как и с кортежом, вы можете извлекать нужные значения в каждом из вариантов выбора, и только те значения, которые вам необходимы.

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

enum Entities {
    case soldier(x: Int, y: Int)
    case tank(x: Int, y: Int)
    case player(x: Int, y: Int)
}

Теперь нам необходимо реализовать цикл отрисовки. Тут нам необходимы только координаты (X и Y).

for e in entities() {
    switch e {
    case let .soldier(x, y):
        drawImage("soldier.png", x, y)
    case let .tank(x, y):
        drawImage("tank.png", x, y)
    case let .player(x, y):
        drawImage("player.png", x, y)
    }
}

6. Паттерн преобразования типов

Название говорит само за себя, этот паттерн умеет преобразовывать или просто сопоставлять типы. Имеется два типа выражений:

  • is type: Сопоставляет тип (или его потомка) с правой частью выражения. Преобразование типа проводиться, но результат отбрасывается. Поэтому выражение выбора не будет знать про получившейся тип.
  • выражение as type: Производит тоже сопоставление что и is, но в случае успешного приведения возвращает полученно значение конкретного типа.

Далее пример обоих выражений

let a: Any = 5
switch a {
    // this fails because a is still anyobject
    // error: binary operator '+' cannot be applied to operands of type 'Any' and 'Int'
    case is Int: print (a + 1)

    // This works and returns '6'
    case let n as Int: print (n + 1)

    default: ()
}

Важно, что перед is нет выражения, сопоставляется переменная выбора (а) напрямую.

7. Паттерн - выражение

Паттер-выражение очень мощный. Он сопоставляет значение в конструкции switch с выражением, реализующим оператор ~=. Для этого оператора существуют реализации по-умолчанию, например, для интервалов, можно сделать так:

switch 5 {
    case 0..<10: print("In range 0-10")
    default: break
}

Интереснее, однако, ситуация, когда этот оператор перегружен для ваших объектов. Предположим, мы решили таки переписать нашу игру про солдатов на структуры.

struct Soldier {
    let hp: Int
    let x: Int
    let y: Int
}

Теперь мы хотим сопоставлять все объекты с жизнью в 0. Можно просто переопределить оператор ~= так:

func ~= (pattern: Int, value: Soldier) -> Bool {
    return pattern == value.hp
}

Теперь можно сопоставлять:

let soldier = Soldier(hp: 99, x: 10, y: 10)
switch soldier {
    case 0: print("dead soldier")
    default: ()
}

Можно сопоставлять кортежи, так:

func ~= (pattern: (hp: Int, x: Int, y: Int), value: Soldier) -> Bool {
   let (hp, x, y) = pattern
   return hp == value.hp && x == value.x && y == value.y
}

Оператор ~= может работать с протоколами:

protocol Entity {
    var value: Int {get}
}

struct Tank: Entity {
    var value: Int
    init(_ value: Int) { self.value = value }
}

struct Peasant: Entity {
    var value: Int
    init(_ value: Int) { self.value = value }
}

func ~=(pattern: Entity, x: Entity) -> Bool {
    return pattern.value == x.value
}

switch Tank(42) {
    case Peasant(42): print("Matched") // Does match
    default: ()
}

Есть еще много интересных вещей, которые можно сделать через паттерн-выражение. Для более детального описания, можете обратится к статье Austin Zheng, правда она на английском.

Мы рассмотрели все возможные варианты, однако перед тем, как мы продолжим, есть еще одна важная вещь для рассмотрения.

Fallthrough, Break, и метки

Этот раздел напрямую к pattern matching’у не относится, но затрагивает то, как работает конструкция switch, поэтому вкратце рассмотрим. По-умолчанию, и в отличии от C/C++/Objective-C, конструкция switch не обрабатывает последующии конструкции выбора (case), поэтому нет необходимости в каждом из вариантов писать break. Однако, привычное поведение можно вернуть с помощью оператора fallthrough.

switch 5 {
case 5:
    print("Is 5")
    fallthrough
default:
    print("Is a number")
}
// Will print: "Is 5" "Is a number"

Если вам необходимо досрочно выйти, то оператором break можно воспользовать. Зачем это надо, если нет обработки следующих конструкций по-умолчанию, спросите вы? Ну, например, внутри конструкции case вы можете определить, что условие не вполне удовлетворяется, и не стоит выполнять следующие операции:

let userType = "system"
let userID = 10
switch (userType, userID)  {
case ("system", _):
    guard let userData = getSystemUser(userID) else { break }
    print("user info: \(userData)")
    insertIntoRemoteDB(userData)
default: ()
}
// ... more code that needs to be executed

В примере, мы не вызываем функцию insertIntoRemoteData, если функция getSystemUser вернула nil. Конечно, здесь можно использовать конструкцию if let, но если их будет несколько, код станет выглядеть ужасно.

Но что если вы поместили ваш switch внутри, например, цикла while, и вам надо выйти из цикла, а не из switch? Для таких случаев Swift предоставляет возможность определять метки, для которых будут работать операции break и continue:

gameLoop: while true {
  switch state() {
     case .waiting: continue gameLoop
     case .done: calculateNextState()
     case .gameOver: break gameLoop
  }
}

На этом мы рассмотрели весь синтаксис и детали реализации конструкции switch и pattern matching’а. Теперь давайте рассмотрим несколько интересных примеров из реальной жизни.

Примеры из реальной жизни

Optionals

Существует огромное количество способов разворачивания Optional, и pattern matching - один из них. Наверняка вы этим часто пользуетесь, но пример все же вот:

let result: String? = secretMethod()
switch result {
case nil:
    print("is nothing")
case let a?:
    print("\(a) is a value")
}

Как можно заметить, result может быть строкой, а может быть и nil - это Optional. Можно поиграться с возвращаемым значением secretMethod() и посмотреть на результат. Кроме того, если в result есть значение, то его можно привязать к переменной (в нашем случае - a). С помощью такого кода - мы явно разделяем два случая - когда там нет значения и когда какое-то есть.

Сопоставление типов

Благодаря строгой типизации в Swift, необходимость в runtime проверке типов возникает редко, реже, нежели в Objective-C. Однако, такие случаи иногда встречаются, особенно если вы работаете с наследием Objective-C (особенно, если кодовая база не была обновлена для работы с generic’ами). В таком случае вам нужна проверка типов. Предположим, у нас есть массив NSString’ов и NSNumber’ов:

let u = NSArray(array: [NSString(string: "String1"), NSNumber(value: 20), NSNumber(value: 40)])

Когда мы будем итерироваться по этому NSArray, мы не знаем точно, объект какого типа мы получаем. В этом случае нам поможет конструкция switch:

for x in u {
    switch x {
    case _ as NSString:
        print("string")
    case _ as NSNumber:
        print("number")
    default:
        print("Unknown types")
    }
}

Сопоставляем баллы с оценками

Предположим вы подрядились написать iOS приложение для учителей в американских школах. Там учитель желает быстро уметь конвертировать некоторое количество баллов (от 0 до 100) в оценку (A - F). И тут pattern matching нам поможет:

let aGrade = 84

switch aGrade {
case 90...100: print("A")
case 80...90: print("B")
case 70...80: print("C")
case 60...70: print("D")
case 0...60: print("F")
default:
    print("Incorrect Grade")
}

Частота слов

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

Вот наши слова:

let wordFreqs = [("k", 5), ("a", 7), ("b", 3)]

Простое решение через map и filter:

let res = wordFreqs.filter({ (e) -> Bool in
    if e.1 > 3 {
        return true
    } else {
        return false
    }
}).map { $0.0 }
print(res)

Однако, с помощью flatmap (map которая возвращает только не nil элементы), можно улучшить и это решение. Первое и самое важное, мы можем избавиться от e.1 и иметь правильное разложение с использованием кортежей. И нам потребуется только один вызов flatmap, вместо filter и map, которые ухудшают производительность.

let res = wordFreqs.flatMap { (e) -> String? in
    switch e {
    case let (s, t) where t > 3: return s
    default: return nil
    }
}
print(res)

Обход дерева каталогов

Предположим, что мы хотим обойти дерево файлов и найти:

  • Все “psd” файлы от customer1 и customer2
  • Все “blend” файлы от customer2
  • Все “jpeg” файлы от всех клиентов
guard let enumerator = FileManager.default.enumerator(atPath: "/customers/2014")
    else { return }

for case let url as URL in enumerator {
    switch (url.pathComponents, url.pathExtension) {

    // psd files from customer1, customer2
    case (let f, "psd")
        where f.contains("customer1") ||
            f.contains("customer2"): print(url)

    // blend files from customer2
    case (let f, "blend")
        where f.contains("customer2"): print(url)

    // all jpg files
    case (_, "jpg"):
        print(url)

    default: ()
    }
}

Фибоначчи

Теперь “черная магия”, считаем числа Фибоначчи с помощью pattern matching’а:

func fibonacci(_ i: Int) -> Int {
    switch(i) {
    case let n where n <= 0: return 0
    case 0, 1: return 1
    case let n: return fibonacci(n - 1) + fibonacci(n - 2)
    }
}

print(fibonacci(8))

Не запускайте на больших числах :) - получите переполнение стэка.

Унаследованнное API и извлечение значений

Частно, когда вы получаете данные из внешнего источника (библиотеки, API и т.д.), перед обработкой хорошей практикой считается их проверить на корректность. Необходимо убедиться что все необходимые ключи присутствуют, данные имеют корректный тип или, например, массив нужной длины. Отсутствие такой проверки может вести от некорректного поведения (нет ключа) до падения приложения (индекс за границей массива). Классический вариант такой проверки - вложенные if’ы.

Предположим, что у нас есть API, которое возвращает пользователя. Однако, существуют два типа пользователей: привелигированные пользователи (такие как администратор) и обычные (John B, Bill Gates и т.д.). Учитывая нюансы развития системы, имеем некоторые особенности такого API:

  • system (привелигированные) и local (обычные) пользователи получаются с помощью одного и того же вызова API
  • Ключ department (подразделение) может отсутствовать, так как первые версии базы данных не содержали такого ключа и поэтому для ранних пользователей он необязателен к заполнению.
  • Массив name (имя) может содержать либо 4 значения (логин, фамилия, имя, отчество) или 2 значения (логин и полное имя). Количество значений зависит от того, как давно был создан пользователь.
  • Поле age (возраст) - целое число указывающее количество полных лет

От нашей системы требуется создавать пользовательские аккаунты для всех привелигированных пользователей со следующей информацией: логин (username), подразделение (department). Нам необходимы только пользователи с датой рождения после 1980 года. Если подразделение не задано, то по-умолчанию считаем “Corp”.

func legacyAPI(id: Int) -> [String: Any] {
    return [
        "type": "system",
        "department": "Dark Arts",
        "age": 57,
        "name": ["voldemort", "Tom", "Marvolo", "Riddle"]
    ]
}

Учитывая вышеназванные ограничения, давайте воспользуемся pattern matching’ом:

let item = legacyAPI(id: 4)
switch (item["type"], item["department"], item["age"], item["name"]) {
case let (sys as String, dep as String, age as Int, name as [String])
    where age < 1980 && sys == "system":
    createSystemUser(name.count == 2 ? name.last! : name.first!, dep: dep.characters.count > 0 ? dep : "Corp")
default:()
}
// returns ("voldemort", "Dark Arts")

Обратите внимание, что приведенный выше код делает одно опасное предположение, которое заключается в том, что если массив name имеет не 2 значения, то он обязан иметь 4. Если это предположение не выполнится, мы получим крэш.

Во всем остальном - это удачные пример того, как pattern matching может помочь вам писать элегантный код и упростить извлечение значений.

И обратите внимание на то, что мы записали let только перед кортежом, дабы не повторять его для каждой из переменных.

Паттерны с другими конструкциями

Как указывает документация на swift, все обозначенные выше паттерны могут быть использованы также с конструкциями if, for и guard.

Небольшой пример, связывание значений (value binding), кортеж и приведение типов в одном примере для всех трех конструкций:

// This is just a collection of keywords that compiles. This code makes no sense
func valueTupleType(_ a: (Int, Any)) -> Bool {
    // guard case Example
    guard case let (x, _ as String) = a else { return false}
    print(x)

    // for case example
    for case let (a, _ as String) in [a] {
        print(a)
    }

    // if case example
    if case let (x, _ as String) = a {
        print("if", x)
    }

    // switch case example
    switch a {
    case let (a, _ as String):
        print(a)
        return true
    default: return false
    }
}
let u: Any = "a"
let b: Any = 5
print(valueTupleType((5, u)))
print(valueTupleType((5, b)))
// 5, 5, "if 5", 5, true, false

Теперь давайте немножко рассмотрим каждую из конструкций более подробно.

Используем for case

Pattern matching в swift стал настолько важен, что возможности конструкции switch распространили и на другие. Для примера, давайте напишем функцию обработки массива, которая возвращает все существующие (non-nil) элементы:

func nonnil<T>(_ array: [T?]) -> [T] {
    var result: [T] = []
    for case let x? in array {
        result.append(x)
    }
    return result
}

print(nonnil(["a", nil, "b", "c", nil]))

Ключевое слово case может быть использовано в циклах for аналогично конструкции switch. Другой пример, помните игру, которую мы рассматривали ранее? Хорошо, после первого рефакторинга она выглядела так:

enum Entity {
    enum EntityType {
        case soldier
        case player
    }
    case Entry(type: EntityType, x: Int, y: Int, hp: Int)
}

Теперь, мы можем нарисовать все элементы с меньшим количеством кода:

for case let Entity.Entry(t, x, y, _) in gameEntities()
    where x > 0 && y > 0 {
        drawEntity(t, x, y)
}

Одной строчкой извлекаем все необходимые свойства, убеждаемся что не рисуем ниже и левее 0 и в конце-концов таки вызываем функцию рисования.

Для того, чтобы убедиться что игрок выиграл игру, нам необходимо знать, остался ли в живых хотя бы один солдат (health > 0).

func gameOver() -> Bool {
    for case Entity.Entry(.soldier, _, _, let hp) in gameEntities()
        where hp > 0 {return false}
    return true
}
print(gameOver())

Хорошо то, что сопоставление с .soldier является частью запроса. Это больше напоминает SQL нежели императивный цикл. Кроме того, такая запись делает нашу задумку более явной для компилятора и открывает возможности для оптимизаций. И нам нет необходимости раскрывать полное имя типа (Entity.EntityType.soldier).

Используем guard case

Другое ключевое слово, которое поддерживает pattern matching - это новая конструкция guard. Вы наверняка уже использовали ее для привязки optional’ов к локальному scope без каскадный конструкций так:

func example(_ a: String?) {
    guard let a = a else { return }
    print(a)
}
example("yes")

guard let case позволяет использовать всю мощь pattern matching’а. Давайте снова взглянем на наших солдатиков. Нам необходимо посчитать требуемую HP до того, как игрок будет полностью здоров. Солдаты не могут восстанавливать HP, поэтому нам необходимо для них всегда возвращать 0.

let MAX_HP = 100

func healthHP(_ entity: Entity) -> Int {
    guard case let Entity.Entry(.player, _, _, hp) = entity, hp < MAX_HP
        else { return 0 }
    return MAX_HP - hp
}

print("Soldier", healthHP(Entity.Entry(type: .soldier, x: 10, y: 10, hp: 79)))
print("Player", healthHP(Entity.Entry(type: .player, x: 10, y: 10, hp: 57)))

// Prints:
"Soldier 0"
"Player 43"

Замечательный пример рассмотренных нами возможностей.

  • Он просто и понятен, без каскадных конструкций
  • Логика и инициализация состояния обрабатывается в начале функции, что повышает читаемость
  • Лаконичный

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

Используем if case

if case можно испоьзовать в противовес guard case. Это замечательный способ извлечения и сопоставления данных в конкретной ветви условия. Продолжаем с нашими солдатиками - нам необходима функция перемещения. Так как наши сущности - это перечисления, нам необходимо вернуть измененную сущность.

func move(_ entity: Entity, xd: Int, yd: Int) -> Entity {
    if case Entity.Entry(let t, let x, let y, let hp) = entity,
        (x + xd) < 1000 && (y + yd) < 1000 {
        return Entity.Entry(type: t, x: (x + xd), y: (y + yd), hp: hp)
    }
    return entity
}
print(move(Entity.Entry(type: .soldier, x: 10, y: 10, hp: 79), xd: 30, yd: 500))
// prints: Entry(main.Entity.EntityType.soldier, 40, 510, 79)

Неожиданные проблемы при написании iOS приложений на Swift

 
 

Как оказалось, не все так просто в ObjC-Swift Interoperability. Иногда возникают проблемы, которые очень сложно диагностировать, и поэтому сложно починить, но которые при этом оказываются достаточно простыми. В этой статье такие проблемы опишу, список пополняется и вы можете в этом поучаствовать.

Swift и CABasicAnimation

Если коротко - CABasicAnimation не работает на Swift’овых классах, например, если хочется сделать примерно такое

class SomeLayer: CALayer {
    var rotate: CGFloat

    func setupRotation() {
        let anim = CABasicAnimation(keyPath: #keyPath(rotate))
        anim.fromValue = NS
        anim.fromValue = NSNumber(value: 0.0)
        anim.toValue = NSNumber(value: 2.0 * M_PI)
        anim.duration = 0.3
        anim.fillMode = kCAFillModeBoth

        removeAnimation(forKey: #keyPath(rotate))
        add(anim, forKey: #keyPath(rotate))
    }
}

работать не будет, даже если вы не забудете про

+ (BOOL)needsDisplayForKey:(NSString*)key

Потому что Swift dynamic свойства это не то же самое что ObjC @dynamic.

Единственный выход, который я нашел на данный момент, - в таких случаях писать такие классы на ObjC.

Установка rmagick на Mac OS X (macOS)

   

Если у вас по каким-то причинам не устанавливается rmagick, например:

current directory: /home/kb10uy/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rmagick-42aa8ce34f61/ext/RMagick
/home/kb10uy/.rbenv/versions/2.3.1/bin/ruby -r ./siteconf20160812-28804-12a4s7p.rb extconf.rb
checking for gcc... yes
checking for Magick-config... no
checking for pkg-config... yes
checking for outdated ImageMagick version (<= 6.4.9)... no
checking for presence of MagickWand API (ImageMagick version >= 6.9.0)... no
checking for Ruby version >= 1.8.5... yes
checking for stdint.h... yes
checking for sys/types.h... yes
checking for wand/MagickWand.h... no

Can't install RMagick 2.15.4. Can't find MagickWand.h.
*** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers.  Check the mkmf.log file for more details.  You may
need configuration options.

Provided configuration options:
        --with-opt-dir
        --without-opt-dir
        --with-opt-include
        --without-opt-include=${opt-dir}/include
        --with-opt-lib
        --without-opt-lib=${opt-dir}/lib
        --with-make-prog
        --without-make-prog
        --srcdir=.
        --curdir
        --ruby=/home/kb10uy/.rbenv/versions/2.3.1/bin/$(RUBY_BASE_NAME)

To see why this extension failed to compile, please check the mkmf.log which can be found here:

  /home/kb10uy/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/extensions/armv6l-linux/2.3.0-static/rmagick-42aa8ce34f61/mkmf.log

extconf failed, exit code 1

Или например так:

jjdevenuta(opal)$ gem install rmagick
Fetching: rmagick-2.13.1.gem (100%)
Building native extensions.  This could take a while...
ERROR:  Error installing rmagick:
ERROR: Failed to build gem native extension.

/Users/jjdevenuta/.rvm/rubies/ruby-1.9.2-head/bin/ruby extconf.rb
checking for Ruby version >= 1.8.5... yes
checking for gcc... yes
checking for Magick-config... no
Can't install RMagick 2.13.1. Can't find Magick-config in /Users/jjdevenuta/.rvm/gems/ruby-1.9.2-head@rails3/bin:/Users/jjdevenuta/.rvm/gems/ruby-1.9.2-head@global/bin:/Users/jjdevenuta/.rvm/rubies/ruby-1.9.2-head/bin:/Users/jjdevenuta/.rvm/bin:/usr/local/bin:/usr/local/sbin:/usr/local/mysql/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/git/bin:/usr/X11/bin

*** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of
necessary libraries and/or headers.  Check the mkmf.log file for more
details.  You may need configuration options.

Provided configuration options:
    --with-opt-dir
    --without-opt-dir
    --with-opt-include
    --without-opt-include=${opt-dir}/include
    --with-opt-lib
    --without-opt-lib=${opt-dir}/lib
    --with-make-prog
    --without-make-prog
    --srcdir=.
    --curdir
    --ruby=/Users/jjdevenuta/.rvm/rubies/ruby-1.9.2-head/bin/ruby

То это значит что у вас в системе стоит imagemagick 7 который не поддерживается rmagick (там переименовали и переместили важные заголовочные файлы). Чтобы поставить rmagick вам надо imagemagick 6, сделать это можно так

brew update
brew rm imagemagick
brew install imagemagick@6
brew link imagemagick@6 --force
bundle

Взято отсюдова

Детектируем недоступные API на данном minimal deployment target

     

Как правило iOS приложения (да и в общем macOS тоже) пишутся с использованием самых новых инструментов разработки (читай последних SDK), но при этом поддерживаются предыдущие версии iOS (macOS). Бывают ситуации, когда по недосмотру используются API из новых версий, которые недоступны на данном minimal deployment target. Если такую ошибку пропустить в прод (зарелизить приложение), то вызов такого метода на версии iOS которая его не поддерживает неминуемо приведет к крэшу. Дабы облегчить себе жизнь можно сделать вот что:

  1. Идем в Build Settings нужного проекта в xcode
  2. Находим раздел “Apple LLVM 8.0 - Custom Compiler Flags”
  3. Находим там пункт “Other Warning Flags”
  4. В него для требуемой настройки сборки (например Debug) добавляем такой флаг “-Wpartial-availability”

После этого Xcode начинает отображать такие места как #warning

Для того, чтобы Xcode не писал #warning’и для тех мест где вы делаете это намерянно, можно переопределить (где-нибудь в pre-compile заголовках) такие макро:

#define START_IGNORE_PARTIAL _Pragma("clang diagnostic push") _Pragma("clang diagnostic ignored \"-Wpartial-availability\"")
#define END_IGNORE_PARTIAL _Pragma("clang diagnostic pop")

Поваренная книга ARC

 
 

  • Про ARC можно почитать тут: Transitioning to ARC Release Notes
  • Майк Эш (Mike Ash) написал хорошую статью в его Friday Q&As
  • Детальная техническая документация доступна на сайте CLANG’а проекта LLVM.

Описанные в этой статье рецепты подразумевают использование iOS 5 и выше. В iOS 4 не доступны weak ссылки, которые являются очень важной вещью. Я понимаю, что в конце 2016 года уже даже iOS 7 официально не поддерживается, но вдруг найдутся такие читатели, которые вынуждены поддерживать очень старый код.

В статье в примерах используется Objective-C. Примеры на Swift’е не привожу - так как там это во-первых все несколько проще, во-вторых, кажется не составит труда отобразить эти правила на Swift, если это необходимо.

Общее

  • скалярные ivar свойства должны использовать assign
@property (nonatomic, assign) int scalarInt;
@property (nonatomic, assign) CGFloat scalarFloat;
@property (nonatomic, assign) CGPoint scalarStruct;
  • ссылочные ivar свойства, на которые необходимо иметь сильную ссылку или это дочерние свойства (“вниз” по иерархии) должны использовать strong
@property (nonatomic, strong) id childObject;
  • ссылочные ivar свойства на родителя (“вверх” по иерархии) должны использовать weak. Кроме того, при ссылках “вне” иерархии (например, делегаты) лучше использовать weak как наиболее безопасный.
@property (nonatomic, weak) id parentObject;
@property (nonatomic, weak) NSObject <SomeDelegate> *delegate;
  • в блоках лучше использовать copy
@property (nonatomic, copy) SomeBlockType someBlock;
  • В dealloc
    • удаляем обсерверы (observers)
    • отписываемся от уведомлений
    • для всех не weak свойств устанавливаем nil
    • инвалидируем все таймеры
  • IBOutlet’ы должны быть weak за исключением корневых, которые должны быть strong.

Bridging

Из документации

id myId;
CFStringRef myCFRef;
NSString   *a = (__bridge NSString  *)myCFRef;          // тривиальное преобразование (noop)
CFStringRef b = (__bridge CFStringRef)myId;             // тривиальное преобразование (noop)
NSString   *c = (__bridge_transfer NSString  *)myCFRef; // -1 на CFRef
CFStringRef d = (__bridge_retained CFStringRef)myId;    // возвращает CFRef +1

Если переводить с непонятного на русский, то:

  • __bridge - тривиальное преобразование (по отношению к управлению памятью) (noop)
  • __bridge_transfer - необходимо для преобразования CF ссылок в объекты Objective-C. ARC уменьшит счетчик ссылок объекта CF, поэтому убедитесь, что CFRef имеет +1.
  • __bridge_retained - необходимо для преобразования объектов Objective-C в CF ссылки. Эта операция даст вым CF ссылку с +1. Далее вы будете ответственны за вызов CFRelease у полученной CFRef ссылки где-нибудь в будущем.

NSError

Вездесущий NSError с точки зрения ARC непростой. Типовое соглашение Cocoa гласит, что ошибки обычно передаются как out-параметры (косвенные указатели).

В ARC out-параметры по-умолчанию __autoreleasing и должны реализовываться так:

- (BOOL)performWithError:(__autoreleasing NSError **)error
{
    // ... случилась ошибка ...
    if (error)
    {
        // записываем ее в out-параметр. ARC автоматически autorelease'ит
        *error = [[NSError alloc] initWithDomain:@""
                                            code:-1
                                        userInfo:nil];
        return NO;
    }
    else
    {
        return YES;
    }
}

При использовании out-параметров, вы как правило будет использовать __autoreleasing на ваших *error объектах примерно таким образом:

NSError __autoreleasing *error = error;
BOOL OK = [myObject performOperationWithError:&error];
if (!OK)
{
    // обрабатываем ошибку.
}

Если __autoreleasing не указать, компилятор автоматически добавит для вас временный autoreleasing объект. Такое решение необходимо как компромисс для обеспечения обратной совместимости. В далекие времена iOS 5 существовали такие настройки компилятора, которые не добавляли автоматически __autoreleasing.

@autoreleasepool

Используйте @autoreleasepool внутри циклов, когда:

  • в нем очень много итераций
  • в одной итерации создается большое количество временных объектов
// если someArray большой
for (id obj in someArray)
{
    @autoreleasepool
    {
        // или вы создаете много
        // временных объектов тут
    }
}

Создание и уничтожение autorelease пулов через @autoreleasepool дешевле чем даром. Не волнуйтесь использовать их внутри цикла. Если же этих заверений недостаточно, можете воспользоваться профайлером.

Blocks

В общем, блоки просто работают, однако есть несколько исключений.

Перед тем как добавлять блоки в коллекцию, необходимо предварительно их скопировать (copy).

someBlockType someBlock = ^{NSLog(@"hi");};
[someArray addObject:[someBlock copy]];

retain-циклы особенно опасны в блоках. Вы могли видеть такой warning:

warning: capturing 'self' strongly in this
block is likely to lead to a retain cycle
[-Warc-retain-cycles,4]
SomeBlockType someBlock = ^{
    [self someMethod];
};

Идея здесь такая. self имеет сильную ссылку на someBlock, someBlock “захватывает” и держит сильную ссылку на self. В следующем примере мы имеем дело с тем же retain циклом, но он уже менее очевиден:

// блок захватит self сильной ссылкой
SomeBlockType someBlock = ^{
    BOOL isDone = _isDone;  // _isDone - это ivar в self
};

Более безопасно (правда малость многословно) это можно сделать с использованием weakSelf:

__weak SomeObjectClass *weakSelf = self;

SomeBlockType someBlock = ^{
    SomeObjectClass *strongSelf = weakSelf;
    if (strongSelf == nil)
    {
        // Оригинальный self здесь не существует.
        // Игнорируйте, уведомите о или каким-то еще способ обработайте этот случай.
    }
    else
    {
        [strongSelf someMethod];
    }
};

Иногда, необходимо отдельно позаботиться о конкретных объектах, чтобы избежать retain-циклов: если someObject будет захвачен сильной ссылок в блоке, который использует someObject, вам потребуется weakSomeObject для решения проблемы.

SomeObjectClass *someObject = ...
__weak SomeObjectClass *weakSomeObject = someObject;

someObject.completionHandler = ^{
    SomeObjectClass *strongSomeObject = weakSomeObject;
    if (strongSomeObject == nil)
    {
        // Оригинальный someObject здесь не существует.
        // Игнорируйте, уведомите о или каким-то еще способ обработайте этот случай.
    }
    else
    {
        // отлично, ТЕПЕРЬ мы можем что-нибудь сделать с someObject
        [strongSomeObject someMethod];
    }
};

Использование CG из NS или UI

UIColor *redColor = [UIColor redColor];
CGColorRef redRef = redColor.CGColor;
// делаем что-нибудь с redRef.

Этот примерн имеет несколько малозаметных проблем. Когда вы создаете redRef, если redColor более нигде не используется, то он удаляется сразу после комментария.

Проблема здесь в том, что redColor “владеет” redRef, и когда redRef используется, redColor’а уже может не быть. Кроме того, такие проблемы редко проявляют себя в симуляторе. Более вероятно, что эта проблема “выстрелит” где-нибудь на устройстве, у которого осталось мало свободной памяти.

Существует несколько workaround’ов. Обычно, необходимо лишь убедиться, что redColor существует все время, пока вы используете redRef.

Самый простой способ добиться этого - использовать __autoreleasing.

UIColor * __autoreleasing redColor = [UIColor redColor];
CGColorRef redRef = redColor.CGColor;

Теперь, redColor не будет уничтожен до тех пор в будущем, пока метод не вернет управление. Поэтому мы можем спокойно использовать redRef в рамках нашего метода.

Другой путь - получить +1 на redRef:

UIColor *redColor = [UIColor redColor];
CGColorRef redRef = CFRetain(redColor.CGColor);
// используем redRef и когда закончим - удаляем его:
CFRelease(redRef)

Важно, что необходимо вызывать CFRetain() в той же строке, в которой вы получаете redColor.CGColor. redColor будет уничтожен сразу после последнего использования, следующий код работать НЕ будет:

UIColor *redColor = [UIColor redColor];
CGColorRef redRef = redColor.CGColor; // redColor будет удален сразу после этого ...
CFRetain(redRef);  // Тут будет крэш ...
...

Есть еще один интересный момент относительно строки “Тут будет крэш”. Как правило, крэш в этой строке в симуляторе не случится, но обязательно произойдет на реальном устройстве.

Singletons

Связаны с ARC только косвенно. Существует огромное количество реализаций singleton’ов “на коленке” (некоторые из них даже переопределяют retain и release).

Вот правильная реализация singleton’а которую стоит использовать:

+ (MyClass *)singleton
{
    static MyClass *sharedMyClass = nil;
    static dispatch_once_t once = 0;
    dispatch_once(&once, ^{
        sharedMyClass = [[self alloc] init];
    });
    return sharedMyClass;
}

Теперь нам необходимо иметь возможность уничтожить такой singleton. Кроме того, если это UnitTest’ы, то лучше не использовать singleton’ы.

// определяем статическую переменную вне метода singleton'а
static MyClass *__sharedMyClass = nil;

+ (MyClass *)singleton
{
    static dispatch_once_t once = 0;
    dispatch_once(&once, ^{
        __sharedMyClass = [[self alloc] init];
    });
    return __sharedMyClass;
}

// Только для использования библиотекой тестирования!!!
- (void)destroyAndRecreateSingleton
{
    __sharedMyClass = [[self alloc] init];
}