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


297

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

int index = val > 0 ? val : -val

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

var index int

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

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


Вы могли бы инициализировать значение с помощью части else и только проверить, не изменилось ли ваше состояние, хотя
— вы

Многие if / thens должны были быть устранены в любом случае. Мы делали это все время со дня, когда я написал свои первые программы на Бейсике 35 лет назад. Ваш пример может быть: int index = -val + 2 * val * (val > 0);
— Hyc

9
@hyc ваш пример далеко не так удобен для чтения, как идиоматический код go или даже версия C, использующая троичный оператор. В любом случае, AFAIK, невозможно реализовать это решение в Go, поскольку логическое значение не может использоваться в качестве числового значения.
— Фабьен

Хотите знать, почему пойти не предоставил такого оператора?
— Эрик Ван

@EricWang Две причины, AFAIK: 1 - вам это не нужно, и они хотели, чтобы язык был как можно меньше. 2 - он имеет тенденцию злоупотребляться, т. Е. Использоваться в запутанных многострочных выражениях, и разработчикам языка это не нравится.
— Фабьен

Ответы:


244

Как указывалось (и, надеюсь, неудивительно), использование 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)

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


184
Эй, ребята, посмотрите! Я просто перенес оператор тернарности на Голанги! play.golang.org/p/ZgLwC_DHm0 . Так эффективно!
— thwd

28
@tomwilde ваше решение выглядит довольно интересно, но в нем отсутствует одна из главных особенностей троичного оператора - условная оценка.
— Владимир Матвеев

12
@VladimirMatveev обернуть значения в замыканиях;)
— nemo

55
c := (map[bool]int{true: a, false: a - 1})[a > b]это пример запутывания ИМХО, даже если оно работает.
— Рик-777

34
Если if/elseэто идиоматическое подход , то , возможно , мог бы рассмотреть Golang позволить if/elseпункты возврата значения: x = if a {1} else {0}. Go не будет единственным языком, который будет работать таким образом. Основным примером является Scala. См .: alvinalexander.com/scala/scala-ternary-operator-syntax
— Макс Мерфи

80

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

Почему у Go нет оператора?:?

В Go нет троичной операции тестирования. Вы можете использовать следующее для достижения того же результата:

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

Причина ?:отсутствует в Go в том, что разработчики языка видели, как эта операция использовалась слишком часто для создания непроницаемых сложных выражений. if-elseФормы, хотя больше, бесспорно яснее. Языку нужна только одна условная конструкция потока управления.

- Часто задаваемые вопросы (FAQ) - Язык программирования Go


1
Так просто потому, что то, что видели дизайнеры языка, они опустили одну строку для всего if-elseблока? И кто говорит, что if-elseне злоупотребляет подобным образом? Я не нападаю на вас, я просто чувствую, что оправдание дизайнеров недостаточно обоснованно
— Альф Мох

58

Предположим, у вас есть следующее троичное выражение (в 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 } }()

Это гарантирует, что обе ветви также не оцениваются.


Полезно знать, что оценивается только одна ветвь встроенной функции anon. Но обратите внимание, что подобные случаи выходят за рамки тернарного оператора Си.
— Вольф

1
Условное выражение С (обычно известный как тройной оператор) имеет три операнда: expr1 ? expr2 : expr3. Если имеет expr1значение true, expr2оценивается и является результатом выражения. В противном случае expr3оценивается и предоставляется как результат. Это из раздела 2.11 языка программирования ANSI C от K & R. Решение My Go сохраняет эту специфическую семантику. @ Волк Можете ли вы уточнить, что вы предлагаете?
— Питер Бойер

Я не уверен, что я имел в виду, возможно, что функции anon предоставляют область видимости (локальное пространство имен), чего нет в случае с троичным оператором в C / C ++. Смотрите пример использования этой области
— Wolf

39

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

c := map[bool]int{true: 1, false: 0} [5 > 4]

Не совсем уверен, почему он получил -2 ... да, это обходной путь, но он работает и безопасен для типов.
— Алессандро Сантини

