Что такое идиоматический эквивалент Go тернарного оператора C?

В C/C++ (и многих языках этого семейства) общая идиома для объявления и инициализации переменной в зависимости от условия использует троичный условный оператор:

int index = val > 0 ? val : -val

Go не имеет условного оператора. Какой самый идиоматичный способ реализовать тот же кусок кода, что и выше? Я пришел к следующему решению, но оно кажется довольно многословным

var index int

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

Есть ли что-то лучше?

16 ответов

Решение

Как указывалось (и, надеюсь, неудивительно), используя if+else действительно идиоматический способ делать условные выражения в Go.

В дополнение к полноценному var+if+else Блок кода, однако, это правописание также часто используется:

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

и если у вас есть блок кода, который является достаточно повторяющимся, например, эквивалент int value = a <= b ? a : bВы можете создать функцию для ее хранения:

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

...

value := min(a, b)

Компилятор встроит такие простые функции, поэтому он будет быстрее, понятнее и короче.

No Go не имеет троичного оператора, использование синтаксиса if/else является идиоматическим способом: http://golang.org/doc/faq

Предположим, у вас есть следующее троичное выражение (в C):

int a = test ? 1 : 2;

Идиоматический подход в Go будет просто использовать if блок:

var a int

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

Однако это может не соответствовать вашим требованиям. В моем случае мне нужно было встроенное выражение для шаблона генерации кода.

Я использовал немедленно оцененную анонимную функцию:

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

Это гарантирует, что обе ветви также не будут оценены.

Предисловие: Не утверждая, чтоif else это путь, мы все еще можем играть и получать удовольствие от языковых конструкций.

Продолжение If конструкция доступна в моем github.com/icza/gox библиотека с множеством других методов, будучи gox.If тип.


Go позволяет присоединять методы к любым пользовательским типам, включая примитивные типы, такие какbool. Мы можем создать собственный тип, имеяboolкак его базовый тип, а затем с помощью простого преобразования типа в условии, у нас есть доступ к его методам. Методы, которые получают и выбирают операнды.

Что-то вроде этого:

type If bool

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

Как мы можем это использовать?

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

Например, тройное выполнение max():

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

Тройное делание abs():

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

Выглядит круто, просто, элегантно и эффективно (также возможно встраивание).

Один недостаток по сравнению с "настоящим" тернарным оператором: он всегда оценивает все операнды.

Чтобы добиться отложенной оценки и оценки только в случае необходимости, единственный вариант - использовать функции ( объявленные функции или методы или функциональные литералы), которые вызываются только при необходимости:

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

Использование: Предположим, у нас есть эти функции для вычисления a а также b:

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

Затем:

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

Например, условие: текущий год> 2020:

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

Если мы хотим использовать функциональные литералы:

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

Последнее замечание: если у вас будут функции с разными сигнатурами, вы не сможете их здесь использовать. В этом случае вы можете использовать функциональный литерал с соответствующей подписью, чтобы сделать их применимыми.

Например, если calca() а также calcb() тоже будет иметь параметры (помимо возвращаемого значения):

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

Вот как вы можете их использовать:

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

Попробуйте эти примеры на игровой площадке Go.

Карта троичного легко читается без скобок:

c := map[bool]int{true: 1, false: 0} [5 > 4]
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)
}

Это не будет превосходить, если / иначе и требует приведения, но работает. FYI:

BenchmarkAbsTernary-8 100000000 18,8 нс / оп

BenchmarkAbsIfElse-8 2000000000 0,27 нс / оп

Как отмечали другие, у golang нет тернарного оператора или какого-либо эквивалента. Это осознанное решение, предполагающее удобочитаемость.

Это недавно привело меня к сценарию, в котором очень эффективно конструирование битовой маски стало трудно читать при идиоматическом написании, потому что он занимал много строк на экране, что было очень неэффективно при инкапсулировании как функции или и того, и другого, поскольку код производит ветви:

package lib

func maskIfTrue(mask uint64, predicate bool) uint64 {
  if predicate {
    return mask
  }
  return 0
}

производство:

        text    "".maskIfTrue(SB), NOSPLIT|ABIInternal, $0-24
        funcdata        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        funcdata        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        movblzx "".predicate+16(SP), AX
        testb   AL, AL
        jeq     maskIfTrue_pc20
        movq    "".mask+8(SP), AX
        movq    AX, "".~r2+24(SP)
        ret
maskIfTrue_pc20:
        movq    $0, "".~r2+24(SP)
        ret

Из этого я понял, что нужно использовать немного больше Go; используя именованный результат в функции (result int)сохраняет мне строку, объявляющую это в функции (и вы можете сделать то же самое с захватами), но компилятор также распознает эту идиому (присваивает только значение IF) и заменяет ее - если возможно - условной инструкцией.

func zeroOrOne(predicate bool) (result int) {
  if predicate {
    result = 1
  }
  return
}

