Quel est l'équivalent Go idiomatique de l'opérateur ternaire de C?

297

En C / C ++ (et dans de nombreux langages de cette famille), un idiome commun pour déclarer et initialiser une variable en fonction d'une condition utilise l'opérateur conditionnel ternaire:

int index = val > 0 ? val : -val

Go n'a pas l'opérateur conditionnel. Quelle est la façon la plus idiomatique d'implémenter le même morceau de code que ci-dessus? Je suis arrivé à la solution suivante, mais elle semble assez verbeuse

var index int

if val > 0 {
    index = val
} else {
    index = -val
}

Y a-t-il quelque chose de mieux?

Fabien
la source
vous pouvez initialiser la valeur avec la partie else et ne vérifier que si votre état change, pas sûr que ce soit mieux cependant
x29a
De nombreux if / thens auraient dû être éliminés de toute façon. Nous le faisions tout le temps depuis le jour où j'ai écrit mes premiers programmes BASIC il y a 35 ans. Votre exemple pourrait être: int index = -val + 2 * val * (val > 0);
hyc
9
@hyc votre exemple est loin d'être aussi lisible que le code idiomatique de go, ou même que la version de C utilisant l'opérateur ternaire. Quoi qu'il en soit, AFAIK, il n'est pas possible d'implémenter cette solution dans Go car un booléen ne peut pas être utilisé comme valeur numérique.
Fabien
Vous vous demandez pourquoi go n'a pas fourni un tel opérateur?
Eric Wang
@EricWang Deux raisons, AFAIK: 1- vous n'en avez pas besoin, et ils voulaient garder la langue aussi petite que possible. 2- il a tendance à être abusé, c'est-à-dire utilisé dans des expressions à plusieurs lignes alambiquées, et les concepteurs de langage ne l'aiment pas.
Fabien

Réponses:

244

Comme souligné (et sans surprise, espérons-le), l'utilisation if+elseest en effet la façon idiomatique de faire des conditions dans Go.

En plus du var+if+elsebloc de code complet , cette orthographe est également souvent utilisée:

index := val
if val <= 0 {
    index = -val
}

et si vous avez un bloc de code suffisamment répétitif, comme l'équivalent de int value = a <= b ? a : b, vous pouvez créer une fonction pour le contenir:

func min(a, b int) int {
    if a <= b {
        return a
    }
    return b
}

...

value := min(a, b)

Le compilateur insérera ces fonctions simples, il est donc rapide, plus clair et plus court.

Gustavo Niemeyer
la source
184
Hé les gars, regardez! Je viens de porter l' opérateur de ternarité sur les golangs! play.golang.org/p/ZgLwC_DHm0 . Si efficace!
jeudi
28
@tomwilde votre solution semble assez intéressante, mais il lui manque une des principales caractéristiques de l'opérateur ternaire - l'évaluation conditionnelle.
Vladimir Matveev
12
@VladimirMatveev encapsule les valeurs dans les fermetures;)
nemo
55
c := (map[bool]int{true: a, false: a - 1})[a > b]est un exemple d'obscurcissement à mon humble avis, même si cela fonctionne.
Rick-777
34
Si if/elseest l'approche idiomatiques alors peut - être golang pourrait envisager de laisser des if/elseclauses retourner une valeur: x = if a {1} else {0}. Go ne serait en aucun cas la seule langue à fonctionner de cette façon. Un exemple courant est Scala. Voir: alvinalexander.com/scala/scala-ternary-operator-syntax
Max Murphy
80

No Go n'a pas d'opérateur ternaire, en utilisant la syntaxe if / else est la manière idiomatique.

Pourquoi Go n'a-t-il pas l'opérateur?:?

Il n'y a aucune opération de test ternaire dans Go. Vous pouvez utiliser les éléments suivants pour obtenir le même résultat:

if expr {
    n = trueVal
} else {
    n = falseVal
}

La raison ?:est absente de Go est que les concepteurs du langage avaient vu l'opération utilisée trop souvent pour créer des expressions d'une complexité impénétrable. La if-elseforme, bien que plus longue, est incontestablement plus claire. Un langage n'a besoin que d'une seule construction de flux de contrôle conditionnel.

- Foire aux questions (FAQ) - Le langage de programmation Go

ishaaq
la source
1
Donc juste parce que ce que les concepteurs de langage ont vu, ils ont omis une ligne pour un if-elsebloc entier ? Et qui a dit if-elsene pas être abusé de la même manière? Je ne t'attaque pas, j'ai juste l'impression que l'excuse des designers n'est pas assez valable
Alf Moh
58

