Jaki jest idiomatyczny odpowiednik Go operatora trójskładnikowego C?

297

W C / C ++ (i wielu językach tej rodziny) wspólny idiom do deklarowania i inicjalizacji zmiennej w zależności od warunku używa potrójnego operatora warunkowego:

int index = val > 0 ? val : -val

Go nie ma operatora warunkowego. Jaki jest najbardziej idiomatyczny sposób implementacji tego samego fragmentu kodu jak powyżej? Doszedłem do następującego rozwiązania, ale wydaje się dość gadatliwe

var index int

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

Czy jest coś lepszego?

Fabien
źródło
możesz zainicjować wartość z częścią else i tylko sprawdzić, czy twój stan się zmienił, ale nie jestem pewien, czy to lepiej
x29a
Zresztą i tak wiele powinno zostać wyeliminowanych. Robiliśmy to cały czas od czasów, gdy pisałem swoje pierwsze programy BASIC 35 lat temu. Twoim przykładem może być: int index = -val + 2 * val * (val > 0);
hyc
9
@ hyc twój przykład jest daleki od bycia tak czytelnym jak idiomatyczny kod go, a nawet wersja C korzystająca z operatora trójskładnikowego. W każdym razie, AFAIK, nie jest możliwe zaimplementowanie tego rozwiązania w Go, ponieważ boolean nie może być użyty jako wartość liczbowa.
Fabien
Zastanawiasz się, dlaczego go nie zapewnił takiego operatora?
Eric Wang
@EricWang Dwa powody, AFAIK: 1- nie potrzebujesz go, a oni chcieli, aby język był jak najmniejszy. 2 - jest nadużywane, tzn. Używane w skomplikowanych wyrażeniach wieloliniowych, a projektantom językowym się to nie podoba.
Fabien

Odpowiedzi:

244

Jak wskazano (i miejmy nadzieję, że nic dziwnego), używanie if+elsejest w rzeczywistości idiomatycznym sposobem wykonywania warunkowych w Go.

Oprócz pełnego var+if+elsebloku kodu pisownia ta jest również często używana:

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

a jeśli masz blok kodu, który jest wystarczająco powtarzalny, taki jak jego odpowiednik int value = a <= b ? a : b, możesz utworzyć funkcję, aby go zatrzymać:

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

...

value := min(a, b)

Kompilator wprowadzi takie proste funkcje, dzięki czemu będzie szybki, wyraźniejszy i krótszy.

Gustavo Niemeyer
źródło
183
Cześć chłopaki, patrzcie! Właśnie przeniosłem operatora ternarity do golangów! play.golang.org/p/ZgLwC_DHm0 . Taki wydajny!
thwd
28
@tomwilde twoje rozwiązanie wygląda dość interesująco, ale brakuje mu jednej z głównych cech operatora trójskładnikowego - oceny warunkowej.
Vladimir Matveev
12
@VladimirMatveev zawija wartości w zamknięciach;)
nemo
55
c := (map[bool]int{true: a, false: a - 1})[a > b]jest przykładem zaciemnienia IMHO, nawet jeśli działa.
Rick-777
34
Jeśli if/elsejest idiomatyczne podejście to może Golang mogłaby rozważyć pozwalając if/elseklauzule zwrócić wartość: x = if a {1} else {0}. Go bynajmniej nie byłby jedynym językiem, który działałby w ten sposób. Przykładem głównego nurtu jest Scala. Zobacz: alvinalexander.com/scala/scala-ternary-operator-syntax
Max Murphy
80

No Go nie posiada potrójny operator, używając if / else składnia jest idiomatyczne sposób.

Dlaczego Go nie ma operatora?:?

W Go nie ma trójskładnikowej operacji testowej. Aby osiągnąć ten sam wynik, możesz użyć następujących opcji:

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

Powodem ?:jest nieobecny Go to, że projektanci w języku widział działanie zbyt często używany do tworzenia nieprzeniknione złożonych wyrażeń. if-elseForma, chociaż już jest bezsprzecznie bardziej przejrzyste. Język potrzebuje tylko jednej konstrukcji przepływu sterowania warunkowego.

