Was ist das idiomatische Go-Äquivalent des ternären Operators von C?

297

In C / C ++ (und vielen Sprachen dieser Familie) verwendet ein allgemeines Idiom zum Deklarieren und Initialisieren einer Variablen in Abhängigkeit von einer Bedingung den ternären Bedingungsoperator:

int index = val > 0 ? val : -val

Go hat keinen bedingten Operator. Was ist die idiomatischste Methode, um denselben Code wie oben zu implementieren? Ich bin zu der folgenden Lösung gekommen, aber sie scheint ziemlich ausführlich zu sein

var index int

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

Gibt es etwas besseres?

Fabien
quelle
Sie könnten den Wert mit dem else-Teil initialisieren und nur prüfen, ob sich Ihr Zustand ändert. Sie sind sich jedoch nicht sicher, ob dies besser ist
x29a
Viele if / thens hätten sowieso eliminiert werden sollen. Wir haben das die ganze Zeit gemacht, seit ich vor 35 Jahren meine ersten BASIC-Programme geschrieben habe. Ihr Beispiel könnte sein: int index = -val + 2 * val * (val > 0);
Hyc
9
@hyc Ihr Beispiel ist bei weitem nicht so lesbar wie der idiomatische Code von go oder sogar die Version von C mit dem ternären Operator. Wie auch immer, AFAIK, es ist nicht möglich, diese Lösung in Go zu implementieren, da ein Boolescher Wert nicht als numerischer Wert verwendet werden kann.
Fabien
Sie fragen sich, warum go keinen solchen Betreiber zur Verfügung gestellt hat?
Eric Wang
@EricWang Zwei Gründe, AFAIK: 1- Sie brauchen es nicht und sie wollten die Sprache so klein wie möglich halten. 2- Es wird häufig missbraucht, dh in verschlungenen mehrzeiligen Ausdrücken verwendet, und die Sprachdesigner mögen es nicht.
Fabien

Antworten:

244

Wie bereits erwähnt (und hoffentlich nicht überraschend), if+elseist die Verwendung in der Tat die idiomatische Methode , um in Go Bedingungen zu erfüllen.

Zusätzlich zum vollständigen var+if+elseCodeblock wird diese Schreibweise jedoch häufig verwendet:

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

Wenn Sie einen Codeblock haben, der sich wiederholt, z. B. das Äquivalent von int value = a <= b ? a : b, können Sie eine Funktion erstellen, um ihn zu speichern:

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

...

value := min(a, b)

Der Compiler integriert solche einfachen Funktionen, sodass sie schnell, klarer und kürzer sind.

Gustavo Niemeyer
quelle
184
Hey Leute, schau! Ich habe gerade den Ternarity- Operator auf die Golangs portiert ! play.golang.org/p/ZgLwC_DHm0 . So effizient!
14.
28
@tomwilde Ihre Lösung sieht ziemlich interessant aus, aber es fehlt eine der Hauptfunktionen des ternären Operators - die bedingte Auswertung.
Vladimir Matveev
12
@VladimirMatveev wickeln die Werte in Verschlüsse;)
nemo
55
c := (map[bool]int{true: a, false: a - 1})[a > b]ist meiner Meinung nach ein Beispiel für Verschleierung, auch wenn es funktioniert.
Rick-777
34
Wenn dies if/elseder idiomatische Ansatz ist, könnte Golang möglicherweise in Betracht ziehen, if/elseKlauseln einen Wert zurückgeben zu lassen : x = if a {1} else {0}. Go wäre keineswegs die einzige Sprache, die auf diese Weise funktioniert. Ein Mainstream-Beispiel ist Scala. Siehe: alvinalexander.com/scala/scala-ternary-operator-syntax
Max Murphy
80

No Go hat keinen ternären Operator. Die Verwendung der if / else-Syntax ist die idiomatische Methode.

Warum hat Go nicht den Operator ?:?

In Go gibt es keine ternäre Testoperation. Sie können Folgendes verwenden, um das gleiche Ergebnis zu erzielen:

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

Der Grund ?:, der in Go fehlt, ist, dass die Designer der Sprache die Operation zu oft verwendet hatten, um undurchdringlich komplexe Ausdrücke zu erstellen. Die if-elseForm ist zwar länger, aber zweifellos klarer. Eine Sprache benötigt nur ein bedingtes Kontrollflusskonstrukt.

- Häufig gestellte Fragen (FAQ) - Die Programmiersprache Go

