Что такое идиоматический эквивалент 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