дающий результат без ветвей:

    movblzx "".predicate+8(SP), AX
    movq    AX, "".result+16(SP)
    ret

которые затем свободно встраиваются.

package lib

func zeroOrOne(predicate bool) (result int) {
  if predicate {
    result = 1
  }
  return
}

type Vendor1 struct {
    Property1 int
    Property2 float32
    Property3 bool
}

// Vendor2 bit positions.
const (
    Property1Bit = 2
    Property2Bit = 3
    Property3Bit = 5
)

func Convert1To2(v1 Vendor1) (result int) {
    result |= zeroOrOne(v1.Property1 == 1) << Property1Bit
    result |= zeroOrOne(v1.Property2 < 0.0) << Property2Bit
    result |= zeroOrOne(v1.Property3) << Property3Bit
    return
}

производит https://go.godbolt.org/z/eKbK17

    movq    "".v1+8(SP), AX
    cmpq    AX, $1
    seteq   AL
    xorps   X0, X0
    movss   "".v1+16(SP), X1
    ucomiss X1, X0
    sethi   CL
    movblzx AL, AX
    shlq    $2, AX
    movblzx CL, CX
    shlq    $3, CX
    orq     CX, AX
    movblzx "".v1+20(SP), CX
    shlq    $5, CX
    orq     AX, CX
    movq    CX, "".result+24(SP)
    ret

Если все ваши ветки имеют побочные эффекты или требуют больших вычислительных ресурсов, то семантически сохраняющий рефакторинг приведёт к следующему:

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

обычно без накладных расходов (встроенные) и, что наиболее важно, без загромождения пространства имен вспомогательными функциями, которые используются только один раз (что затрудняет удобочитаемость и обслуживание). Живой пример

Обратите внимание, если вы должны были наивно применять подход Густаво:

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

вы получите программу с другим поведением; в случае val <= 0 Программа выдаст неположительное значение, а не должна! (Аналогично, если вы изменили направление ветвления, вы бы добавили накладные расходы, без необходимости вызывая медленную функцию.)

Остроты, которых создатели избегают, имеют свое место.

Это решает проблему ленивого вычисления, позволяя вам, необязательно, передавать функции для оценки при необходимости:

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

Выход

hello
hello world
hello world
hello
bye
  • Переданные функции должны возвращать interface{} для выполнения операции внутреннего приведения.
  • В зависимости от контекста вы можете выбрать приведение вывода к определенному типу.
  • Если вы хотите вернуть функцию из этого, вам нужно будет обернуть ее, как показано, с c.

Автономное решение здесь тоже неплохо, но для некоторых целей может быть менее понятным.

Теперь, с выпуском дженериков go1.18, очень легко сделать это с помощью такой универсальной функции, и ее можно повторно использовать во всем приложении.

      package main

import (
    "fmt"
)

func Ternary[T any](condition bool, If, Else T) T {
    if condition {
        return If
    }
    return Else
}

func main() {
    fmt.Println(Ternary(1 < 2, "yes", "no")) // yes
    fmt.Println(Ternary(1 < 2, 1, 0)) // 1
    fmt.Println(Ternary[bool](1 < 2, true, false)) // true
}


имейте в виду, если вы используете его в этом случае, он выйдет из строя. в этом случае просто используйте оператор if (потому что вы передаете в функцию указатель nil VS оператор if не вызывает этот раздел, если он ложен)

      
var a *string
fmt.Println(Ternary(a != nil, *a, "some thing else"))

решение вызывает его с помощью функции, поэтому оно не будет выполнено, если оно ложно

      func TernaryPointer[T any](condition bool, If, Else func() T) T {
    if condition {
        return If()
    }
    return Else()
}
      var pString *string
fmt.Println(TernaryPointer(
    pString != nil, // condition 
    func() string { return *pString }, // true
    func() string { return "new data" }, // false
))

но в этом случае я думаю, что обычный оператор if чище (за исключением того, что в будущем go добавит стрелочные функции

)

отдать должное этому ответу, он уже ответил на него

Ответ Эольда интересный и креативный, возможно, даже умный.

Тем не менее, рекомендуется вместо этого сделать:

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

Да, они оба компилируются по существу в одну и ту же сборку, однако этот код гораздо более разборчив, чем вызов анонимной функции только для того, чтобы вернуть значение, которое могло быть записано в переменную в первую очередь.

По сути, простой и понятный код лучше креативного кода.

Кроме того, любой код, использующий литерал карты, не является хорошей идеей, потому что карты на Go совсем не легки. Начиная с Go 1.3, случайный порядок итераций для маленьких карт гарантирован, и для обеспечения этого он стал немного менее эффективным с точки зрения памяти для маленьких карт.

В результате создание и удаление многочисленных небольших карт занимает много места и времени. У меня был фрагмент кода, в котором использовалась небольшая карта (скорее всего, два или три ключа, но в общем случае использовалась только одна запись) Но код был слишком медленным. Мы говорим как минимум на 3 порядка медленнее, чем тот же код, переписанный для использования карты двойного ключа [index]=>data[index]. И скорее всего было больше. Поскольку некоторые операции, которые раньше выполнялись за пару минут, начали выполняться за миллисекунды.\

