Qual é o equivalente Go idiomático do operador ternário de C?

297

Em C / C ++ (e em muitos idiomas dessa família), um idioma comum para declarar e inicializar uma variável dependendo de uma condição usa o operador condicional ternário:

int index = val > 0 ? val : -val

Go não tem o operador condicional. Qual é a maneira mais idiomática de implementar o mesmo trecho de código acima? Cheguei à seguinte solução, mas parece bastante detalhada

var index int

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

Existe algo melhor?

Fabien
fonte
você pode inicializar o valor com a parte mais e só verificar para a sua condição de mudança, não tenho certeza que isso é melhor ainda
x29a
Muitos if / thens deveriam ter sido eliminados de qualquer maneira. Costumávamos fazer isso o tempo todo desde os dias em que escrevi meus primeiros programas BASIC, há 35 anos. Seu exemplo pode ser: int index = -val + 2 * val * (val > 0);
hyc
9
@hyc, seu exemplo está longe de ser tão legível quanto o código idiomático de go, ou mesmo a versão de C, usando o operador ternário. De qualquer forma, AFAIK, não é possível implementar esta solução no Go, pois um booleano não pode ser usado como um valor numérico.
Fabien
Querendo saber por que ir não forneceu tal operador?
Eric Wang
@EricWang Duas razões, AFAIK: 1- você não precisa, e eles queriam manter o idioma o menor possível. 2- tende a ser abusado, ou seja, usado em expressões complicadas de várias linhas, e os designers de linguagem não gostam disso.
Fabien

Respostas:

244

Como apontado (e espero que não surpreenda), usar if+elseé realmente a maneira idiomática de fazer condicionais no Go.

Além do var+if+elsebloco de código completo , porém, essa ortografia também é usada com frequência:

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

e se você tiver um bloco de código que seja repetitivo o suficiente, como o equivalente a int value = a <= b ? a : b, poderá criar uma função para mantê-lo:

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

...

value := min(a, b)

O compilador incorporará funções simples, por isso é rápido, mais claro e mais curto.

Gustavo Niemeyer
fonte
184
Ei pessoal, olhem! Acabei de transportar o operador ternário para os golangs! play.golang.org/p/ZgLwC_DHm0 . Tão eficiente!
Thwd 14/11
28
@ tomwilde sua solução parece bastante interessante, mas carece de um dos principais recursos do operador ternário - avaliação condicional.
21413 Vladimir Matveev
12
@VladimirMatveev empacota os valores nos fechamentos;)
nemo
55
c := (map[bool]int{true: a, false: a - 1})[a > b]é um exemplo de ofuscação IMHO, mesmo que funcione.
21815 Rick-777
34
Se if/elseé a abordagem idiomática então talvez golang poderia considerar deixar if/elsecláusulas retornar um valor: x = if a {1} else {0}. O Go não seria de modo algum o único idioma para funcionar dessa maneira. Um exemplo principal é o Scala. Veja: alvinalexander.com/scala/scala-ternary-operator-syntax
Max Murphy
80

O No Go não possui um operador ternário, usando a sintaxe if / else é a maneira idiomática.

Por que o Go não possui o operador?:?

Não há operação de teste ternário no Go. Você pode usar o seguinte para alcançar o mesmo resultado:

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

O motivo ?:é ausente de Go é que os designers da linguagem viram a operação ser usada com muita frequência para criar expressões impenetrávelmente complexas. A if-elseforma, embora mais longa, é inquestionavelmente mais clara. Uma linguagem precisa apenas de uma construção de fluxo de controle condicional.

- Perguntas freqüentes (FAQ) - The Go Programming Language

ishaaq
fonte
1
Então, apenas porque o que os designers de linguagem viram, eles omitiram uma linha para um if-elsebloco inteiro ? E quem disse que if-elsenão é abusado da mesma maneira? Não estou atacando você, apenas sinto que a desculpa dos designers não é válida o suficiente
Alf Moh
58

Suponha que você tenha a seguinte expressão ternária (em C):

int a = test ? 1 : 2;

A abordagem idiomática no Go seria simplesmente usar um ifbloco:

var a int

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

No entanto, isso pode não atender aos seus requisitos. No meu caso, eu precisava de uma expressão embutida para um modelo de geração de código.

Eu usei uma função anônima imediatamente avaliada:

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

Isso garante que as duas ramificações também não sejam avaliadas.

