Даже если вы живете на стороне Kotlin, возможно, время от времени вы также проверяете код Swift. Если вы это сделаете, вы, вероятно, заметили, насколько похожи оба языка. Для меня Kotlin выглядит более лаконичным, но я также предвзято отношусь к работе с Kotlin ежедневно. С другой стороны, у Swift тоже есть несколько замечательных функций. Один из них - guard keyword, действительно хороший инструмент Swift-разработчиков, которого нам не хватает. Можем ли мы привезти его в Котлин?

Что такое охранник?

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

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

func printResultFor(input: String) -> Void {
  guard let result = Int(input) else {
     println("input was not an integer")
     return
  }
  // function continues with valid int
  print("result: ", 100 * result)
}

Конечно, такой простой код можно было бы написать с помощью стандартных операторов _3 _ / _ 4_!
Но сила guard становится очевидной, когда многие из этих операторов используются последовательно. Написанное традиционным способом, это может легко привести к аду вложенных if, где вы убедитесь, что все безопасно в самом внутреннем блоке, когда все if истинны.

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

func submit(usernameText: String?, passwordText: String?, ageText: String?) {
  guard let username = usernameText, !username.isEmpty else {
      print("username is not set or blank")
      return
  }
  guard let password = passwordText, !password.isEmpty else {
      print("password is not set or blank")
      return
  }
  guard let ageString = ageText else {
      print("age is not set")
      return
  }
  guard let age = Int(ageString), age > 18 else {
      print("age not valid")
      return
  }
  // all values are checked and valid here
  register(username, password, age)
}

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

Как бы мы построили это с помощью Kotlin?

Версия 1

Давайте вернемся к нашей первоначальной простой проверке и начнем с чего-то вроде:

fun printResultFor(input: String) {
    val result = input.toIntOrNull() ?: run {
       println("input was not an integer")
       return
    }
    println("result: ${100 * result}")
}

Это самый простой способ реализовать ранний возврат в Kotlin (как также показано в этом посте).

Фактически, благодаря тому, как каждое выражение в Kotlin что-то оценивает, мы могли бы реализовать это также с традиционным _9 _ / _ 10_:

val result = if (input.toIntOrNull() != null) input.toInt() else {
    println("input was not an integer")
    return
}
println("result: ${100 * result}")

Компилятор обнаруживает, что случай else завершается раньше, и после этого мы получаем допустимую переменную result.

Оба этих варианта (run + _14 _ / _ 15_) работают нормально, пока мы можем return из функции.

Есть ли другой способ?

Как еще мы могли бы получить функцию, которая показывает компиляции правильный тип, но на самом деле никогда не возвращает? Простой (но тяжелый) способ добиться этого - просто выбросить исключение:

fun <T> shouldNotHappen(function: () -> String): T {
    println(function())
    throw IllegalArgumentException(function())
}

Теперь использование может быть:

fun printResultFor(input: String) {
    val result = input.toIntOrNull() ?: shouldNotHappen {
        "input was not an integer"
    }
    println("result: ${100 * result}")
}

Может, дадим ему имя получше, а как насчет otherwise? (к сожалению, else здесь не работает, как в Swift).

fun printResultFor(input: String) {
    val result = input.toIntOrNull() ?: otherwise {
        "input was not an integer"
    }
    println("result: ${100 * result}")
}

Как вы, наверное, заметили, это уже не то же самое, что мы начали:
Во-первых, непонятно, вызывает ли этот блок исключение.
Во-вторых, мы не хотим, чтобы исключение генерировалось!

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

Версия 2

Мы можем добиться этого, добавив к нему еще одну функцию, которая может выполнять захват и обработку (мы отметим ее inline, чтобы избежать дополнительных затрат):

fun <T> otherwise(function: () -> Any): T {
    throw GuardedException(function)
}
inline fun <T> guarded(guardedBlock: () -> T) {
    try {
        guardedBlock()
    } catch (e: GuardedException) {
        e.guardBroken()
    }
}
class GuardedException(val guardBroken: () -> Any) : Exception()

Использование этого будет выглядеть так:

