Лучшая практика для проверки нулей в Scala

Я понимаю, что null недоверчиво встречается в Scala, и что каждый должен всегда переносить необязательные значения в Option (или "нулевой тип", если он доступен).

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

На практике, однако, ничто не мешает значению быть null - правило не применяется компилятором и является скорее джентльменским соглашением. Это может легко нарушиться при взаимодействии с Java API, например.

Есть ли для этого наилучшая практика? Скажем, например, что мне нужно написать класс case Url, а экземпляры этого класса можно создать из java.net.URI:

object Url {
  def apply(uri: URI): Url = Url(uri.getScheme, uri.getHost, uri.getRawPath)
}

case class Url(protocol: String, host: String, path: String) {
  def protocol(value: String): Url = copy(protocol = value)
  def host(value: String): Url = copy(host = value)
  def path(value: String): Url = copy(path = value)
}

An URI экземпляр может возвращать null для getScheme, getHost и getRawPath. Является ли это достаточным для защиты от них в методе apply(URI) (например, с операторами require)? Технически, чтобы быть полностью безопасным, было бы лучше защитить вспомогательные методы protocol, host и path, а также конструктор, но это звучит как ужасно много работы и шаблона.

Является ли безопасным вместо этого защищать от API, которые, как известно, принимают/возвращают значения null (URI в нашем примере) и предполагают, что внешние вызывающие абоненты либо не пройдут значения null, либо только сами винить, если они это делают?

Ответ 1

При работе с чистыми функциями всегда лучше придерживаться полных реализаций и вместо использования null, require или броска исключений для кодирования всех сбоев данных. Такие типы, как Option и Either, существуют именно для этого. Оба они являются монадами, поэтому вы можете использовать для них "for" -syntax.

Следующая модификация вашего кода показывает полную функцию apply, которая дает только содержательное значение, когда входной Java URI не предоставляет null s. Мы кодируем понятие "значимого", завершая результат в Option.

object Url {
  def apply(uri: java.net.URI): Option[Url] =
    for {
      protocol <- Option(uri.getScheme)
      host <- Option(uri.getHost)
      path <- Option(uri.getRawPath)
    }
    yield Url(protocol, host, path)
}

case class Url(protocol: String, host: String, path: String)

Функция Option - это стандартная функция проверки нуль, которая отображает null в None и переносит значения в Some.


Что касается ваших "настроек", вы можете реализовать null -checking в них аналогичным образом, используя Option. Например:.

case class Url(protocol: String, host: String, path: String) {
  def protocol(value: String): Option[Url] = 
    Option(value).map(v => copy(protocol = v))
  // so on...
}

Однако я бы советовался с этим. В Scala традиционно никогда не использовать null s, поэтому принято также применять только null -обслуживание для "мостовых" API, когда вы получаете значения из Java libs. То, что null -checking имеет смысл при преобразовании из java.net.URI, но после этого вы находитесь в мире Scala, где не должно быть null s, и, следовательно, null -checking просто становится избыточным.