Supposons que vous ayez l'expression ternaire suivante (en C):

int a = test ? 1 : 2;

L'approche idiomatique dans Go serait d'utiliser simplement un ifbloc:

var a int

if test {
  a = 1
} else {
  a = 2
}

Cependant, cela pourrait ne pas correspondre à vos besoins. Dans mon cas, j'avais besoin d'une expression en ligne pour un modèle de génération de code.

J'ai utilisé une fonction anonyme immédiatement évaluée:

a := func() int { if test { return 1 } else { return 2 } }()

Cela garantit que les deux branches ne sont pas également évaluées.

Peter Boyer
la source
Bon à savoir qu'une seule branche de la fonction anon en ligne est évaluée. Mais notez que des cas comme celui-ci sont au-delà de la portée de l'opérateur ternaire de C.
Wolf
1
La C expression conditionnelle (communément appelé l'opérateur ternaire) a trois opérandes: expr1 ? expr2 : expr3. Si expr1évalue à true, expr2est évalué et est le résultat de l'expression. Sinon, expr3est évalué et fourni comme résultat. Ceci provient de la section 2.11 du langage de programmation C ANSI de K&R. Ma solution Go conserve ces sémantiques spécifiques. @Wolf Pouvez-vous clarifier ce que vous proposez?
Peter Boyer
Je ne suis pas sûr de ce que j'avais en tête, peut-être que les fonctions anon fournissent une portée (espace de noms local), ce qui n'est pas le cas avec l'opérateur ternaire en C / C ++. Voir un exemple d'utilisation de cette lunette
Wolf
39

La carte ternaire est facile à lire sans parenthèses:

c := map[bool]int{true: 1, false: 0} [5 > 4]
user1212212
la source
Je ne sais pas exactement pourquoi il a -2 ... oui, c'est une solution de contournement, mais cela fonctionne et est sûr pour le type.
Alessandro Santini
30
Oui, cela fonctionne, est sûr pour le type et est même créatif; cependant, il existe d'autres mesures. Les opérations ternaires sont équivalentes à if / else (voir par exemple ce post S / O ). Cette réponse n'est pas parce que 1) les deux branches sont exécutées, 2) crée une carte 3) appelle un hachage. Tous ces éléments sont "rapides", mais pas aussi rapides qu'un if / else. De plus, je dirais que ce n'est pas plus lisible que var r T si la condition {r = foo ()} else {r = bar ()}
chevalier
Dans d'autres langues, j'utilise cette approche lorsque j'ai plusieurs variables et avec des fermetures ou des pointeurs de fonction ou des sauts. L'écriture de if imbriqués devient sujette aux erreurs lorsque le nombre de variables augmente, tandis que par exemple {(0,0,0) => {code1}, (0,0,1) => {code2} ...} [(x> 1 , y> 1, z> 1)] (pseudocode) devient de plus en plus attractif à mesure que le nombre de variables augmente. Les fermetures gardent ce modèle rapide. Je m'attends à ce que des compromis similaires s'appliquent.
Max Murphy
Je suppose que vous utiliseriez un interrupteur pour ce modèle. J'adore la façon dont les commutateurs se cassent automatiquement, même si cela est parfois gênant.
Max Murphy
8
comme l'a souligné Cassy Foesch: simple and clear code is better than creative code.
Wolf
11
func Ternary(statement bool, a, b interface{}) interface{} {
    if statement {
        return a
    }
    return b
}

func Abs(n int) int {
    return Ternary(n >= 0, n, -n).(int)
}

Cela ne surclassera pas si / else et nécessite un cast mais fonctionne. Pour info:

BenchmarkAbsTernary-8 100000000 18.8 ns / op

BenchmarkAbsIfElse-8 2000000000 0,27 ns / op

Phillip Dominy
la source
C'est la meilleure solution, félicitations! Une ligne qui gère tous les cas possibles
Alexandro de Oliveira
2
Je ne pense pas que cela gère l'évaluation conditionnelle, ou non? Avec les branches libres d'effets secondaires, cela n'a pas d'importance (comme dans votre exemple), mais si c'est quelque chose avec des effets secondaires, vous rencontrerez des problèmes.
Ashton Wiersdorf
7

Si toutes vos branches produisent des effets secondaires ou sont coûteuses en calcul, ce qui suit serait une refactorisation sémantiquement préservée :

index := func() int {
    if val > 0 {
        return printPositiveAndReturn(val)
    } else {
        return slowlyReturn(-val)  // or slowlyNegate(val)
    }
}();  # exactly one branch will be evaluated