Peter Boyer
fonte
É bom saber que apenas um ramo da função anon inline é avaliado. Mas observe que casos como esse estão além do escopo do operador ternário de C.
Wolf
1
A expressão condicional C (vulgarmente conhecido como o operador ternário) tem três operandos: expr1 ? expr2 : expr3. Se expr1avalia como true, expr2é avaliado e é o resultado da expressão. Caso contrário, expr3é avaliado e fornecido como resultado. Isto é da seção de linguagem de programação ANSI C 2.11 da K&R. A solução My Go preserva essas semânticas específicas. @Wolf Você pode esclarecer o que está sugerindo?
Peter Boyer
Não tenho certeza do que eu tinha em mente, talvez as funções anon forneçam um escopo (espaço de nome local) que não é o caso do operador ternário em C / C ++. Veja um exemplo para usar esse escopo
Wolf
39

O ternário do mapa é fácil de ler sem parênteses:

c := map[bool]int{true: 1, false: 0} [5 > 4]
user1212212
fonte
Não sei ao certo por que ele tem -2 ... sim, é uma solução alternativa, mas funciona e é seguro para o tipo.
Alessandro Santini
30
Sim, funciona, é seguro para o tipo e é até criativo; no entanto, existem outras métricas. As operações ternárias são equivalentes ao tempo de execução de if / else (veja, por exemplo, esta publicação de S / O ). Essa resposta não ocorre porque 1) os dois ramos são executados, 2) cria um mapa 3) chama um hash. Todos estes são "rápidos", mas não tão rápidos quanto um if / else. Além disso, eu diria que não é mais legível do que var r T se a condição {r = foo ()} else {r = bar ()}
cavaleiro
Em outros idiomas, uso essa abordagem quando tenho várias variáveis ​​e com fechamentos ou ponteiros de função ou saltos. Escrever ifs aninhados se torna propenso a erros à medida que o número de variáveis ​​aumenta, enquanto, por exemplo, {(0,0,0) => {code1}, (0,0,1) => {code2} ...} [(x> 1 , y> 1, z> 1)] (pseudocódigo) se torna cada vez mais atraente à medida que o número de variáveis ​​aumenta. Os fechamentos mantêm esse modelo rápido. Espero que trocas similares se apliquem em movimento.
Max Murphy
Suponho que você usaria uma opção para esse modelo. Adoro o modo como os interruptores quebram automaticamente, mesmo que ocasionalmente seja inconveniente.
Max Murphy
8
como Cassy Foesch apontou: 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)
}

Isso não terá um desempenho superior a if / else e requer conversão, mas funciona. PARA SUA INFORMAÇÃO:

BenchmarkAbsTernary-8 100000000 18,8 ns / op

BenchmarkAbsIfElse-8 2000000000 0,27 ns / op

Phillip Dominy
fonte
Esta é a melhor solução, parabéns! Uma linha que lidar com todos os possíveis casos
Alexandro de Oliveira
2
Eu não acho que isso lida com avaliação condicional, ou não? Com ramificações livres de efeitos colaterais, isso não importa (como no seu exemplo), mas se for algo com efeitos colaterais, você terá problemas.
Ashton Wiersdorf 26/09/19
7

Se todas as suas ramificações produzem efeitos colaterais ou são computacionalmente caras, o seguinte seria uma refatoração que preserva semanticamente :

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

normalmente sem sobrecarga (embutida) e, o mais importante, sem sobrecarregar seu espaço de nome com funções auxiliares que são usadas apenas uma vez (o que dificulta a legibilidade e a manutenção). Exemplo ao vivo

Observe se você deveria aplicar ingenuamente a abordagem de Gustavo :

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

você obteria um programa com um comportamento diferente ; caso o val <= 0programa imprima um valor não positivo enquanto não deveria! (Analogamente, se você revertesse as ramificações, introduziria sobrecarga chamando uma função lenta desnecessariamente.)

eold
fonte
1
Leitura interessante, mas não estou realmente entendendo o ponto de suas críticas à abordagem de Gustavo. Eu vejo uma (tipo de) absfunção no código original (bem, eu mudaria <=para <). No seu exemplo, vejo uma inicialização que é redundante em alguns casos e pode ser expansiva. Você pode esclarecer: explique um pouco mais a sua ideia?
Wolf
A principal diferença é que chamar uma função fora de qualquer ramo causará efeitos colaterais, mesmo que esse ramo não deva ter sido tomado. No meu caso, apenas números positivos serão impressos porque a função printPositiveAndReturné chamada apenas para números positivos. Por outro lado, sempre executando uma ramificação, "fixar" o valor com a execução de uma ramificação diferente não desfaz os efeitos colaterais da primeira ramificação .
eold
Entendo, mas as experiências dos programadores normalmente estão cientes dos efeitos colaterais. Nesse caso, prefiro a solução óbvia de Cassy Foesch a uma função incorporada, mesmo que o código compilado seja o mesmo: é mais curto e parece óbvio para a maioria dos programadores. Não me interpretem mal: eu realmente amo fechamentos da GO;)
Lobo
1
" experiências que os programadores normalmente estão cientes dos efeitos colaterais " - Não. Evitar a avaliação de termos é uma das principais características de um operador ternário.
Jonathan Hartley
6