ishaaq
quelle
1
Nur weil die Sprachdesigner gesehen haben, haben sie einen Einzeiler für einen ganzen if-elseBlock weggelassen ? Und wer sagt, dass er if-elsenicht auf die gleiche Weise missbraucht wird? Ich greife dich nicht an, ich habe nur das Gefühl, dass die Entschuldigung der Designer nicht gültig genug ist
Alf Moh
58

Angenommen, Sie haben den folgenden ternären Ausdruck (in C):

int a = test ? 1 : 2;

Der idiomatische Ansatz in Go wäre, einfach einen ifBlock zu verwenden:

var a int

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

Dies entspricht jedoch möglicherweise nicht Ihren Anforderungen. In meinem Fall benötigte ich einen Inline-Ausdruck für eine Codegenerierungsvorlage.

Ich habe eine sofort ausgewertete anonyme Funktion verwendet:

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

Dies stellt sicher, dass nicht beide Zweige ebenfalls ausgewertet werden.

Peter Boyer
quelle
Gut zu wissen, dass nur ein Zweig der Inline-Anon-Funktion ausgewertet wird. Beachten Sie jedoch, dass solche Fälle den Rahmen des ternären Operators von C sprengen.
Wolf
1
Der C-bedingte Ausdruck (allgemein als ternärer Operator bekannt) hat drei Operanden : expr1 ? expr2 : expr3. Wenn expr1ausgewertet wird true, expr2wird ausgewertet und ist das Ergebnis des Ausdrucks. Andernfalls expr3wird ausgewertet und als Ergebnis bereitgestellt. Dies stammt aus dem Abschnitt 2.11 der ANSI C-Programmiersprache von K & R. Die My Go-Lösung behält diese spezifische Semantik bei. @ Wolf Kannst du klarstellen, was du vorschlägst?
Peter Boyer
Ich bin mir nicht sicher, was ich vorhatte, vielleicht bieten anon-Funktionen einen Bereich (lokaler Namespace), was beim ternären Operator in C / C ++ nicht der Fall ist. Sehen Sie ein Beispiel für die Verwendung dieses Bereichs
Wolf
39

Die ternäre Karte ist ohne Klammern leicht zu lesen:

c := map[bool]int{true: 1, false: 0} [5 > 4]
user1212212
quelle
Ich bin mir nicht ganz sicher, warum es -2 hat ... ja, es ist eine Problemumgehung, aber es funktioniert und ist typsicher.
Alessandro Santini
30
Ja, es funktioniert, ist typsicher und sogar kreativ. Es gibt jedoch andere Metriken. Ternäre Operationen entsprechen zur Laufzeit if / else (siehe z. B. diesen S / O-Beitrag ). Diese Antwort liegt nicht daran, dass 1) beide Zweige ausgeführt werden, 2) eine Karte erstellt wird 3) ein Hash aufgerufen wird. Alle diese sind "schnell", aber nicht so schnell wie ein if / else. Außerdem würde ich argumentieren, dass es nicht besser lesbar ist als var r T, wenn Bedingung {r = foo ()} else {r = bar ()}
Ritter
In anderen Sprachen verwende ich diesen Ansatz, wenn ich mehrere Variablen habe und mit Abschlüssen oder Funktionszeigern oder Sprüngen. Das Schreiben verschachtelter ifs wird mit zunehmender Anzahl von Variablen fehleranfällig, während z. B. {(0,0,0) => {code1}, (0,0,1) => {code2} ...} [(x> 1) , y> 1, z> 1)] (Pseudocode) wird mit zunehmender Anzahl von Variablen immer attraktiver. Die Verschlüsse halten dieses Modell schnell. Ich gehe davon aus, dass ähnliche Kompromisse in go gelten.
Max Murphy
Ich nehme an, Sie würden unterwegs einen Schalter für dieses Modell verwenden. Ich mag die Art und Weise, wie Go-Schalter automatisch kaputt gehen, auch wenn es gelegentlich unpraktisch ist.
Max Murphy
8
wie Cassy Foesch betonte: 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)
}

Dies wird nicht übertreffen, wenn / sonst und erfordert Besetzung, funktioniert aber. Zu Ihrer Information:

BenchmarkAbsTernary-8 100000000 18,8 ns / op

BenchmarkAbsIfElse-8 2000000000 0,27 ns / op