sans surcharge (en ligne) et, surtout, sans encombrer votre espace de noms avec des fonctions d'assistance qui ne sont utilisées qu'une seule fois (ce qui nuit à la lisibilité et à la maintenance). Exemple en direct

Notez si vous deviez appliquer naïvement l'approche de Gustavo :

    index := printPositiveAndReturn(val);
    if val <= 0 {
        index = slowlyReturn(-val);  // or slowlyNegate(val)
    }

vous obtiendriez un programme avec un comportement différent ; au cas où le val <= 0programme imprimerait une valeur non positive alors qu'il ne devrait pas! (De façon analogue, si vous inversiez les branches, vous introduiriez une surcharge en appelant une fonction lente inutilement.)

eold
la source
1
Lecture intéressante, mais je ne comprends pas vraiment le point de votre critique de l'approche de Gustavo. Je vois une (sorte de) absfonction dans le code original (bien, je changerais <=à <). Dans votre exemple, je vois une initialisation, qui est redondante dans certains cas et pourrait être expansive. Pouvez-vous préciser: expliquez un peu plus votre idée?
Wolf
La principale différence est que l'appel d'une fonction en dehors de l'une ou l'autre branche aura des effets secondaires même si cette branche n'aurait pas dû être prise. Dans mon cas, seuls les nombres positifs seront imprimés car la fonction printPositiveAndReturnn'est appelée que pour les nombres positifs. Inversement, toujours exécuter une branche, puis "fixer" la valeur avec l'exécution d'une branche différente n'annule pas les effets secondaires de la première branche .
eold
Je vois, mais les programmeurs expérimentés sont normalement conscients des effets secondaires. Dans ce cas, je préférerais la solution évidente de Cassy Foesch à une fonction intégrée, même si le code compilé peut être le même: il est plus court et semble évident pour la plupart des programmeurs. Ne vous méprenez pas: je vraiment aime les fermetures de Go;)
Loup
1
« les programmeurs d'expériences sont normalement conscients des effets secondaires » - Non. Éviter l'évaluation des termes est l'une des principales caractéristiques d'un opérateur ternaire.
Jonathan Hartley
6

Avant-propos: Sans prétendre que if elsec'est la voie à suivre, nous pouvons toujours jouer avec et trouver du plaisir dans les constructions basées sur le langage.

La Ifconstruction suivante est disponible dans ma github.com/icza/goxbibliothèque avec beaucoup d'autres méthodes, étant le builtinx.Iftype.


Go permet d'attacher des méthodes à tous les types définis par l'utilisateur , y compris les types primitifs tels que bool. Nous pouvons créer un type personnalisé ayant boolcomme type sous-jacent , puis avec une conversion de type simple à la condition, nous avons accès à ses méthodes. Méthodes qui reçoivent et sélectionnent les opérandes.

Quelque chose comme ça:

type If bool

func (c If) Int(a, b int) int {
    if c {
        return a
    }
    return b
}

Comment pouvons-nous l'utiliser?

i := If(condition).Int(val1, val2)  // Short variable declaration, i is of type int
     |-----------|  \
   type conversion   \---method call

Par exemple un ternaire faisant max():

i := If(a > b).Int(a, b)

Un ternaire faisant abs():

i := If(a >= 0).Int(a, -a)

