Qual è l'equivalente Go idiomatico dell'operatore ternario di C?


297

In C / C ++ (e molte lingue di quella famiglia), un linguaggio comune per dichiarare e inizializzare una variabile a seconda di una condizione usa l'operatore condizionale ternario:

int index = val > 0 ? val : -val

Go non ha l'operatore condizionale. Qual è il modo più idiomatico per implementare lo stesso codice di cui sopra? Sono arrivato alla seguente soluzione, ma sembra abbastanza prolisso

var index int

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

C'è qualcosa di meglio?


potresti inizializzare il valore con l'altra parte e controllare solo se le tue condizioni cambiano, non sono sicuro che sia meglio però
x29a

Un sacco di se / thens avrebbe dovuto essere eliminato comunque. Lo facevamo sempre dai giorni in cui ho scritto i miei primi programmi BASIC 35 anni fa. Il tuo esempio potrebbe essere: int index = -val + 2 * val * (val > 0);
hyc,

9
@hyc il tuo esempio è lungi dall'essere leggibile come il codice idiomatico di go, o anche come la versione di C che utilizza l'operatore ternario. Ad ogni modo, AFAIK, non è possibile implementare questa soluzione in Go poiché un valore booleano non può essere utilizzato come valore numerico.
Fabien,

Ti chiedi perché go non abbia fornito un simile operatore?
Eric Wang,

@EricWang Due motivi, AFAIK: 1- non ne hai bisogno, e volevano mantenere la lingua il più piccola possibile. 2- tende ad essere abusato, cioè utilizzato in espressioni contorte a più righe, e ai progettisti del linguaggio non piace.
Fabien

Risposte:


244

Come sottolineato (e speriamo non sorprende), l'uso if+elseè davvero il modo idiomatico di fare condizionali in Go.

Oltre al var+if+elseblocco completo del codice, tuttavia, questa ortografia viene spesso utilizzata anche:

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

e se hai un blocco di codice abbastanza ripetitivo, come l'equivalente di int value = a <= b ? a : b, puoi creare una funzione per tenerlo:

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

...

value := min(a, b)

Il compilatore incorporerà funzioni così semplici, quindi è veloce, più chiaro e più breve.


184
Ehi ragazzi, guardate! Ho appena porting del ternarity all'operatore ai golangs! play.golang.org/p/ZgLwC_DHm0 . Così efficiente!
Giovedì

28
@tomwilde la tua soluzione sembra piuttosto interessante, ma manca di una delle caratteristiche principali dell'operatore ternario: la valutazione condizionale.
Vladimir Matveev,

12
@VladimirMatveev racchiude i valori nelle chiusure;)
nemo

55
c := (map[bool]int{true: a, false: a - 1})[a > b]è un esempio di offuscamento IMHO, anche se funziona.
Rick-777,

34
Se if/elseè l'approccio idiomatico allora forse Golang potrebbe considerare lasciando if/elseclausole restituiscono un valore: x = if a {1} else {0}. Go non sarebbe affatto l'unica lingua che funzioni in questo modo. Un esempio tradizionale è Scala. Vedi: alvinalexander.com/scala/scala-ternary-operator-syntax
Max Murphy

80

No Go non ha un operatore ternario, usando la sintassi if / else è il modo idiomatico.

Perché Go non ha l'operatore?:?

Non ci sono operazioni di test ternari in Go. È possibile utilizzare quanto segue per ottenere lo stesso risultato:

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

Il motivo ?:è assente da Go è che i progettisti del linguaggio avevano visto l'operazione usata troppo spesso per creare espressioni impenetrabili complesse. La if-elseforma, sebbene più lunga, è senza dubbio più chiara. Una lingua richiede solo un costrutto del flusso di controllo condizionale.

- Domande frequenti (FAQ) - Il linguaggio di programmazione Go


1
Quindi, solo perché ciò che i designer di linguaggi hanno visto, hanno omesso una riga per un intero if-elseblocco? E chi dice che if-elsenon sia abusato in modo simile? Non ti sto attaccando, sento solo che la scusa dei designer non è abbastanza valida
Alf Moh,

58

Supponiamo di avere la seguente espressione ternaria (in C):

int a = test ? 1 : 2;

L'approccio idiomatico in Go sarebbe semplicemente usare un ifblocco:

var a int

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

Tuttavia, potrebbe non essere adatto alle tue esigenze. Nel mio caso, avevo bisogno di un'espressione incorporata per un modello di generazione del codice.

Ho usato una funzione anonima immediatamente valutata:

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

Ciò garantisce che anche i due rami non vengano valutati.


Buono a sapersi che viene valutato solo un ramo della funzione anon incorporata. Ma si noti che casi come questo vanno oltre l'ambito dell'operatore ternario di C.
Lupo,