fun printResultFor(input: String) {
    guarded {
       val result = input.toIntOrNull() ?: otherwise {
           println("input was not an integer")
       }
       println("result: ${100 * result}")
    }
}

Выглядит красиво! Мы почти у цели!

Думаю, было бы неплохо пометить otherwise так, чтобы было ясно, что он никогда не вернется. В Kotlin для этого есть тип Nothing!

fun otherwise(function: () -> Any): Nothing {
    throw GuardedException(function)
}

И вуаля! Мы сделали!

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

Другой подход

Однако я хотел бы изучить другой подход: есть ли способ пометить otherwise как необязательный без потери безопасности?

Было бы неплохо написать что-то вроде:

guarded {
   val result1 = guard{ input1.toIntOrNull() }
   val result2 = guard{ input2.toIntOrNull() }
   println("result: ${100 * result1 * result2}")
}

Здесь у нас есть два защищенных блока, и нам нужен только оператор println, когда оба они успешны.

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

В отличие от оператора if, в операторе guard всегда есть предложение else - код внутри предложения else выполняется, если условие не истинно.

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

guarded {
   val result1 = guard{ input1.toIntOrNull() }
   val result2 = guard(guardedBlock = { input2.toIntOrNull() }) { 
                   println("input was not an integer") }
   println("result: ${100 * result1 * result2}")
}

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

fun <V> guard(
  guardedBlock: () -> V?, 
  alternativeBlock: () -> Any = {}): V 
    = guardedBlock() ?: throw GuardedException(alternativeBlock)

Разве это не было сложно, правда?

Еще одна модификация

У наших примеров есть один недостаток: они полагаются на допустимость значений NULL, в то время как Swifts guard обрабатывает логические выражения. Процитируем документацию:

Оператор guard, как и оператор if, выполняет операторы в зависимости от логического значения выражения.

Достичь этого немного сложнее, но мы можем продолжить то, что построили выше.

Поскольку теперь нам нужно вернуть Boolean из нашей функции, сохранив исходное значение, мы сделаем guard функцией расширения самого экземпляра.
Итак, вместо

guarded {
  val result1 = guard{ input.toIntOrNull() }
  // ...
}

Ну пиши:

guarded {
  val result1 = input.guard({ isNotBlank() }).toInt()
  // ...
}

Обратите внимание, как мы можем безопасно позвонить toInt из-за охраны.

Наши охранные функции изменены на:

fun <V> V?.guard(
    guardedBlock: V.() -> Boolean?, 
    alternativeBlock: () -> Any = {}) =
    if (this != null && guardedBlock(this) == true) 
       this else throw GuardedException(alternativeBlock)

Выглядит сложнее, чем есть на самом деле. Мы просто сделали его функцией расширения нашего значения. И в зависимости от Boolean результата guardedBlock он возвращает само значение или выдает исключение, как и раньше.

Таким образом мы можем написать:

guarded {

    val username = usernameText.guard(String::isNotEmpty) {
        println("username is blank")
    }
    val password = passwordText.guard(String::isNotEmpty) {
        println("password is blank")
    }
    val ageString = ageText.guard(String::isNotEmpty) {
        println("age is empty")
    }
    // ...
}

Если мы хотим, мы можем использовать объявление infix, чтобы сделать это немного лучше, а также вернуть otherwise

infix fun <V> V?.guard(block: Pair<V.() -> Boolean?, () -> Any>) = // as before
infix fun <V> (V.() -> Boolean?).otherwise(that: () -> Any)
                                    = this to that

что приводит к:

guarded {

    val username = usernameText guard(String::isNotEmpty otherwise {
        println("username is blank")
    })
    val password = passwordText guard(String::isNotEmpty otherwise {
        println("password is blank")
    })
    val ageString = ageText guard(String::isNotEmpty otherwise {
        println("age is empty")
    })
    // ...
}

Заключение

Как вы видели, в Kotlin можно поиграть с множеством концепций, чтобы достичь чего-то сопоставимого.

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

PS: Спасибо всем рецензентам, которых я собрал за несколько минут в Твиттере! Вы великолепны и помогли мне формализовать и улучшить эти мысли.
PPS: Не стесняйтесь комментировать еще больше вариантов!