Какой смысл в классе Option [T]? - PullRequest
82 голосов
/ 17 января 2010

Я не могу понять суть класса Option[T] в Scala. Я имею в виду, я не могу видеть никаких преимуществ None над null.

Например, рассмотрим код:

object Main{
  class Person(name: String, var age: int){
    def display = println(name+" "+age)
  }

  def getPerson1: Person = {
    // returns a Person instance or null
  }

  def getPerson2: Option[Person] = {
    // returns either Some[Person] or None
  }

  def main(argv: Array[String]): Unit = {
    val p = getPerson1
    if (p!=null) p.display

    getPerson2 match{
      case Some(person) => person.display
      case None => /* Do nothing */
    }
  }
}

Теперь предположим, что метод getPerson1 возвращает null, а затем вызов display в первой строке main обязательно завершится с NPE. Точно так же, если getPerson2 возвращает None, вызов display снова завершится ошибкой с некоторой похожей ошибкой.

Если это так, то почему Scala усложняет ситуацию, вводя новый упаковщик значений (Option[T]) вместо того, чтобы следовать простому подходу, используемому в Java?

UPDATE:

Я отредактировал свой код в соответствии с предложением @ Mitch . Я до сих пор не вижу каких-либо особых преимуществ Option[T]. Я должен проверить для исключительных null или None в обоих случаях. (

Если я правильно понял из @ ответа Михаила , единственное преимущество Option[T] заключается в том, что он явно сообщает программисту, что этот метод может вернуть None ? Это единственная причина такого выбора дизайна?

Ответы [ 18 ]

71 голосов
/ 17 января 2010

Вы получите Option балл лучше, если заставите себя никогда, никогда не использовать get. Это потому, что get является эквивалентом «хорошо, отправь меня обратно на нуль-землю».

Итак, возьмите этот пример. Как бы вы назвали display без использования get? Вот несколько альтернатив:

getPerson2 foreach (_.display)
for (person <- getPerson2) person.display
getPerson2 match {
  case Some(person) => person.display
  case _ =>
}
getPerson2.getOrElse(Person("Unknown", 0)).display

Ни одна из этих альтернатив не позволит вам позвонить display на то, что не существует.

Что касается того, почему существует get, Scala не говорит вам, как должен быть написан ваш код. Это может мягко подтолкнуть вас, но если вы хотите отказаться от сети безопасности, это ваш выбор.


Вы прибили это здесь:

является единственным преимуществом опции [T] что это явно говорит программист, что этот метод мог вернуть нет?

За исключением "только". Но позвольте мне повторить это по-другому: main преимущество Option[T] перед T - это безопасность типов. Это гарантирует, что вы не будете отправлять метод T для объекта, который может не существовать, так как компилятор не позволит вам.

Вы сказали, что должны проверять на обнуляемость в обоих случаях, но если вы забудете - или не знаете - вы должны проверить на нулевое значение, скажет ли вам компилятор? Или будут ваши пользователи?

Конечно, из-за своей совместимости с Java, Scala допускает нулевые значения так же, как и Java. Поэтому, если вы используете библиотеки Java, если вы используете плохо написанные библиотеки Scala, или если вы используете плохо написанные личные библиотеки Scala, вам все равно придется иметь дело с нулевыми указателями.

Другие два важных преимущества Option, о которых я могу думать:

  • Документация: подпись типа метода скажет вам, всегда ли возвращается объект или нет.

  • Монадическая компоновка.

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

for {
  person <- getUsers
  email <- person.getEmail // Assuming getEmail returns Option[String]
} yield (person, email)
31 голосов
/ 17 января 2010

Сравнить:

val p = getPerson1 // a potentially null Person
val favouriteColour = if (p == null) p.favouriteColour else null

с:

val p = getPerson2 // an Option[Person]
val favouriteColour = p.map(_.favouriteColour)

Монадическое свойство bind , которое появляется в Scala как функция map , позволяет нам связывать операции над объектами, не заботясь о том, являются ли они «нулевыми» или нет. *

Возьмем этот простой пример чуть дальше. Скажем, мы хотели найти все любимые цвета списка людей.

// list of (potentially null) Persons
for (person <- listOfPeople) yield if (person == null) null else person.favouriteColour

// list of Options[Person]
listOfPeople.map(_.map(_.favouriteColour))
listOfPeople.flatMap(_.map(_.favouriteColour)) // discards all None's

Или, возможно, мы хотели бы найти имя сестры матери отца человека:

// with potential nulls
val father = if (person == null) null else person.father
val mother = if (father == null) null else father.mother
val sister = if (mother == null) null else mother.sister

// with options
val fathersMothersSister = getPerson2.flatMap(_.father).flatMap(_.mother).flatMap(_.sister)

Я надеюсь, что это проливает некоторый свет на то, как варианты могут сделать жизнь немного проще.

21 голосов
/ 17 января 2010

Разница тонкая. Имейте в виду, чтобы быть действительно функцией, она должна возвращать значение - значение null на самом деле не считается «нормальным возвращаемым значением» в этом смысле, скорее, тип основания / ничто.

Но в практическом смысле, когда вы вызываете функцию, которая может что-то возвращать, вы должны сделать:

getPerson2 match {
   case Some(person) => //handle a person
   case None => //handle nothing 
}

Конечно, вы можете сделать что-то похожее с null - но это делает семантику вызова getPerson2 очевидной в силу того факта, что он возвращает Option[Person] (хорошая практическая вещь, за исключением того, что кто-то читает документ и получает NPE, потому что они не читают документ).

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

15 голосов
/ 17 января 2010

Для меня опции действительно интересны, когда обрабатываются для понимания синтаксиса. Принимая synesso предыдущий пример:

// with potential nulls
val father = if (person == null) null else person.father
val mother = if (father == null) null else father.mother
val sister = if (mother == null) null else mother.sister

// with options
val fathersMothersSister = for {
                                  father <- person.father
                                  mother <- father.mother
                                  sister <- mother.sister
                               } yield sister

Если какое-либо из назначений None, fathersMothersSister будет None, но NullPointerException повышаться не будет. Затем вы можете спокойно передавать fathersMothersSister в функцию, принимающую параметры Option, не беспокоясь. так что вы не проверяете на ноль и не заботитесь об исключениях. Сравните это с java-версией, представленной в примере synesso .

9 голосов
/ 18 января 2010

У вас есть довольно мощные возможности композиции с опцией:

def getURL : Option[URL]
def getDefaultURL : Option[URL]


val (host,port) = (getURL orElse getDefaultURL).map( url => (url.getHost,url.getPort) ).getOrElse( throw new IllegalStateException("No URL defined") )
8 голосов
/ 02 августа 2010

Может быть, кто-то еще указал на это, но я этого не видел:

Одно из преимуществ сопоставления с шаблоном с помощью Option [T] по сравнению с проверкой нуля состоит в том, что Option является запечатанным классом, поэтому компилятор Scala выдаст предупреждение, если вы пренебрегаете кодированием варианта Some или None. В компиляторе есть флаг компилятора, который превратит предупреждения в ошибки. Таким образом, можно предотвратить сбой обработки случая «не существует» во время компиляции, а не во время выполнения. Это огромное преимущество перед использованием нулевого значения.

7 голосов
/ 17 января 2010

Это не для того, чтобы избежать проверки на ноль, а для принудительной проверки на ноль.Суть становится ясной, когда в вашем классе 10 полей, два из которых могут быть нулевыми.И ваша система имеет 50 других подобных классов.В мире Java вы пытаетесь предотвратить использование NPE в этих областях, используя некоторую комбинацию умственных способностей, именования или даже аннотаций.И каждый Java-разработчик в значительной степени терпит неудачу.Класс Option не только делает «обнуляемые» значения визуально понятными для всех разработчиков, пытающихся понять код, но и позволяет компилятору применять этот ранее невысказанный контракт.

6 голосов
/ 26 марта 2011

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

То есть вы можете иметь Option[Option[A]], который будет населёнNone, Some(None) и Some(Some(a)), где a - один из обычных жителей A.Это означает, что если у вас есть какой-то контейнер, и вы хотите иметь возможность хранить в нем нулевые указатели и выводить их, вам нужно вернуть некоторое дополнительное логическое значение, чтобы узнать, действительно ли вы получили значение.Подобные бородавки имеются в большом количестве в API-интерфейсах java-контейнеров, и некоторые варианты без блокировок даже не могут их предоставить.

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

Например, когда вы проверяете

if (x == null) ...
else x.foo()

, вы должны носить с собой вваша голова по всей ветке else, что x != null и что это уже проверено.Однако, когда вы используете что-то вроде опции

x match {
case None => ...
case Some(y) => y.foo
}

, вы знаете, что y не является None по построению - и вы знаете, что это не null, если ононе было ошибки Хоара миллиардов долларов .

6 голосов
/ 29 июля 2010

[скопировано с этого комментария от Даниэль Спивак ]

Если бы единственный способ использовать Option был для сопоставления с образцом, чтобы получить Значит, тогда да, я согласен, что это не улучшается вообще больше нуля. Тем не менее, вы пропускаете * огромный * класс его функциональности. Единственный веская причина для использования Option является если вы используете его более высокий порядок вспомогательные функции. Эффективно вы нужно использовать его монадическую природу. Например (при условии определенной суммы API-обрезки):

val row: Option[Row] = database fetchRowById 42
val key: Option[String] = row flatMap { _ get “port_key” }
val value: Option[MyType] = key flatMap (myMap get)
val result: MyType = value getOrElse defaultValue

Там, разве это не было изящно? Мы можем на самом деле сделать намного лучше, если мы используем for -comprehensions:

val value = for {
row <- database fetchRowById 42
key <- row get "port_key"
value <- myMap get key
} yield value
val result = value getOrElse defaultValue

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

Я никогда не делал явного соответствия против Option, и я знаю много другие разработчики Scala, которые находятся в та же лодка Дэвид Поллак упомянул я только на днях, что он использует такое явное совпадение на Option (или Box, в случае с лифтом) как знак что разработчик, который написал код не до конца понимает язык и его стандартная библиотека.

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

3 голосов
/ 18 января 2010

Опция [T] - это монада, которая действительно полезна при использовании функций высокого порядка для манипулирования значениями.

Я предлагаю вам прочитать статьи, перечисленные ниже, это действительно хорошие статьи, которые показывают вам, почему Option [T] полезен и как его можно использовать функционально.

...