1
L'espressione condizionale C (comunemente noto come operatore ternario) ha tre operandi: expr1 ? expr2 : expr3. Se viene expr1valutato true, expr2viene valutato ed è il risultato dell'espressione. In caso contrario, expr3viene valutato e fornito come risultato. Questo è tratto dalla sezione 2.11 del linguaggio di programmazione ANSI C di K&R. La soluzione My Go conserva queste specifiche semantiche. @Wolf Puoi chiarire cosa stai suggerendo?
Peter Boyer,

Non sono sicuro di ciò che avevo in mente, forse le funzioni anon forniscono un ambito (spazio dei nomi locale) che non è il caso dell'operatore ternario in C / C ++. Vedi un esempio per l'utilizzo di questo ambito
Wolf,

39

La mappa ternaria è di facile lettura senza parentesi:

c := map[bool]int{true: 1, false: 0} [5 > 4]

Non sono del tutto sicuro del motivo per cui ha -2 ... sì, è una soluzione alternativa ma funziona ed è sicuro per i tipi.
Alessandro Santini,

30
Sì, funziona, è sicuro per i tipi ed è persino creativo; tuttavia, ci sono altre metriche. Le operazioni ternarie sono equivalenti al tempo di esecuzione di if / else (vedere ad esempio questo post S / O ). Questa risposta non è perché 1) vengono eseguiti entrambi i rami, 2) crea una mappa 3) chiama un hash. Tutti questi sono "veloci", ma non veloci come un if / else. Inoltre, direi che non è più leggibile di var r T se la condizione {r = foo ()} else {r = bar ()}
cavaliere

In altre lingue utilizzo questo approccio quando ho più variabili e con chiusure o puntatori a funzioni o salti. Scrivere if annidati diventa soggetto a errori man mano che aumenta il numero di variabili, mentre ad es. {(0,0,0) => {code1}, (0,0,1) => {code2} ...} [(x> 1 , y> 1, z> 1)] (pseudocodice) diventa sempre più attraente con l'aumentare del numero di variabili. Le chiusure mantengono questo modello veloce. Mi aspetto che simili compromessi si applichino in atto.
Max Murphy,

Suppongo che in questo caso useresti un interruttore per quel modello. Adoro il modo in cui gli interruttori si rompono automaticamente, anche se a volte è scomodo.
Max Murphy,

8
come ha sottolineato 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)
}

Questo non supererà se / else e richiede cast ma funziona. FYI:

BenchmarkAbsTernary-8 100000000 18,8 ns / op

BenchmarkAbsIfElse-8 2000000000 0,27 ns / op


Questa è la soluzione migliore, complimenti! Una linea che gestisce tutti i casi possibili
Alexandro de Oliveira,

2
Non penso che questo gestisca la valutazione condizionale o no? Con i rami liberi degli effetti collaterali questo non ha importanza (come nel tuo esempio), ma se si tratta di qualcosa con effetti collaterali incontrerai dei problemi.
Ashton Wiersdorf,

7

Se tutti i tuoi rami producono effetti collaterali o sono computazionalmente costosi , un refactoring semanticamente conservante sarebbe il seguente :

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

normalmente senza sovraccarico (inline) e, soprattutto, senza ingombrare il tuo spazio dei nomi con funzioni di supporto che vengono utilizzate una sola volta (che ostacola la leggibilità e la manutenzione). Esempio live

Nota se dovessi applicare ingenuamente l'approccio di Gustavo :

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

otterresti un programma con un comportamento diverso ; nel caso in cui il val <= 0programma stampasse un valore non positivo mentre non dovrebbe! (Analogamente, se invertissi i rami, avresti introdotto il sovraccarico chiamando inutilmente una funzione lenta.)


1
Lettura interessante, ma non capisco davvero il punto delle tue critiche sull'approccio di Gustavo. Vedo una (sorta di) absfunzione nel codice originale (beh, cambierei <=in <). Nel tuo esempio vedo un'inizializzazione, che in alcuni casi è ridondante e potrebbe essere espansiva. Potete per favore chiarire: spiegare un po 'di più la vostra idea?
Lupo,

La differenza principale è che chiamare una funzione al di fuori di entrambi i rami produrrà effetti collaterali anche se quel ramo non avrebbe dovuto essere preso. Nel mio caso, verranno stampati solo numeri positivi perché la funzione printPositiveAndReturnviene chiamata solo per numeri positivi. Viceversa, eseguire sempre un ramo, quindi "correggere" il valore con l'esecuzione di un ramo diverso non annulla gli effetti collaterali del primo ramo .
vecchio

Vedo, ma le esperienze dei programmatori sono normalmente consapevoli degli effetti collaterali. In tal caso preferirei l'ovvia soluzione di Cassy Foesch a una funzione incorporata, anche se il codice compilato potrebbe essere lo stesso: è più breve e sembra ovvio per la maggior parte dei programmatori. Non fraintendetemi: adoro le chiusure di Go;)
Wolf,

1
" esperienze che i programmatori sono normalmente consapevoli degli effetti collaterali " - No. Evitare la valutazione dei termini è una delle caratteristiche principali di un operatore ternario.
Jonathan Hartley,

6

