Il n'y a pas trop longtemps, j'ai commencé à utiliser Scala au lieu de Java. Une partie du processus "Conversion" entre les langues pour moi a appris à utiliser Either
s au lieu de (vérifié) Exception
s. J'ai codé de cette façon pour un moment, mais récemment, j'ai commencé à me demander si c'est vraiment une meilleure façon de partir.
Un avantage majeur Either
a sur Exception
est une meilleure performance; Un Exception
doit construire une trace de pile et est lancée. Autant que je sache, cependant, jetant le Exception
n'est pas la partie exigeante, mais la construction de la trace de pile est.
Mais alors, on peut toujours construire/hériter Exception
s avec scala.util.control.NoStackTrace
, et encore plus encore, je vois beaucoup de cas où le côté gauche d'un Either
est en fait un Exception
(fortifier la performance boost).
Un autre avantage Either
a le compilateur-sécurité; le Scala === Compiler ne se plaingait pas de non manipulé Exception
s (contrairement au compilateur de Java). Mais si je ne me trompe pas, cette décision est motivée par le même raisonnement que est discuté dans ce sujet, alors ...
En termes de syntaxe, je me sens comme Exception
- style est bien plus clair. Examinez les blocs de code suivants (atteignant la même fonctionnalité):
Either
style:
def compute(): Either[String, Int] = {
val aEither: Either[String, String] = if (someCondition) Right("good") else Left("bad")
val bEithers: Iterable[Either[String, Int]] = someSeq.map {
item => if (someCondition(item)) Right(item.toInt) else Left("bad")
}
for {
a <- aEither.right
bs <- reduce(bEithers).right
ignore <- validate(bs).right
} yield compute(a, bs)
}
def reduce[A,B](eithers: Iterable[Either[A,B]]): Either[A, Iterable[B]] = ??? // utility code
def validate(bs: Iterable[Int]): Either[String, Unit] = if (bs.sum > 22) Left("bad") else Right()
def compute(a: String, bs: Iterable[Int]): Int = ???
Exception
style:
@throws(classOf[ComputationException])
def compute(): Int = {
val a = if (someCondition) "good" else throw new ComputationException("bad")
val bs = someSeq.map {
item => if (someCondition(item)) item.toInt else throw new ComputationException("bad")
}
if (bs.sum > 22) throw new ComputationException("bad")
compute(a, bs)
}
def compute(a: String, bs: Iterable[Int]): Int = ???
Ce dernier a l'air beaucoup plus propre pour moi et le code qui manipule la défaillance (soit une correspondance de modèle sur Either
ou try-catch
) est assez clair dans les deux cas.
Donc, ma question est - pourquoi utiliser Either
sur (coché) Exception
?
Mise à jour
Après avoir lu les réponses, j'ai réalisé que j'aurais pu avoir échoué au cœur de mon dilemme. Ma préoccupation est non avec le manque de try-catch
; On peut soit "attraper" un Exception
avec Try
, ou utilisez le catch
pour envelopper l'exception avec Left
.
Mon problème principal avec Either
/Try
vient lorsque j'écris du code qui pourrait échouer à de nombreux points en cours de route; Dans ces scénarios, lors de la rencontre d'une défaillance, je dois propager cette défaillance tout au long de mon code, rendant ainsi le code de manière plus encombrante (comme indiqué dans les exemples susmentionnés).
Il y a en fait une autre façon de casser le code sans Exception
s en utilisant return
(qui est en fait un autre "tabou" dans SCALA). Le code serait toujours plus clair que l'approche Either
, et tout en étant un peu moins propre que le style Exception
, il n'y aurait aucune crainte de non capturé Exception
s.
def compute(): Either[String, Int] = {
val a = if (someCondition) "good" else return Left("bad")
val bs: Iterable[Int] = someSeq.map {
item => if (someCondition(item)) item.toInt else return Left("bad")
}
if (bs.sum > 22) return Left("bad")
val c = computeC(bs).rightOrReturn(return _)
Right(computeAll(a, bs, c))
}
def computeC(bs: Iterable[Int]): Either[String, Int] = ???
def computeAll(a: String, bs: Iterable[Int], c: Int): Int = ???
implicit class ConvertEither[L, R](either: Either[L, R]) {
def rightOrReturn(f: (Left[L, R]) => R): R = either match {
case Right(r) => r
case Left(l) => f(Left(l))
}
}
Fondamentalement le return Left
remplace throw new Exception
, et la méthode implicite sur l'un ou l'autre, rightOrReturn
, est un complément pour la propagation automatique des exceptions sur la pile.
Si vous utilisez seulement un Either
exactement comme un impératif try-catch
Block, bien sûr, il va ressembler à une syntaxe différente pour faire exactement la même chose. Eithers
sont une valeur. Ils n'ont pas les mêmes limitations. Vous pouvez:
Eithers
n'a pas besoin d'être juste à côté de la partie "Essayer". Il peut même être partagé dans une fonction commune. Cela facilite beaucoup plus facilement les préoccupations correctement.Eithers
dans les structures de données. Vous pouvez les boucler et consolider les messages d'erreur, trouver le premier qui n'a pas échoué, les évalue paresseusement, ou similaire.Eithers
? Vous pouvez écrire des fonctions d'assistance pour les rendre plus faciles à utiliser pour vos cas d'utilisation particuliers. Contrairement à try-Catch, vous n'êtes pas bloqué de manière permanente avec seulement l'interface par défaut que les concepteurs de langue sont venus.Remarque Pour la plupart des cas d'utilisation, un Try
a une interface plus simple, possède la plupart des avantages ci-dessus et répond généralement à vos besoins tout aussi. Un Option
est encore plus simple si tout ce que vous aimez est le succès ou l'échec et n'avez besoin d'aucune information d'état comme des messages d'erreur sur le cas de défaillance.
Dans mon expérience, Eithers
_ ne sont pas vraiment préférés sur Trys
si si le Left
est autre chose qu'un message Exception
, peut-être un message d'erreur de validation, et vous Voulez-vous agréger les valeurs Left
. Par exemple, vous traitez certaines entrées et vous souhaitez collecter tous les messages d'erreur, pas seulement une erreur sur la première. Ces situations ne se présentent pas très souvent dans la pratique, du moins pour moi.
Regardez au-delà du modèle Try-Catch et vous trouverez Eithers
beaucoup plus agréable à travailler avec.
Mise à jour: Cette question attire toujours l'attention et je n'avais pas vu la mise à jour de l'OP clarifier la question de la syntaxe, alors je pensais ajouter plus.
À titre d'exemple de rupture de la mental d'exception, voici comment je voudrais généralement écrire votre code d'exemple:
def compute(): Either[String, Int] = {
Right(someSeq)
.filterOrElse(someACondition, "someACondition failed")
.filterOrElse(_ forall someSeqCondition, "someSeqCondition failed")
.map(_ map {_.toInt})
.filterOrElse(_.sum <= 22, "Sum is greater than 22")
.map(compute("good",_))
}
Ici, vous pouvez voir que la sémantique de filterOrElse
_ Capturez parfaitement ce que nous faisons principalement dans cette fonction: Vérification des conditions et associer des messages d'erreur avec les échecs donnés. Comme il s'agit d'un cas d'utilisation très courant, filterOrElse
est dans la bibliothèque standard, mais si ce n'était pas, vous pourriez le créer.
C'est le point où quelqu'un propose un contre-exemple qui ne peut pas être résolu précisément de la même manière que la mienne, mais le point que j'essaie de faire est qu'il n'y a pas qu'un moyen d'utiliser Eithers
, contrairement à des exceptions. Il existe des moyens de simplifier le code pour la plupart des situations de traitement des erreurs, si vous explorez tous Les fonctions disponibles à utiliser avec Eithers
et sont ouverts à l'expérimentation.
La bonne chose à propos des exceptions est que le code d'origine a déjà classé la circonstance exceptionnelle pour vous. La différence entre un IllegalStateException
et un BadParameterException
est beaucoup plus facile de différencier les langages dactylographiés plus forts. Si vous essayez d'analyser "bon" et "mauvais", vous ne profitiez pas de l'outil extrêmement puissant à votre disposition, à savoir le Scala compiler. Vos extensions à Throwable
Peut inclure autant d'informations que nécessaire, en plus de la chaîne de message textuelle.
C'est la véritable utilité de Try
dans le Scala Environment, avec Throwable
agissant en tant que wrapper d'éléments de gauche pour faciliter la vie.