Phillip Dominy
quelle
Dies ist die beste Lösung, herzlichen Glückwunsch! Eine Zeile, die alle möglichen Fälle behandelt
Alexandro de Oliveira
2
Ich glaube nicht, dass dies die bedingte Bewertung behandelt, oder? Bei Nebenwirkungsfreien Zweigen spielt dies keine Rolle (wie in Ihrem Beispiel), aber wenn es sich um Nebenwirkungen handelt, treten Probleme auf.
Ashton Wiersdorf
7

Wenn alle Ihre Filialen Nebenwirkungen haben oder rechenintensiv sind, wäre das Folgende ein semantisch konservierendes Refactoring:

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

Normalerweise ohne Overhead (inline) und vor allem ohne Ihren Namespace mit Hilfsfunktionen zu überladen, die nur einmal verwendet werden (was die Lesbarkeit und Wartung beeinträchtigt). Live-Beispiel

Beachten Sie, wenn Sie Gustavos Ansatz naiv anwenden sollten :

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

Sie würden ein Programm mit einem anderen Verhalten erhalten ; falls das val <= 0Programm einen nicht positiven Wert drucken würde, während dies nicht der Fall sein sollte! (Analog dazu würden Sie, wenn Sie die Zweige umkehren, Overhead einführen, indem Sie unnötig eine langsame Funktion aufrufen.)

eold
quelle
1
Interessante Lektüre, aber ich verstehe den Punkt in Ihrer Kritik an Gustavos Ansatz nicht wirklich. Ich sehe eine (Art) absFunktion im ursprünglichen Code (na ja, würde ich ändern <=zu <). In Ihrem Beispiel sehe ich eine Initialisierung, die in einigen Fällen redundant ist und expansiv sein kann. Können Sie bitte klarstellen: Erklären Sie Ihre Idee etwas genauer?
Wolf
Der Hauptunterschied besteht darin, dass das Aufrufen einer Funktion außerhalb eines Zweigs Nebenwirkungen hat, selbst wenn dieser Zweig nicht hätte verwendet werden dürfen. In meinem Fall werden nur positive Zahlen gedruckt, da die Funktion printPositiveAndReturnnur für positive Zahlen aufgerufen wird. Umgekehrt werden die Nebenwirkungen des ersten Zweigs nicht rückgängig gemacht , wenn immer ein Zweig ausgeführt und dann der Wert durch Ausführen eines anderen Zweigs "festgelegt" wird .
Eold
Ich verstehe, aber Erfahrungen Programmierer sind sich normalerweise der Nebenwirkungen bewusst. In diesem Fall würde ich Cassy Foeschs offensichtliche Lösung einer eingebetteten Funktion vorziehen , auch wenn der kompilierte Code der gleiche sein könnte: Er ist kürzer und sieht für die meisten Programmierer offensichtlich aus. Versteh mich nicht falsch: Ich liebe Go's Schließungen wirklich ;)
Wolf
1
" Erfahrungen Programmierer sind sich normalerweise der Nebenwirkungen bewusst " - Nein. Das Vermeiden der Bewertung von Begriffen ist eines der Hauptmerkmale eines ternären Operators.
Jonathan Hartley
6

Vorwort: Ohne zu argumentieren, dass dies if elseder richtige Weg ist, können wir immer noch mit sprachfähigen Konstrukten spielen und Freude daran haben.

Das folgende IfKonstrukt ist in meiner github.com/icza/goxBibliothek mit vielen anderen Methoden verfügbar , nämlich dem builtinx.IfTyp.


Mit Go können Methoden an benutzerdefinierte Typen angehängt werden , einschließlich primitiver Typen wie z bool. Wir können einen benutzerdefinierten Typ mit booldem zugrunde liegenden Typ erstellen und dann mit einer einfachen Typkonvertierung für die Bedingung auf seine Methoden zugreifen. Methoden, die Operanden empfangen und aus diesen auswählen.

Etwas wie das:

type If bool

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

Wie können wir es benutzen?

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

Zum Beispiel ein ternäres Tun max():

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

Ein ternäres Tun abs():

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

Das sieht cool aus, ist einfach, elegant und effizient (es ist auch zum Inlining geeignet ).

Ein Nachteil im Vergleich zu einem "echten" ternären Operator: Er wertet immer alle Operanden aus.

Um eine verzögerte Auswertung zu erreichen, besteht die einzige Möglichkeit darin, Funktionen (entweder deklarierte Funktionen oder Methoden oder Funktionsliterale ) zu verwenden, die nur bei Bedarf aufgerufen werden:

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