- Często zadawane pytania (FAQ) - Język programowania Go

ishaaq
źródło
1
Czyli dlatego, że to, co widzieli projektanci języków, pominęli jedną linijkę dla całego if-elsebloku? A kto mówi, że if-elsenie jest wykorzystywany w podobny sposób? Nie atakuję cię, po prostu czuję, że wymówka projektantów nie jest wystarczająca
Alf Moh
58

Załóżmy, że masz następujące wyrażenie trójkowe (w C):

int a = test ? 1 : 2;

Idiomatycznym podejściem w Go byłoby po prostu użycie ifbloku:

var a int

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

Może to jednak nie spełniać Twoich wymagań. W moim przypadku potrzebowałem wyrażenia wbudowanego dla szablonu generowania kodu.

Użyłem natychmiast anonimowej funkcji:

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

Zapewnia to, że obie gałęzie również nie są oceniane.

Peter Boyer
źródło
Dobrze wiedzieć, że oceniana jest tylko jedna gałąź wbudowanej funkcji anonowej. Należy jednak pamiętać, że takie przypadki są poza zakresem operatora trójskładnikowego C.
Wolf
1
Wyrażenie warunkowe C (powszechnie znany jako operator potrójnego) ma trzy wielkości: expr1 ? expr2 : expr3. Jeśli expr1ocenia to true, expr2jest oceniane i jest wynikiem wyrażenia. W przeciwnym razie expr3jest oceniany i podawany jako wynik. Pochodzi z sekcji 2.11 ANSI C Programming Language autorstwa K&R. Rozwiązanie My Go zachowuje tę specyficzną semantykę. @Wolf Czy możesz wyjaśnić, co sugerujesz?
Peter Boyer
Nie jestem pewien, co miałem na myśli, być może funkcje anonowe zapewniają zakres (lokalną przestrzeń nazw), czego nie ma w przypadku operatora trójskładnikowego w C / C ++. Zobacz przykład użycia tego zakresu
Wolf
39

Trójskładnik mapy jest łatwy do odczytania bez nawiasów:

c := map[bool]int{true: 1, false: 0} [5 > 4]
użytkownik1212212
źródło
Nie do końca wiadomo, dlaczego ma -2 ... tak, to obejście, ale działa i jest bezpieczne dla typu.
Alessandro Santini,
30
Tak, działa, jest bezpieczny dla typu, a nawet kreatywny; istnieją jednak inne wskaźniki. Operacje trójskładnikowe są środowiskiem wykonawczym równoważnym if / else (patrz np. Ten post S / O ). Ta odpowiedź nie jest spowodowana tym, że 1) oba gałęzie są wykonane, 2) tworzy mapę 3) wywołuje skrót. Wszystkie są „szybkie”, ale nie tak szybkie jak if / else. Ponadto argumentowałbym, że nie jest bardziej czytelny niż var r T, jeśli warunek {r = foo ()} else {r = bar ()}
knight
W innych językach stosuję to podejście, gdy mam wiele zmiennych oraz z zamknięciami lub wskaźnikami funkcji lub skokami. Pisanie zagnieżdżonych ifs staje się podatne na błędy wraz ze wzrostem liczby zmiennych, podczas gdy np. {(0,0,0) => {code1}, (0,0,1) => {code2} ...} [(x> 1 , y> 1, z> 1)] (pseudokod) staje się coraz bardziej atrakcyjny wraz ze wzrostem liczby zmiennych. Zamknięcia utrzymują ten model szybko. Oczekuję, że podobne kompromisy będą obowiązywać.
Max Murphy
Podejrzewam, że użyjesz przełącznika dla tego modelu. Uwielbiam sposób, w jaki przełączniki psują się automatycznie, nawet jeśli czasami jest to niewygodne.
Max Murphy
8
jak wskazała 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)
}

To nie osiągnie lepszych wyników, jeśli / else i wymaga obsady, ale działa. FYI:

BenchmarkAbsTernary-8 100000000 18,8 ns / op

BenchmarkAbsIfElse-8 2000000000 0,27 ns / op

