¿Cuál es el Go idiomático equivalente del operador ternario de C?

297

En C / C ++ (y en muchos idiomas de esa familia), un idioma común para declarar e inicializar una variable dependiendo de una condición utiliza el operador condicional ternario:

int index = val > 0 ? val : -val

Go no tiene el operador condicional. ¿Cuál es la forma más idiomática de implementar el mismo código que el anterior? Llegué a la siguiente solución, pero parece bastante detallada

var index int

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

¿Hay algo mejor?

Fabien
fuente
puede inicializar el valor con la parte else y solo verificar si su condición cambia, aunque no estoy seguro de que sea mejor
x29a
Muchos if / thens deberían haberse eliminado de todos modos. Solíamos hacer esto todo el tiempo desde los días en que escribí mis primeros programas BASIC hace 35 años. Su ejemplo podría ser: int index = -val + 2 * val * (val > 0);
hyc
99
@hyc su ejemplo está lejos de ser tan legible como el código idiomático de go, o incluso como la versión de C que usa el operador ternario. De todos modos, AFAIK, no es posible implementar esta solución en Go, ya que un booleano no se puede usar como un valor numérico.
Fabien
¿Se pregunta por qué ir no proporcionó tal operador?
Eric Wang
@EricWang Dos razones, AFAIK: 1- no lo necesitas, y querían mantener el idioma lo más pequeño posible. 2- tiende a ser abusado, es decir, usado en expresiones de líneas múltiples enrevesadas, y a los diseñadores de lenguaje no les gusta.
Fabien

Respuestas:

244

Como se señaló (y es de esperar, como era de esperar), el uso if+elsees de hecho la forma idiomática de hacer condicionales en Go.

Sin var+if+elseembargo, además del bloque de código completo , esta ortografía también se usa a menudo:

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

y si tiene un bloque de código que es lo suficientemente repetitivo, como el equivalente de int value = a <= b ? a : b, puede crear una función para contenerlo:

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

...

value := min(a, b)

El compilador incorporará funciones tan simples, por lo que es rápido, más claro y más corto.

Gustavo Niemeyer
fuente
184
Hola chicos, miren! ¡Acabo de portar el operador ternarity a los golangs! play.golang.org/p/ZgLwC_DHm0 . Tan eficiente!
thwd
28
@tomwilde su solución parece bastante interesante, pero carece de una de las características principales del operador ternario: la evaluación condicional.
Vladimir Matveev
12
@VladimirMatveev ajusta los valores en cierres;)
nemo
55
c := (map[bool]int{true: a, false: a - 1})[a > b]es un ejemplo de ofuscación en mi humilde opinión, incluso si funciona.
Rick-777
34
Si if/elsees el enfoque idiomática entonces tal vez podría Golang considere dejar if/elsecláusulas devuelven un valor: x = if a {1} else {0}. Ir no sería el único idioma para trabajar de esta manera. Un ejemplo convencional es Scala. Ver: alvinalexander.com/scala/scala-ternary-operator-syntax
Max Murphy
80

No Go no tiene un operador ternario, el uso de la sintaxis if / else es la forma idiomática.

¿Por qué Go no tiene el operador?:

No hay operación de prueba ternaria en Go. Puede usar lo siguiente para lograr el mismo resultado:

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

La razón ?:por la que Go está ausente es que los diseñadores del lenguaje habían visto que la operación se usaba con demasiada frecuencia para crear expresiones impenetrablemente complejas. La if-elseforma, aunque más larga, es indudablemente más clara. Un lenguaje necesita solo una construcción de flujo de control condicional.

- Preguntas frecuentes (FAQ) - El lenguaje de programación Go

ishaaq
fuente
1
Entonces, ¿solo porque lo que han visto los diseñadores de idiomas han omitido una frase para todo un if-elsebloque? ¿Y quién dice que if-elseno se abusa de la misma manera? No te estoy atacando, solo siento que la excusa de los diseñadores no es lo suficientemente válida
Alf Moh
58

Suponga que tiene la siguiente expresión ternaria (en C):

int a = test ? 1 : 2;

El enfoque idiomático en Go sería simplemente usar un ifbloque:

var a int

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

Sin embargo, eso podría no ajustarse a sus requisitos. En mi caso, necesitaba una expresión en línea para una plantilla de generación de código.

Usé una función anónima evaluada inmediatamente:

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

Esto asegura que ambas ramas no sean evaluadas también.