30
Да, это работает, безопасно для типов и даже креативно; Однако есть и другие показатели. Тройные операции - это время выполнения, эквивалентное if / else (см., Например, этот пост S / O ). Этот ответ не потому, что 1) обе ветви выполняются, 2) создает карту 3) вызывает хеш. Все они «быстрые», но не такие быстрые, как if / else. Кроме того, я бы сказал, что он не более читабелен, чем var r T, если условие {r = foo ()} else {r = bar ()}
— рыцарь

В других языках я использую этот подход, когда у меня есть несколько переменных и с замыканиями или указателями на функции или переходами. Запись вложенных ifs становится подверженной ошибкам по мере увеличения числа переменных, тогда как, например, {(0,0,0) => {code1}, (0,0,1) => {code2} ...} [(x> 1) , y> 1, z> 1)] (псевдокод) становится все более привлекательным по мере увеличения числа переменных. Закрытия держат эту модель быстро. Я ожидаю, что аналогичные компромиссы применяются в го.
— Макс Мерфи

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

8
как указала Кэсси Фош: simple and clear code is better than creative code.
— Вольф

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

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

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

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


Это лучшее решение, поздравляю! Одна строка, которая обрабатывает все возможные случаи
— Александро де Оливейра

2
Я не думаю, что это обрабатывает условную оценку, или делает это? С ветвями без побочных эффектов это не имеет значения (как в вашем примере), но если это что-то с побочными эффектами, вы столкнетесь с проблемами.
— Эштон

7

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

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программа выдаст неположительное значение, а это не так! (Аналогично, если бы вы изменили направление ветвлений, вы бы добавили накладные расходы, без необходимости вызывая медленную функцию.)


1
Интересное чтиво, но я не совсем понимаю смысл вашей критики подхода Густаво. Я вижу (вид) absфункцию в исходном коде (ну, я бы изменил <=на <). В вашем примере я вижу инициализацию, которая в некоторых случаях является избыточной и может быть обширной. Не могли бы вы уточнить: объясните свою идею немного подробнее?
— Вольф

Основное отличие состоит в том, что вызов функции за пределами одной из ветвей будет иметь побочные эффекты, даже если эта ветвь не должна была выполняться. В моем случае будут напечатаны только положительные числа, потому что функция printPositiveAndReturnвызывается только для положительных чисел. И наоборот, всегда выполняя одну ветвь, тогда «исправление» значения при выполнении другой ветки не отменяет побочных эффектов первой ветви .
— Старый

Я вижу, но опытные программисты обычно знают о побочных эффектах. В этом случае я бы предпочел очевидное решение Cassy Foesch встроенной функции, даже если скомпилированный код может быть таким же: он короче и выглядит очевидным для большинства программистов. Не поймите меня неправильно: я действительно люблю замыкания Го;)
— Волк

1
« Опытные программисты обычно знают о побочных эффектах ». Нет. Избегание оценки терминов является одной из основных характеристик троичного оператора.
— Джонатан Хартли,

6

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

Следующая Ifконструкция доступна в моей github.com/icza/goxбиблиотеке с множеством других методов, являющихся builtinx.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 .


4

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

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

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

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

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

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

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


1
simple and clear code is better than creative code- это мне очень нравится, но я немного запутался в последнем разделе после dog slow, может быть, это может сбить с толку и других?
— Вольф

1
Итак, в основном ... У меня был некоторый код, который создавал маленькие карты с одной, двумя или тремя записями, но код работал очень медленно. Итак, много m := map[string]interface{} { a: 42, b: "stuff" }, а затем и другая функция, перебирающая ее: for key, val := range m { code here } после переключения на двухслойную систему:, keys = []string{ "a", "b" }, data = []interface{}{ 42, "stuff" }а затем перебираем, как for i, key := range keys { val := data[i] ; code here }вещи ускоряются в 1000 раз.
— Кэсси Фош

Понятно, спасибо за разъяснения. (Возможно, сам ответ мог бы быть улучшен в этом пункте.)
— Волк

1
-.- ... Туше, логика ... Туше ... Я пойду на это в конце концов ...;)
— Кэсси Фош

3

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

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

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
  • Переданные функции должны возвращать a interface{}для удовлетворения внутренней операции приведения.
  • В зависимости от контекста вы можете выбрать приведение вывода к определенному типу.
  • Если вы хотите вернуть функцию из этого, вам нужно будет обернуть ее, как показано с помощью c.

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


Даже если это определенно не академично, это довольно мило.
— Фабьен
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.