Phillip Dominy
źródło
To najlepsze rozwiązanie, gratulacje! Jedna linia, która obsługuje wszystkie możliwe sprawy
Alexandro de Oliveira,
2
Nie sądzę, że obsługuje to ocenę warunkową, czy tak? W przypadku gałęzi bez efektów ubocznych nie ma to znaczenia (jak w twoim przykładzie), ale jeśli jest to coś z efektami ubocznymi, napotkasz problemy.
Ashton Wiersdorf
7

Jeśli wszystkie twoje oddziały powodują skutki uboczne lub są drogie obliczeniowo, następujące byłoby refaktoryzacja semantycznie zachowująca :

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

bez narzutów (wstawiane) i, co najważniejsze, bez zaśmiecania przestrzeni nazw funkcjami pomocniczymi, które są używane tylko raz (co utrudnia czytelność i konserwację). Przykład na żywo

Uwaga, jeśli naiwnie zastosujesz podejście Gustavo :

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

dostaniesz program o innym zachowaniu ; na wypadek val <= 0, gdyby program wypisał wartość dodatnią, a nie powinien! (Analogicznie, jeśli odwrócisz gałęzie, wprowadzisz koszty ogólne, niepotrzebnie wywołując funkcję slow).

śmiały
źródło
1
Interesująca lektura, ale tak naprawdę nie rozumiem sensu twojej krytyki podejścia Gustavo. Widzę (rodzaj) absfunkcję w oryginalnym kodzie (cóż, chciałbym zmienić <=na <). W twoim przykładzie widzę inicjalizację, która w niektórych przypadkach jest zbędna i może być ekspansywna. Czy możesz wyjaśnić: wyjaśnić swój pomysł jeszcze bardziej?
Wolf
Główną różnicą jest to, że wywołanie funkcji poza którąkolwiek gałęzią spowoduje skutki uboczne, nawet jeśli ta gałąź nie powinna była zostać podjęta. W moim przypadku zostaną wydrukowane tylko liczby dodatnie, ponieważ funkcja printPositiveAndReturnjest wywoływana tylko dla liczb dodatnich. I odwrotnie, zawsze wykonanie jednej gałęzi, a następnie „ustalenie” wartości przy wykonaniu innej gałęzi nie cofnie skutków ubocznych pierwszej gałęzi .
odważny
Rozumiem, ale doświadczeni programiści są zwykle świadomi skutków ubocznych. W takim przypadku wolałbym oczywiste rozwiązanie Cassy Foesch zamiast funkcji wbudowanej, nawet jeśli skompilowany kod może być taki sam: jest krótszy i wygląda na oczywisty dla większości programistów. Nie zrozumcie mnie źle: naprawdę uwielbiam zamknięcia Go;)
Wolf
1
doświadczenia programiści są zwykle świadomi skutków ubocznych ” - Nie. Unikanie oceny terminów jest jedną z podstawowych cech operatora trójskładnikowego.
Jonathan Hartley,
6

Przedmowa: Bez kłócenia się, że if elsetak należy, nadal możemy bawić się i czerpać przyjemność z konstrukcji opartych na języku.

Poniższy Ifkonstrukt jest dostępny w mojej github.com/icza/goxbibliotece z wieloma innymi metodami, które są builtinx.Iftypem.


Go pozwala na dołączanie metod do dowolnych typów zdefiniowanych przez użytkownika , w tym typów pierwotnych, takich jak bool. Możemy stworzyć typ niestandardowy mający booljako jego typ podstawowy , a następnie przy pomocy prostej konwersji typu pod warunkiem mamy dostęp do jego metod. Metody, które odbierają i wybierają spośród argumentów.

Coś takiego:

type If bool

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

Jak możemy tego użyć?

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

Na przykład trójka robi max():

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

Trójka robi abs():

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

Wygląda to fajnie, jest proste, eleganckie i wydajne (nadaje się również do wstawiania ).

Jeden minus w porównaniu do „prawdziwego” operatora trójskładnikowego: zawsze ocenia wszystkie operandy.

Aby uzyskać odroczoną i tylko w razie potrzeby ocenę, jedyną opcją jest użycie funkcji ( deklarowanych funkcji lub metod lub literałów funkcji ), które są wywoływane tylko wtedy, gdy jest to konieczne:

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