Теперь, когда есть дженерики, можно реализовать эту глупую функцию...

      func ternary[RV any](cmp bool, rvt RV) RV {
    if cmp {
        return rvt
    }
    var rvf RV
    return rvf
}

...который затем можно использовать таким образом...

          for i, data := range trials {
        ps.Pages = append(ps.Pages, PDFPage{
            Content:   data,
            ClassName: ternary(i > 0, "pagebreak"),
        })
    }

Возможно, неправильное использование возможностей дженериков D:

Я собрал некоторые пункты и сравнил скорость.

      /*
go test ternary_op_test.go -v -bench="^BenchmarkTernaryOperator" -run=none -benchmem
*/
package _test

import (
    "testing"
)

func BenchmarkTernaryOperatorIfElse(b *testing.B) {
    for i := 0; i < b.N; i++ {
        if i%2 == 0 {
            _ = i
        } else {
            _ = -i
        }
    }
}

// https://stackoverflow.com/a/45886594/9935654
func Ternary(statement bool, a, b interface{}) interface{} {
    if statement {
        return a
    }
    return b
}

func BenchmarkTernaryOperatorTernaryFunc(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = Ternary(i%2 == 0, i, -i).(int)
    }
}

// https://stackoverflow.com/a/34636594/9935654
func BenchmarkTernaryOperatorWithFunc(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = func() int {
            if i%2 == 0 {
                return i
            } else {
                return -i
            }
        }
    }
}

// https://stackoverflow.com/a/31483763/9935654
func BenchmarkTernaryOperatorMap(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = map[bool]int{true: i, false: -i}[i%2 == 0]
    }
}

выход

      goos: windows
goarch: amd64
cpu: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz
BenchmarkTernaryOperatorIfElse
BenchmarkTernaryOperatorIfElse-8                1000000000               0.4460 ns/op          0 B/op          0 allocs/op
BenchmarkTernaryOperatorTernaryFunc
BenchmarkTernaryOperatorTernaryFunc-8           1000000000               0.3602 ns/op          0 B/op          0 allocs/op
BenchmarkTernaryOperatorWithFunc
BenchmarkTernaryOperatorWithFunc-8              659517496                1.642 ns/op           0 B/op          0 allocs/op
BenchmarkTernaryOperatorMap
BenchmarkTernaryOperatorMap-8                   13429532                82.48 ns/op            0 B/op          0 allocs/op
PASS
ok      command-line-arguments  4.365s

Я играл с решением, которое не использует функцию трех аргументов. Не поймите меня неправильно, решение с тремя аргументами отлично работает, но лично мне нравится называть вещи явно.

Что бы я хотел, так это явный интерфейс, подобный этому:

      When(<condition>).Then(<true value>).Else(<false value>)

Я реализовал это так:

      type Else[T any] interface {
    ElseDo(fn func() T) T
    Else(value T) T
}

type Then[T any] interface {
    ThenDo(fn func() T) Else[T]
    Then(value T) Else[T]
}

type Condition[T any] struct {
    condition bool
    thenValue T
    thenFn    func() T
}

func When[T any](condition bool) Then[T] {
    return &Condition[T]{condition: condition}
}

func (c *Condition[T]) ThenDo(fn func() T) Else[T] {
    c.thenFn = fn
    return c
}

func (c *Condition[T]) Then(value T) Else[T] {
    c.thenValue = value
    return c
}

func (c *Condition[T]) ElseDo(fn func() T) T {
    if c.condition {
        return c.then()
    }

    return fn()
}

func (c *Condition[T]) Else(value T) T {
    if c.condition {
        return c.then()
    }

    return value
}

func (c *Condition[T]) then() T {
    if c.thenFn != nil {
        return c.thenFn()
    }
    return c.thenValue
}

Использование:

      When[int](something == "expectedValue").Then(0).Else(1)

When[int](value > 0).Then(value).Else(1)

When[int](value > 0).ThenDo(func()int {return value * 4}).Else(1)

When[string](boolean == true).Then("it is true").Else("it is false")

К сожалению, я не нашел способа избавиться от явного типа при вызовеWhenфункция. Тип не выводится автоматически возвращаемыми типами Then/Else ‍♂️

Еще одно предложение для идиоматического подхода к тернарному оператору в Go:

      package main

import (
    "fmt"
)

func main() {
    val := -5

    index := func (test bool, n, d int) int {
        if test {
            return n
        }
        return d
    }(val > 0, val, -val)
    
    fmt.Println(index)
}

Игровая площадка

/*
* type ListNode struct {
*     Val int
*     Next *ListNode
* }
*/

val := (l1 ? l1->val : 0) + (l2 ? l2->val : 0) + carry
Другие вопросы по тегам