Понимание того, как использовать параметры в распространенных сценариях
Scala становится все популярнее как альтернатива Java. Он имеет строгую систему типов, полезные языковые функции, такие как сопоставление с образцом, и поддержку функционального программирования. К сожалению, особенности, которые отличают Scala от более «традиционных» языков, также могут затруднить переход к программированию на Scala. Если вы недавно начали работать над проектом Scala, одной из первых языковых функций, которые вам, возможно, потребуется понять, являются Параметры.
Что такое параметры в Scala?
Option - это контейнер для ровно 0 или 1 экземпляров любого указанного вами типа (например, Boolean, Int, List и т. Д.). Один из способов представить себе вариант - это как ящик: вы знаете, что в нем находится, и когда вы открываете ящик, в нем либо что-то есть, либо ничего.
Как вы «открываете» опцию, чтобы увидеть, есть ли в ней что-нибудь? Если он содержит элемент, Option будет экземпляром Some; если он «пустой», Option будет экземпляром None.
Мы можем увидеть это в действии, вызвав функцию List find
, которая возвращает Option:
// Scala val numbers: List[Int] = 1 :: 3 :: 5 :: 7 :: Nil val result: Option[Int] = numbers.find(_ > 4) result match { case Some(numberFound) => print(s"$numberFound is greater than 4") case None => print("There aren't any numbers > 4) } // Prints "5 is greater than 4"
В этом примере функция find
возвращает параметр, содержащий тип Int. Это говорит нам о том, что result
- это Option, который либо содержит один Int, либо пуст. Поскольку список numbers
содержит элементы больше 4, result
будет экземпляром Some, содержащим значение 5.
Чем полезны опции?
На первый взгляд Options может показаться не такой уж большой проблемой. Однако они становятся действительно мощными, когда заменяют функции, которые могут возвращать null. Давайте посмотрим на простой пример:
// Java public String someFunction(String argument) { // do some work return someValue; }
Эта функция, по-видимому, принимает значение String и возвращает значение String, но так ли это на самом деле? Как мы узнаем, может ли функция обрабатывать нулевые значения? Гарантируется ли, что возвращаемое значение будет ненулевым, или нам нужно проверить, не является ли оно ненулевым после вызова этой функции? Незнание, когда проверять нулевые значения, является рецептом для множества исключений NullPointerExceptions. Единственный способ убедиться в этом - прочитать документацию или код внутри функции.
Однако в Scala:
// Scala def someFunction(arg: String): Option[String] = { // do some work someOption }
В отличие от примера Java, это объявление функции более явно указывает, что она принимает и возвращает. Он определяет, что ему требуется (ненулевой) String, и потребитель функции вынужден проверять, имеет ли возвращаемый Option значение.
Давайте посмотрим на это на другом примере
Рассмотрим Java HashMap:
// Java HashMap simpleParse = new HashMap<Integer, String>(); simpleParse.put(1, "one"); simpleParse.put(2, "two"); simpleParse.put(3, "three"); String word1 = (String)simpleParse.get(1); String word4 = (String)simpleParse.get(4);
Оба вызова simpleParse.get
выполняются и возвращают строки, но word4
действительно имеет значение null. Если функция должна вернуть вызов simpleParse.get
, любой, кто вызывает эту функцию, может не знать, что нужно проверять нулевые значения.
Параметры явно определяют, когда функция может или не может возвращать значение. Давайте посмотрим на тот же сценарий в Scala с параметрами:
// Scala val simpleParse = Map(1 -> "one", 2 -> "two", 3 -> "three") val word1: Option[String] = simpleParse.get(1) val word4: Option[String] = simpleParse.get(4)
Сделано таким образом, и word1
, и word4
являются параметрами типа string, но word1
будет экземпляром Some, а word4
будет экземпляром None. Любые функции, возвращающие simpleParse.get
, требуют, чтобы вызывающий проверил, имеет ли параметр значение. Больше никаких исключений NullPointerExceptions.
Слово предостережения
В Scala's Option есть метод, который я рекомендую никогда не использовать: get()
Этот метод часто используется в некоторых онлайн-руководствах и отличается от метода Map.get()
, использованного в приведенных выше примерах. Функция Option.get
возвращает значение непустого параметра Option; если Option пуст, он выдает java.util.NoSuchElementException
.
Итак, если мы используем параметры word1
и word4
, указанные выше:
// Scala val result1 = word1.get // result1 == "one" val result4 = word4.get // throws NoSuchElementException
По моему опыту, это фактически сводит на нет преимущества использования опций в первую очередь, поскольку теперь вам нужно выполнять нулевые проверки или обрабатывать исключения. Есть гораздо более эффективные способы получения значений из параметров, которые мы рассмотрим ниже.
Как работать с опциями
В следующих примерах мы исследуем общие операции в Java и способы их улучшения с помощью параметров Scala.
Во-первых, примечание о методах Option:
Многие методы членов в Options аналогичны методам, которые существуют в коллекциях, таких как Lists. Например, обычно используемые методы Option включают map, flatMap, foreach, filter, exists и т. Д.
Поначалу может сбивать с толку их использование в параметрах, но в этих случаях может быть полезно думать о параметрах как о списках, которые имеют значение 0 или 1. Мы увидим несколько примеров этого в следующих случаях.
Выполнение действия, если значение не равно нулю
В большинстве приложений будет много нулевых проверок, которые выглядят примерно так:
// Java String parsed = (String)simpleParse.get(3); if (parsed != null) { // do some work involving the parsed value }
Теперь давайте посмотрим на общий способ выполнения этой функции с помощью параметров:
// Scala simpleParse.get(3).map { parsed => // do some work involving the parsed value }
Обратите внимание на использование map
в опции, возвращаемой simpleParse.get
. Думайте о Option в этом случае как о списке с 0 или 1 элементами. Если опция пуста, следующий за ней блок кода запускаться не будет. Если Option имеет значение, оно присваивается переменной parsed
, которая, как мы знаем, не равна нулю и может использоваться в блоке кода.
Назначение значения по умолчанию при нулевом значении
Допустим, мы хотим получить значение от simpleParse
, но если мы не получаем результат, мы хотим присвоить слово «неизвестно». В Java мы могли бы сделать что-то вроде этого:
// Java String parsed = (String)simpleParse.get(-2); String result = (parsed != null) ? parsed : "unknown";
Используя тернарный оператор, мы смогли попробовать получить значение с карты и присвоить значение по умолчанию в 2 строки. Поскольку функция Scala Map.get
возвращает Option, мы можем использовать вместо нее функцию getOrElse
Option:
// Scala val result: String = simpleParse.get(-2).getOrElse("unknown")
В одной строке мы можем безопасно выполнить одну и ту же задачу! Вызов getOrElse
для Option возвращает значение Option, если оно не пустое. Поскольку наш вызов simpleParse.get(-2)
возвращает пустой Option, getOrElse
вместо этого возвращает значение, которое мы предоставили, в данном случае строку «unknown».
Что, если мы хотим поработать, прежде чем присвоить значение по умолчанию?
// Java String parsed = (String)simpleParse.get(-2); String result; if (parsed != null) { result = parsed; } else { // do some task, like logging parse failure result = "unknown"; }
В этом примере мы можем выполнить некоторую задачу перед присвоением result
значения по умолчанию. Это увеличило количество кода, требуемого для партии.
Посмотрим, как это можно сделать в Scala:
// Scala val result = simpleParse.get(-2).getOrElse { // do some task, like logging parse failure "unknown" }
Версия Scala дает тот же результат с меньшим количеством кода и более четким потоком.
Манипулирование проанализированным значением перед назначением
Допустим, в вашем Java-коде вы хотите выполнить некоторые действия перед назначением result = parsed
, например, зарегистрировать успешный синтаксический анализ или добавить сообщение. На Java это может выглядеть примерно так:
// Java String parsed = (String)simpleParse.get(2); String result; if (parsed != null) { // do some task, like logging the successful parse result = "Parsed number: " + parsed; } else { // do some other task result = "unknown"; }
Чтобы добиться этого в Scala, мы можем объединить уже изученные примеры:
// Scala val result = simpleParse.get(2).map { parsed => // do some task, like logging the successful parse "Parsed number: " + parsed }.getOrElse { // do some other task "unknown" }
Давайте разберемся с этим.
Функция map
концептуально изменяет содержимое Option с вызова на simpleParse.get(2)
. Он не возвращает измененную строку, а возвращает параметр, содержащий измененную строку. В этом случае непустая опция, содержащая «два», становится непустой опцией, содержащей «проанализированное число: два», к тому времени, когда она достигает вызова getOrElse
. Таким образом, функция просто возвращает значение в Option и пропускает следующий блок кода.
Если вызов simpleParse.get
должен был дать пустой Option, вызов map
пропустил бы следующий блок кода и вместо этого остался бы пустым, что привело бы к вызову getOrElse
для выполнения его определенной настройки блока кода result
на «неизвестно».
Обработка вложенных параметров
Часто значения Java, которые могут быть нулевыми, передаются другим функциям, которые также могут создавать нулевые значения. Этот сценарий мало что изменит:
// Java String parsed = (String)simpleParse.get(-1); String result; if (parsed != null) { result = functionThatMayReturnNull(parsed); // result is either a String or null } // do something based on the value of result
Однако в Scala это может немного отличаться:
// Scala val result = simpleParse.get(-1).map { parsed => functionThatReturnsAnOption(parsed) } // result is an Option[Option[String]] // do something based on the value of result
Вызов map
возвращает Option, содержащий все, что оценивается блоком кода. Поскольку мы вызываем функцию, которая возвращает параметр типа String, мы получаем вложенный параметр. Чтобы избавиться от вложенного Option, мы могли бы сделать что-то вроде этого:
// Scala val result = simpleParse.get(-1).map { parsed => functionThatReturnsAnOption(parsed).getOrElse("") } // result is an Option[String]
Эта модификация извлекает значение параметра или устанавливает значение по умолчанию внутри блока кода map
. Хотя в некоторых случаях это может быть правильным шаблоном, обычно более правильным будет использовать flatMap
:
// Scala val result = simpleParse.get(-1).flatMap { parsed => functionThatReturnsAnOption(parsed) } // result is an Option[String]
Параметры сведения с flatMap
концептуально аналогичны использованию flatMap
в списке. Поскольку Option может иметь только 0 или 1 значение, мы можем думать о flatMap
как об удалении самого внешнего Option, оставляя нам только внутренний Option, пустой или непустой. Этот шаблон больше всего похож на приведенный выше пример Java.
Вывод
При использовании в качестве альтернативы нулевым значениям параметры позволяют более явно указать, будет ли функция возвращать значение. Они требуют от потребителя выполнения проверок, предотвращающих непредвиденные исключения NullPointerExceptions, и упрощают запоминание крайних случаев, поощряя более надежный код. Опции также предоставляют отличные инструменты для уменьшения количества необходимых строк кода при сохранении читабельности и надежности.
Несмотря на то, что они требуют небольшого обучения, обучение эффективному использованию опций значительно улучшит качество вашей кодовой базы.