Korzystanie z niego: Załóżmy, że mamy te funkcje do obliczania ai b:

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

Następnie:

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

Na przykład warunkiem jest bieżący rok> 2020:

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

Jeśli chcemy użyć literałów funkcyjnych:

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

Uwaga końcowa: jeśli miałbyś funkcje z różnymi podpisami, nie możesz ich tutaj użyć. W takim przypadku możesz użyć literału funkcji z pasującym podpisem, aby nadal mieć zastosowanie.

Na przykład jeśli calca()i calcb()miałby również parametry (oprócz wartości zwracanej):

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

Oto jak możesz ich użyć:

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

Wypróbuj te przykłady na Go Playground .

icza
źródło
4

Odpowiedź Eolda jest interesująca i kreatywna, a może nawet sprytna.

Jednak zamiast tego zaleca się:

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

Tak, oba kompilują się w zasadzie do tego samego zestawu, jednak ten kod jest znacznie bardziej czytelny niż wywoływanie funkcji anonimowej tylko po to, aby zwrócić wartość, która mogła zostać zapisana w zmiennej.

Zasadniczo prosty i przejrzysty kod jest lepszy niż kod kreatywny.

Ponadto dowolny kod korzystający z literału mapy nie jest dobrym pomysłem, ponieważ mapy w Go nie są wcale lekkie. Od wersji 1.3, losowa kolejność iteracji dla małych map jest gwarantowana, a aby to wymusić, stała się nieco mniej wydajna pod względem pamięci dla małych map.

W rezultacie tworzenie i usuwanie wielu małych map zajmuje zarówno dużo miejsca, jak i czasu. Miałem kawałek kodu, który korzystał z małej mapy (prawdopodobnie dwa lub trzy klucze, ale częstym przypadkiem użycia był tylko jeden wpis), ale kod był bardzo powolny. Mówimy o co najmniej 3 rzędy wielkości wolniej niż ten sam kod przepisany na klucz z podwójnym wycinkiem [indeks] => mapa danych [indeks]. I prawdopodobnie było więcej. Ponieważ niektóre operacje, które wcześniej trwały kilka minut, rozpoczęły się w milisekundach. \

Cassy Foesch
źródło
1
simple and clear code is better than creative code- to bardzo mi się podoba, ale w ostatniej części jestem trochę zdezorientowany dog slow, może to też może być mylące dla innych?
Wolf
1
Zasadniczo ... miałem kod, który tworzył małe mapy z jednym, dwoma lub trzema wpisami, ale kod działał bardzo wolno. Tak więc wiele m := map[string]interface{} { a: 42, b: "stuff" }, a następnie w innej funkcji, iterując po niej: for key, val := range m { code here } Po przejściu na system dwóch plasterków keys = []string{ "a", "b" }, data = []interface{}{ 42, "stuff" }:, a następnie iteruj jak for i, key := range keys { val := data[i] ; code here }rzeczy przyspieszyły 1000 razy.
Cassy Foesch,
Rozumiem, dziękuję za wyjaśnienie. (Być może sama odpowiedź mogłaby zostać poprawiona w tym punkcie.)
Wolf,
1
-.- ... touché, logika ... touché ... W końcu się tym zajmę ...;)
Cassy Foesch
3

One-linery, choć odrzucane przez twórców, mają swoje miejsce.

Ten rozwiązuje leniwy problem oceny, umożliwiając opcjonalnie przekazanie funkcji do oceny w razie potrzeby:

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

Wynik

hello
hello world
hello world
hello
bye
  • Przekazywane funkcje muszą zwracać wartość an, interface{}aby spełnić operację rzutowania wewnętrznego.
  • W zależności od kontekstu możesz rzucić dane wyjściowe na określony typ.
  • Jeśli chcesz zwrócić z tego funkcję, musisz ją zawinąć, jak pokazano za pomocą c.

Autonomiczne rozwiązanie tutaj jest również miłe, ale może być mniej jasne dla niektórych zastosowań.

nobar
źródło
Nawet jeśli to zdecydowanie nie jest akademickie, jest to całkiem miłe.
Fabien