Cela a l'air cool, c'est simple, élégant et efficace (il est également éligible pour l'inline ).

Un inconvénient par rapport à un "vrai" opérateur ternaire: il évalue toujours tous les opérandes.

Pour obtenir une évaluation différée et uniquement si nécessaire, la seule option consiste à utiliser des fonctions (soit des fonctions ou méthodes déclarées , soit des littéraux de fonction ), qui ne sont appelées que lorsque / si nécessaire:

func (c If) Fint(fa, fb func() int) int {
    if c {
        return fa()
    }
    return fb()
}

Utilisation: Supposons que nous avons ces fonctions pour calculer aet b:

func calca() int { return 3 }
func calcb() int { return 4 }

Ensuite:

i := If(someCondition).Fint(calca, calcb)

Par exemple, la condition étant l'année en cours> 2020:

i := If(time.Now().Year() > 2020).Fint(calca, calcb)

Si nous voulons utiliser des littéraux de fonction:

i := If(time.Now().Year() > 2020).Fint(
    func() int { return 3 },
    func() int { return 4 },
)

Remarque finale: si vous aviez des fonctions avec des signatures différentes, vous ne pourriez pas les utiliser ici. Dans ce cas, vous pouvez utiliser un littéral de fonction avec une signature correspondante pour les rendre toujours applicables.

Par exemple, si calca()et calcb()aurait également des paramètres (en plus de la valeur de retour):

func calca2(x int) int { return 3 }
func calcb2(x int) int { return 4 }

Voici comment vous pouvez les utiliser:

i := If(time.Now().Year() > 2020).Fint(
    func() int { return calca2(0) },
    func() int { return calcb2(0) },
)

Essayez ces exemples sur le Go Playground .

icza
la source
4

La réponse d'Eold est intéressante et créative, peut-être même intelligente.

Cependant, il serait recommandé de faire à la place:

var index int
if val > 0 {
    index = printPositiveAndReturn(val)
} else {
    index = slowlyReturn(-val)  // or slowlyNegate(val)
}

Oui, ils se compilent tous les deux essentiellement vers le même assembly, mais ce code est beaucoup plus lisible que d'appeler une fonction anonyme juste pour renvoyer une valeur qui aurait pu être écrite dans la variable en premier lieu.

Fondamentalement, un code simple et clair est meilleur qu'un code créatif.

De plus, tout code utilisant un littéral de carte n'est pas une bonne idée, car les cartes ne sont pas du tout légères dans Go. Depuis Go 1.3, l'ordre d'itération aléatoire pour les petites cartes est garanti, et pour l'appliquer, il est devenu un peu moins efficace en termes de mémoire pour les petites cartes.

Par conséquent, la création et la suppression de nombreuses petites cartes prennent à la fois de l'espace et du temps. J'avais un morceau de code qui utilisait une petite carte (deux ou trois clés, probablement, mais le cas d'utilisation courant n'était qu'une entrée) Mais le code était lent. Nous parlons d'au moins 3 ordres de grandeur plus lentement que le même code réécrit pour utiliser une clé à double tranche [index] => data [index] map. Et c'était probablement plus. Comme certaines opérations qui prenaient auparavant quelques minutes à exécuter, ont commencé à se terminer en millisecondes. \

Cassy Foesch
la source
1
simple and clear code is better than creative code- ça j'aime beaucoup, mais je suis un peu confus dans la dernière section après dog slow, peut-être que ça pourrait être déroutant pour les autres aussi?
Wolf
1
Donc, fondamentalement ... j'avais du code qui créait de petites cartes avec une, deux ou trois entrées, mais le code fonctionnait très lentement. Donc, beaucoup de choses m := map[string]interface{} { a: 42, b: "stuff" }, puis dans une autre fonction, itérant à travers celui-ci: for key, val := range m { code here } Après être passé à un système à deux tranches keys = []string{ "a", "b" }, data = []interface{}{ 42, "stuff" }:, puis parcourez comme des for i, key := range keys { val := data[i] ; code here }choses accélérées 1000 fois.
Cassy Foesch
Je vois, merci pour la clarification. (Peut-être que la réponse elle - même pourrait être améliorée sur ce point.)
Wolf
1
-.- ... touché, logique ... touché ... j'y reviendrai éventuellement ...;)
Cassy Foesch
3

Les doublures, bien que rejetées par les créateurs, ont leur place.

Celui-ci résout le problème de l'évaluation paresseuse en vous laissant, éventuellement, passer des fonctions à évaluer si nécessaire:

func FullTernary(e bool, a, b interface{}) interface{} {
    if e {
        if reflect.TypeOf(a).Kind() == reflect.Func {
            return a.(func() interface{})()
        }
        return a
    }
    if reflect.TypeOf(b).Kind() == reflect.Func {
        return b.(func() interface{})()
    }
    return b
}

func demo() {
    a := "hello"
    b := func() interface{} { return a + " world" }
    c := func() interface{} { return func() string { return "bye" } }
    fmt.Println(FullTernary(true, a, b).(string)) // cast shown, but not required
    fmt.Println(FullTernary(false, a, b))
    fmt.Println(FullTernary(true, b, a))
    fmt.Println(FullTernary(false, b, a))
    fmt.Println(FullTernary(true, c, nil).(func() string)())
}

Production

hello
hello world
hello world
hello
bye
  • Les fonctions passées doivent retourner un interface{}pour satisfaire l'opération de transtypage interne.
  • Selon le contexte, vous pouvez choisir de convertir la sortie en un type spécifique.
  • Si vous souhaitez renvoyer une fonction à partir de cela, vous devrez l'encapsuler comme indiqué avec c.

La solution autonome ici est également agréable, mais pourrait être moins claire pour certaines utilisations.

nobar
la source
Même si ce n'est certainement pas académique, c'est plutôt bien.
Fabien