Premessa: Senza discutere che if elsesia la strada da percorrere, possiamo ancora giocare e trovare piacere nei costrutti abilitati al linguaggio.

Il seguente Ifcostrutto è disponibile nella mia github.com/icza/goxlibreria con molti altri metodi, essendo il builtinx.Iftipo.


Go consente di associare metodi a qualsiasi tipo definito dall'utente , inclusi tipi primitivi come bool. Possiamo creare un tipo personalizzato avente boolcome tipo sottostante , e quindi con una semplice conversione del tipo a condizione, abbiamo accesso ai suoi metodi. Metodi che ricevono e selezionano dagli operandi.

Qualcosa come questo:

type If bool

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

Come possiamo usarlo?

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

Ad esempio un ternario che fa max():

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

Un ternario che fa abs():

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

Sembra bello, semplice, elegante ed efficiente (è idoneo anche per l'allineamento ).

Un aspetto negativo rispetto a un "vero" operatore ternario: valuta sempre tutti gli operandi.

Per ottenere una valutazione differita e solo se necessaria, l'unica opzione è utilizzare le funzioni ( funzioni o metodi dichiarati o valori letterali delle funzioni ), che vengono chiamate solo quando / se necessario:

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

Usandolo: supponiamo di avere queste funzioni per calcolare ae b:

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

Poi:

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

Ad esempio, la condizione è l'anno corrente> 2020:

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

Se vogliamo usare i letterali delle funzioni:

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

Nota finale: se avessi funzioni con firme diverse, non potresti usarle qui. In tal caso è possibile utilizzare una funzione letterale con firma corrispondente per renderli ancora applicabili.

Ad esempio se calca()e calcb()avrebbe anche dei parametri (oltre al valore restituito):

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

Ecco come potresti usarli:

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

Prova questi esempi sul Go Playground .


4

la risposta di eold è interessante e creativa, forse persino intelligente.

Tuttavia, si consiglia invece di fare:

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

Sì, entrambi si compilano essenzialmente nello stesso assembly, tuttavia questo codice è molto più leggibile che chiamare una funzione anonima solo per restituire un valore che avrebbe potuto essere scritto nella variabile in primo luogo.

Fondamentalmente, il codice semplice e chiaro è meglio del codice creativo.

Inoltre, qualsiasi codice che utilizza una mappa letterale non è una buona idea, poiché le mappe non sono affatto leggere in Go. Da Go 1.3, l'ordine di iterazione casuale per le mappe di piccole dimensioni è garantito e, per far ciò, è diventato un po 'meno efficiente dal punto di vista della memoria per le mappe di piccole dimensioni.

Di conseguenza, la creazione e la rimozione di numerose piccole mappe richiede tempo e spazio. Avevo un pezzo di codice che utilizzava una piccola mappa (probabilmente due o tre chiavi, ma il caso d'uso comune era solo una voce) Ma il codice era lento. Stiamo parlando di almeno 3 ordini di grandezza più lenti rispetto allo stesso codice riscritto per utilizzare una chiave dual slice [index] => data [index] map. E probabilmente era di più. Poiché alcune operazioni che in precedenza richiedevano un paio di minuti per l'esecuzione, iniziarono a completarsi in millisecondi. \


1
simple and clear code is better than creative code- questo mi piace molto, ma mi sto confondendo un po 'nell'ultima sezione dopo dog slow, forse questo potrebbe essere fonte di confusione anche per gli altri?
Lupo,

1
Quindi, in sostanza ... Avevo del codice che creava piccole mappe con una, due o tre voci, ma il codice funzionava molto lentamente. Quindi, un sacco di m := map[string]interface{} { a: 42, b: "stuff" }, e poi in un'altra funzione che scorre attraverso di esso: for key, val := range m { code here } dopo essere passati a un sistema a due sezioni keys = []string{ "a", "b" }, data = []interface{}{ 42, "stuff" }:, e quindi scorrere attraverso le for i, key := range keys { val := data[i] ; code here }cose come accelerato di 1000 volte.
Cassy Foesch,

Vedo, grazie per il chiarimento. (Forse la risposta stessa potrebbe essere migliorata in questo punto.)
Wolf,

1
-.- ... Touché, logica ... Touché ... Prendo il che alla fine ...;)
Cassy Foesch

3

I one-liner, sebbene evitati dai creatori, hanno il loro posto.

Questo risolve il problema della valutazione lenta consentendo, facoltativamente, di passare funzioni da valutare se necessario:

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)())
}

Produzione

hello
hello world
hello world
hello
bye
  • Le funzioni passate devono restituire an interface{}per soddisfare l'operazione di cast interna.
  • A seconda del contesto, è possibile scegliere di trasmettere l'output a un tipo specifico.
  • Se si desidera restituire una funzione da questa, è necessario avvolgerla come mostrato c.

Anche la soluzione standalone qui è bella, ma potrebbe essere meno chiara per alcuni usi.


Anche se questo non è assolutamente accademico, è abbastanza carino.
Fabien,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.