Peter Boyer
fuente
Es bueno saber que solo se evalúa una rama de la función anón en línea. Pero tenga en cuenta que casos como este están más allá del alcance del operador ternario de C.
Lobo
1
La expresión condicional C (comúnmente conocido como el operador ternario) tiene tres operandos: expr1 ? expr2 : expr3. Si se expr1evalúa como true, expr2se evalúa y es el resultado de la expresión. De lo contrario, expr3se evalúa y se proporciona como resultado. Esto es de la sección 2.11 del lenguaje de programación ANSI C de K&R. La solución My Go conserva estas semánticas específicas. @ Wolf ¿Puedes aclarar lo que estás sugiriendo?
Peter Boyer
No estoy seguro de lo que tenía en mente, tal vez las funciones anon proporcionan un ámbito (espacio de nombres local) que no es el caso con el operador ternario en C / C ++. Vea un ejemplo para usar este alcance
Wolf
39

El mapa ternario es fácil de leer sin paréntesis:

c := map[bool]int{true: 1, false: 0} [5 > 4]
usuario1212212
fuente
No estoy completamente seguro de por qué tiene -2 ... sí, es una solución pero funciona y es de tipo seguro.
Alessandro Santini
30
Sí, funciona, es de tipo seguro e incluso creativo; Sin embargo, hay otras métricas. Las operaciones ternarias son tiempo de ejecución equivalente a if / else (ver, por ejemplo, esta publicación de S / O ). Esta respuesta no es porque 1) ambas ramas se ejecutan, 2) crea un mapa 3) llama a un hash. Todos estos son "rápidos", pero no tan rápidos como un if / else. Además, diría que no es más legible que var r T si la condición {r = foo ()} else {r = bar ()}
caballero del
En otros idiomas, uso este enfoque cuando tengo múltiples variables y con cierres o punteros de función o saltos. Escribir ifs anidados se vuelve propenso a errores a medida que aumenta el número de variables, mientras que, por ejemplo, {(0,0,0) => {code1}, (0,0,1) => {code2} ...} [(x> 1 , y> 1, z> 1)] (pseudocódigo) se vuelve más y más atractivo a medida que aumenta el número de variables. Los cierres mantienen este modelo rápido. Espero que se apliquen compensaciones similares en go.
Max Murphy
Supongo que irías usarías un interruptor para ese modelo. Me encanta la forma en que los interruptores se rompen automáticamente, incluso si ocasionalmente es inconveniente.
Max Murphy
8
como Cassy Foesch señaló: 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)
}

Esto no superará si / de lo contrario y requiere lanzamiento pero funciona. FYI:

BenchmarkAbsTernary-8 100000000 18.8 ns / op

BenchmarkAbsIfElse-8 2000000000 0.27 ns / op

Phillip Dominy
fuente
¡Esta es la mejor solución, felicidades! Una línea que maneja todos los casos posibles
Alexandro de Oliveira
2
No creo que esto maneje la evaluación condicional, ¿o sí? Con las ramas libres de efectos secundarios esto no importa (como en su ejemplo), pero si es algo con efectos secundarios, se encontrará con problemas.
Ashton Wiersdorf
7

Si todas sus ramas producen efectos secundarios o son computacionalmente costosas, lo siguiente sería una refactorización de preservación semántica :

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

normalmente sin sobrecarga (en línea) y, lo más importante, sin saturar su espacio de nombres con funciones auxiliares que solo se usan una vez (lo que dificulta la legibilidad y el mantenimiento). Ejemplo en vivo

Observe si aplicara ingenuamente el enfoque de Gustavo :

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

obtendrías un programa con un comportamiento diferente ; ¡en caso de que el val <= 0programa imprima un valor no positivo mientras no debería! (De manera análoga, si invierte las ramas, introduciría sobrecarga llamando innecesariamente a una función lenta).

Eold
fuente
1
Lectura interesante, pero realmente no entiendo el punto en su crítica del enfoque de Gustavo. Veo una (tipo de) absfunción en el código original (bueno, me gustaría cambiar <=a <). En su ejemplo veo una inicialización, que en algunos casos es redundante y podría ser expansiva. ¿Puedes aclarar: explica tu idea un poco más?
Lobo
La principal diferencia es que llamar a una función fuera de cualquiera de las ramas producirá efectos secundarios, incluso si esa rama no se hubiera tomado. En mi caso, solo se imprimirán números positivos porque la función printPositiveAndReturnsolo se llama para números positivos. Por el contrario, siempre ejecutando una rama, luego "arreglar" el valor con la ejecución de una rama diferente no deshace los efectos secundarios de la primera rama .
pasado
Ya veo, pero los programadores de experiencias normalmente son conscientes de los efectos secundarios. En ese caso, preferiría la solución obvia de Cassy Foesch a una función incrustada, incluso si el código compilado puede ser el mismo: es más corto y parece obvio para la mayoría de los programadores. No me malinterpreten: realmente me encantan los cierres de Go;)
Wolf
1
"los programadores de experiencias normalmente son conscientes de los efectos secundarios " - No. Evitar la evaluación de términos es una de las características principales de un operador ternario.
Jonathan Hartley
6