Verwenden Sie es: Nehmen wir an, wir haben diese Funktionen zu berechnen aund b:

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

Dann:

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

Zum Beispiel ist die Bedingung aktuelles Jahr> 2020:

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

Wenn wir Funktionsliterale verwenden möchten:

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

Schlussbemerkung: Wenn Sie Funktionen mit unterschiedlichen Signaturen hätten, könnten Sie diese hier nicht verwenden. In diesem Fall können Sie ein Funktionsliteral mit übereinstimmender Signatur verwenden, um sie weiterhin anwendbar zu machen.

Zum Beispiel, wenn calca()und calcb()neben dem Rückgabewert auch Parameter vorhanden wären:

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

So könnten Sie sie verwenden:

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

Probieren Sie diese Beispiele auf dem Go Playground aus .

icza
quelle
4

Eolds Antwort ist interessant und kreativ, vielleicht sogar klug.

Es wird jedoch empfohlen, stattdessen Folgendes zu tun:

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

Ja, beide werden im Wesentlichen auf dieselbe Assembly kompiliert. Dieser Code ist jedoch viel besser lesbar als das Aufrufen einer anonymen Funktion, nur um einen Wert zurückzugeben, der überhaupt in die Variable geschrieben werden könnte.

Grundsätzlich ist einfacher und klarer Code besser als kreativer Code.

Darüber hinaus ist jeder Code, der ein Kartenliteral verwendet, keine gute Idee, da Karten in Go überhaupt nicht leichtgewichtig sind. Seit Go 1.3 ist die zufällige Iterationsreihenfolge für kleine Karten garantiert, und um dies zu erzwingen, ist die Speicherkapazität für kleine Karten erheblich geringer.

Das Erstellen und Entfernen zahlreicher kleiner Karten ist daher sowohl platz- als auch zeitaufwändig. Ich hatte einen Code, der eine kleine Karte verwendete (zwei oder drei Schlüssel sind wahrscheinlich, aber der übliche Anwendungsfall war nur ein Eintrag). Aber der Code war hundeschwach. Wir sprechen von mindestens 3 Größenordnungen langsamer als derselbe Code, der neu geschrieben wurde, um eine Dual-Slice-Key-Karte [index] => data [index] zu verwenden. Und wahrscheinlich war mehr. Einige Vorgänge, deren Ausführung zuvor einige Minuten dauerte, wurden in Millisekunden abgeschlossen. \

Cassy Foesch
quelle
1
simple and clear code is better than creative code- das gefällt mir sehr gut, aber ich werde im letzten Abschnitt etwas verwirrt dog slow, vielleicht könnte das auch für andere verwirrend sein?
Wolf
1
Also im Grunde ... Ich hatte Code, der kleine Karten mit einem, zwei oder drei Einträgen erstellte, aber der Code lief sehr langsam. Also viel m := map[string]interface{} { a: 42, b: "stuff" }und dann in einer anderen Funktion, die es durchläuft: for key, val := range m { code here } Nach dem Wechsel zu einem Zwei-Slice-System: keys = []string{ "a", "b" }, data = []interface{}{ 42, "stuff" }und dann durchlaufen, als ob die for i, key := range keys { val := data[i] ; code here }Dinge 1000-fach beschleunigt worden wären.
Cassy Foesch
Ich verstehe, danke für die Klarstellung. (Vielleicht ist die Antwort selbst könnte in diesem Punkt verbessert werden.)
Wolf
1
-.- ... touché, Logik ... touché ... darauf komme ich irgendwann zurück ...;)
Cassy Foesch
3

Einzeiler haben ihren Platz, obwohl sie von den Machern gemieden werden.

Dieser löst das Problem der verzögerten Bewertung, indem Sie optional Funktionen übergeben können, die bei Bedarf ausgewertet werden sollen:

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

Ausgabe

hello
hello world
hello world
hello
bye
  • Übergebene Funktionen müssen ein zurückgeben interface{}, um die interne Cast-Operation zu erfüllen.
  • Je nach Kontext können Sie die Ausgabe in einen bestimmten Typ umwandeln.
  • Wenn Sie eine Funktion von dieser zurückgeben möchten, müssen Sie sie wie gezeigt umbrechen c.

Die eigenständige Lösung hier ist ebenfalls nett, könnte aber für einige Anwendungen weniger klar sein.

kein Balken
quelle
Auch wenn dies definitiv nicht akademisch ist, ist dies ziemlich nett.
Fabien