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

eold
источник
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 .

icza
источник
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]. И скорее всего было больше. Поскольку некоторые операции, которые раньше выполнялись за пару минут, начали выполняться за миллисекунды. \

Cassy Foesch
источник
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.

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

nobar
источник
Даже если это определенно не академично, это довольно мило.
Фабьен