82 votes

Quel est l'intérêt de la classe Option[T] ?

Je ne suis pas capable de comprendre le point de Option[T] en Scala. Je veux dire, je ne vois pas les avantages de None sur null .

Par exemple, considérez le code :

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 */
    }
  }
}

Supposons maintenant que la méthode getPerson1 renvoie à null puis l'appel fait à display sur la première ligne de main est voué à l'échec avec NPE . De même, si getPerson2 renvoie à None le display échouera à nouveau avec une erreur similaire.

Si c'est le cas, pourquoi Scala complique-t-il les choses en introduisant un nouveau wrapper de valeurs ( Option[T] ) au lieu de suivre une approche simple utilisée en Java ?

UPDATE :

J'ai modifié mon code comme suit @Mitch La suggestion de la Commission. Je n'arrive toujours pas à voir un avantage particulier de Option[T] . Je dois tester pour l'exceptionnel null ou None dans les deux cas. :(

Si j'ai bien compris de Réponse de @Michael est le seul avantage de Option[T] est qu'elle indique explicitement au programmeur que cette méthode pourrait renvoyer None ? Est-ce la seule raison de ce choix de conception ?

72voto

Daniel C. Sobral Points 159554

Vous obtiendrez le point de Option mieux si vous vous forcez à ne jamais, jamais, utiliser get . C'est parce que get est l'équivalent de "ok, renvoyez-moi à null-land".

Alors, prenez votre exemple. Comment appelleriez-vous display sans utiliser get ? Voici quelques alternatives :

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

Aucune de ces alternatives ne vous permettra d'appeler display sur quelque chose qui n'existe pas.

Quant à savoir pourquoi get existe, Scala ne vous dit pas comment votre code doit être écrit. Il peut vous stimuler gentiment, mais si vous voulez vous rabattre sur aucun filet de sécurité, c'est votre choix.


Vous l'avez cloué ici :

le seul avantage de l'option [T] est qu'elle indique explicitement à la programmeur que cette méthode pourrait retourner None ?

Sauf pour le "seulement". Mais laissez-moi reformuler cela d'une autre manière : le principal l'avantage de Option[T] sur T est la sécurité du type. Elle garantit que vous n'enverrez pas un T à un objet qui peut ne pas exister, car le compilateur ne vous laissera pas faire.

Vous avez dit que vous devez tester la nullité dans les deux cas, mais si vous oubliez -- ou ne savez pas -- que vous devez vérifier la nullité, le compilateur vous le dira-t-il ? Ou vos utilisateurs ?

Bien entendu, en raison de son interopérabilité avec Java, Scala autorise les valeurs nulles tout comme Java. Donc si vous utilisez des bibliothèques Java, si vous utilisez des bibliothèques Scala mal écrites, ou si vous utilisez des bibliothèques Scala mal écrites personnel Scala, vous devrez toujours faire face aux pointeurs nuls.

Les deux autres avantages importants de Option Je pense à ceux-là :

  • Documentation : la signature d'une méthode vous indique si un objet est toujours retourné ou non.

  • Composabilité monadique.

Ce dernier prend beaucoup plus de temps à apprécier pleinement, et il n'est pas bien adapté aux exemples simples, car il ne montre sa force que sur du code complexe. Je vais donc donner un exemple ci-dessous, mais je suis bien conscient qu'il ne signifiera presque rien, sauf pour les personnes qui l'ont déjà compris.

for {
  person <- getUsers
  email <- person.getEmail // Assuming getEmail returns Option[String]
} yield (person, email)

31voto

Synesso Points 8865

Comparez :

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

avec :

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

La propriété monadique lier qui apparaît en Scala comme le carte nous permet d'enchaîner des opérations sur des objets sans nous soucier de savoir s'ils sont "nuls" ou non.

Poussons un peu plus loin cet exemple simple. Supposons que nous voulions trouver toutes les couleurs préférées d'une liste de personnes.

// 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

Ou peut-être voudrions-nous trouver le nom de la sœur du père de la mère d'une personne :

// 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)

J'espère que cela vous aidera à comprendre comment les options peuvent vous faciliter la vie.

22voto

Michael Neale Points 7704

La différence est subtile. Gardez à l'esprit que pour être vraiment une fonction, il faut doit renvoie une valeur - null n'est pas vraiment considéré comme une "valeur de retour normale" dans ce sens, plutôt comme une type de fond /Rien.

Mais, dans un sens pratique, lorsque vous appelez une fonction qui renvoie facultativement quelque chose, vous le feriez :

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

Il est vrai que vous pouvez faire quelque chose de similaire avec null - mais cela rend la sémantique de l'appel à getPerson2 évident en vertu du fait qu'il renvoie Option[Person] (une bonne chose pratique, autre que de compter sur quelqu'un qui lit la doc et qui obtient un NPE parce qu'il ne lit pas la doc).

Je vais essayer de trouver un programmeur fonctionnel qui pourra donner une réponse plus stricte que la mienne.

15voto

paradigmatic Points 20871

Pour moi, les options sont vraiment intéressantes lorsqu'elles sont traitées avec la syntaxe de compréhension. En prenant synesso exemple précédent :

// 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

Si l'une des assignations est None le fathersMothersSister sera None mais non NullPointerException sera relevé. Vous pouvez alors passer en toute sécurité fathersMothersSister à une fonction prenant des paramètres d'option sans s'inquiéter. Ainsi, vous ne vérifiez pas les nullités et vous ne vous souciez pas des exceptions. Comparez cela à la version java présentée dans synesso exemple.

9voto

Viktor Klang Points 14826

Vous avez des capacités de composition assez puissantes avec Option :

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") )

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X