Prefácio: Sem argumentando queif else é o caminho a seguir, ainda podemos brincar e encontrar prazer em construções habilitadas para idiomas.

A Ifconstrução a seguir está disponível na minha github.com/icza/goxbiblioteca com muitos outros métodos, sendo o builtinx.Iftipo


Go permite anexar métodos a qualquer tipo definido pelo usuário , incluindo tipos primitivos como bool. Podemos criar um tipo personalizado tendo boolcomo tipo subjacente e, em seguida, com uma simples conversão de tipo na condição, teremos acesso a seus métodos. Métodos que recebem e selecionam dos operandos.

Algo assim:

type If bool

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

Como podemos usá-lo?

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

Por exemplo, um ato ternário max():

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

Um ato ternário abs():

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

Parece legal, é simples, elegante e eficiente (também é elegível para inlining ).

Uma desvantagem em comparação com um operador ternário "real": ele sempre avalia todos os operandos.

Para obter uma avaliação adiada e apenas se necessário, a única opção é usar funções ( funções ou métodos declarados ou literais de função ), que são chamados apenas quando / se necessário:

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

Usando-o: Vamos assumir que temos essas funções para calcular ae b:

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

Então:

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

Por exemplo, a condição sendo o ano atual> 2020:

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

Se queremos usar literais de função:

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

Nota final: se você tiver funções com diferentes assinaturas, não poderá usá-las aqui. Nesse caso, você pode usar uma função literal com assinatura correspondente para torná-las ainda aplicáveis.

Por exemplo, se calca()e calcb()também teria parâmetros (além do valor de retorno):

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

É assim que você pode usá-los:

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

Experimente estes exemplos no Go Playground .

icza
fonte
4

A resposta de Eold é interessante e criativa, talvez até inteligente.

No entanto, seria recomendável fazer:

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

Sim, ambos são compilados essencialmente no mesmo assembly, no entanto, esse código é muito mais legível do que chamar uma função anônima apenas para retornar um valor que poderia ter sido gravado na variável em primeiro lugar.

Basicamente, código simples e claro é melhor que código criativo.

Além disso, qualquer código que use um literal de mapa não é uma boa ideia, porque os mapas não são leves no Go. Desde o Go 1.3, a ordem de iteração aleatória para mapas pequenos é garantida e, para impor isso, é um pouco menos eficiente em termos de memória para mapas pequenos.

Como resultado, fazer e remover vários pequenos mapas consome espaço e consome tempo. Eu tinha um pedaço de código que usava um mapa pequeno (provavelmente duas ou três chaves, mas o caso de uso comum era apenas uma entrada). Mas o código era lento. Estamos falando de pelo menos três ordens de magnitude mais lentas que o mesmo código reescrito para usar um mapa de chaves de índice duplo [index] => data [index]. E provavelmente era mais. Como algumas operações que estavam demorando alguns minutos para serem executadas, começaram a ser concluídas em milissegundos. \

Cassy Foesch
fonte
1
simple and clear code is better than creative code- isso eu gosto muito, mas estou ficando um pouco confuso na última seção depois dog slow, talvez isso possa ser confuso para os outros também?
Wolf
1
Então, basicamente ... eu tinha um código que estava criando pequenos mapas com uma, duas ou três entradas, mas o código estava sendo executado muito lentamente. Assim, muitas m := map[string]interface{} { a: 42, b: "stuff" }e, em seguida, em outra função iterando através dele: for key, val := range m { code here } Depois de mudar para um sistema de duas fatias keys = []string{ "a", "b" }, data = []interface{}{ 42, "stuff" }:, e depois iterar como se as for i, key := range keys { val := data[i] ; code here }coisas acelerassem 1000 vezes.
Cassy Foesch
Entendo, obrigado pelo esclarecimento. (Talvez a resposta em si poderia ser melhorado nesse ponto.)
Lobo
1
-.- ... touché, a lógica ... touché ... eu vou chegar em que, eventualmente, ...;)
Cassy Foesch
3

As frases de linha, embora evitadas pelos criadores, têm seu lugar.

Este resolve o problema de avaliação lenta, permitindo que você, opcionalmente, passe as funções a serem avaliadas, se necessário:

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

Resultado

hello
hello world
hello world
hello
bye
  • As funções transmitidas devem retornar um interface{}para satisfazer a operação de conversão interna.
  • Dependendo do contexto, você pode optar por converter a saída para um tipo específico.
  • Se você quisesse retornar uma função disso, seria necessário agrupá-la como mostrado em c.

A solução independente aqui também é boa, mas pode ser menos clara para alguns usos.

nobar
fonte
Mesmo que isso definitivamente não seja acadêmico, isso é muito bom.
Fabien