Prefacio: Sin discutir que ese if elsees el camino a seguir, aún podemos jugar y encontrar placer en las construcciones habilitadas por el lenguaje.

La siguiente Ifconstrucción está disponible en mi github.com/icza/goxbiblioteca con muchos otros métodos, siendo el builtinx.Iftipo.


Go permite adjuntar métodos a cualquier tipo definido por el usuario , incluidos los tipos primitivos como bool. Podemos crear un tipo personalizado teniendo boolcomo tipo subyacente , y luego con una conversión de tipo simple en la condición, tenemos acceso a sus métodos. Métodos que reciben y seleccionan de los operandos.

Algo como esto:

type If bool

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

¿Cómo podemos usarlo?

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

Por ejemplo, un ternario que hace max():

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

Un ternario haciendo abs():

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

Esto se ve genial, es simple, elegante y eficiente (también es elegible para la inserción ).

Una desventaja en comparación con un operador ternario "real": siempre evalúa todos los operandos.

Para lograr una evaluación diferida y solo si es necesaria, la única opción es usar funciones (ya sea funciones o métodos declarados , o literales de función ), que solo se llaman cuando / si es necesario:

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

Uso: supongamos que tenemos estas funciones para calcular ay b:

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

Luego:

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

Por ejemplo, la condición es el año actual> 2020:

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

Si queremos usar literales de función:

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

Nota final: si tuviera funciones con diferentes firmas, no podría usarlas aquí. En ese caso, puede usar una función literal con la firma correspondiente para que sigan siendo aplicables.

Por ejemplo, si calca()y calcb()también tuviera parámetros (además del valor de retorno):

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

Así es como puedes usarlos:

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

Pruebe estos ejemplos en Go Playground .

icza
fuente
4

La respuesta de eold es interesante y creativa, tal vez incluso inteligente.

Sin embargo, se recomienda hacer en su lugar:

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

Sí, ambos se compilan esencialmente en el mismo ensamblado, sin embargo, este código es mucho más legible que llamar a una función anónima solo para devolver un valor que podría haberse escrito en la variable en primer lugar.

Básicamente, un código simple y claro es mejor que el código creativo.

Además, cualquier código que use un literal de mapa no es una buena idea, porque los mapas no son ligeros en absoluto en Go. Desde Go 1.3, el orden de iteración aleatorio para mapas pequeños está garantizado, y para hacer cumplir esto, se ha vuelto un poco menos eficiente en cuanto a memoria para mapas pequeños.

Como resultado, hacer y eliminar numerosos mapas pequeños requiere mucho espacio y tiempo. Tenía un código que usaba un mapa pequeño (es probable que haya dos o tres teclas, pero el caso de uso común era solo una entrada) Pero el código era lento. Estamos hablando de al menos 3 órdenes de magnitud más lento que el mismo código reescrito para usar una clave de división dual [index] => data [index] map. Y probablemente fue más. Como algunas operaciones que antes tardaban un par de minutos en ejecutarse, comenzaron a completarse en milisegundos.

Cassy Foesch
fuente
1
simple and clear code is better than creative code- Esto me gusta mucho, pero me estoy confundiendo un poco en la última sección después dog slow, ¿tal vez esto también podría ser confuso para los demás?
Wolf
1
Entonces, básicamente ... Tenía un código que creaba pequeños mapas con una, dos o tres entradas, pero el código se ejecutaba muy lentamente. Entonces, mucho m := map[string]interface{} { a: 42, b: "stuff" }, y luego en otra función iterando a través de él: for key, val := range m { code here } Después de cambiar a un sistema de dos divisiones:, keys = []string{ "a", "b" }, data = []interface{}{ 42, "stuff" }y luego iterar a través de for i, key := range keys { val := data[i] ; code here }cosas similares aceleradas 1000 veces.
Cassy Foesch
Ya veo, gracias por la aclaración. (Tal vez la respuesta en sí misma podría mejorarse en este punto.)
Wolf
1
-.- ... touché, lógica ... touché ... Eventualmente lo entenderé ...;)
Cassy Foesch
3

Las frases ingenuas, aunque rechazadas por los creadores, tienen su lugar.

Este resuelve el problema de la evaluación diferida permitiéndole, opcionalmente, pasar funciones para evaluar si es necesario:

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

Salida

hello
hello world
hello world
hello
bye
  • Las funciones pasadas deben devolver una interface{}para satisfacer la operación de conversión interna.
  • Dependiendo del contexto, puede elegir convertir la salida a un tipo específico.
  • Si desea devolver una función de esto, necesitaría ajustarla como se muestra con c.

La solución independiente aquí también es buena, pero podría ser menos clara para algunos usos.

sin bar
fuente
Incluso si esto definitivamente no es académico, esto es bastante bueno.
Fabien