Який ідіоматичний Go еквівалент потрійного оператора С?


297

У C / C ++ (і багатьох мовах цього сімейства) загальна ідіома для оголошення та ініціалізації змінної залежно від умови використовує потрійний умовний оператор:

int index = val > 0 ? val : -val

Go не має умовного оператора. Який самий ідіоматичний спосіб реалізувати той самий фрагмент коду, що і вище? Я прийшов до наступного рішення, але це здається досить багатослівним

var index int

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

Чи є щось краще?


ви можете ініціалізувати значення з іншою частиною і лише перевірити, чи зміниться ваш стан, не впевнений, що все-таки краще
x29a

Дуже багато, якщо / тенів було б ліквідовано. Ми звикли це робити весь час від тих днів, коли я писав свої перші програми BASIC 35 років тому. Вашим прикладом може бути: int index = -val + 2 * val * (val > 0);
hyc

9
@hyc ваш приклад далеко не такий читабельний, як ідіоматичний код go або навіть як версія C, що використовує потрійний оператор. У будь-якому випадку, AFAIK, неможливо реалізувати це рішення в Go, оскільки булеве значення не може використовуватися як числове значення.
Фаб’єн

Цікаво, чому компанія «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)

Компілятор вбудує такі прості функції, тому він швидший, зрозуміліший і коротший.


183
Гей, хлопці, подивіться! Я просто портував ternarity оператора до golangs! play.golang.org/p/ZgLwC_DHm0 . Так ефективно!
четвер

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

12
@VladimirMatveev загорнув цінність у закриття;)
nemo

55
c := (map[bool]int{true: a, false: a - 1})[a > b]є прикладом затухання ІМХО, навіть якщо воно працює.
Rick-777

34
Якщо if/elseце ідіоматичне підхід , то , можливо , міг би розглянути Golang дозволити if/elseпункти повернення значення: x = if a {1} else {0}. Go - це аж ніяк не єдина мова, яка працює таким чином. Основний приклад - Scala. Дивіться: alvinalexander.com/scala/scala-ternary-operator-syntax
Max Murphy

80

No Go не має потрійного оператора, використовуючи синтаксис if / else - ідіоматичний спосіб.

Чому Go не має оператора ?:

У Гоу не проводиться потрійне тестування. Ви можете використовувати наступне, щоб досягти того ж результату:

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

Причина ?: Go відсутня в тому, що дизайнери мови бачили, що операція використовується занадто часто для створення непрохідно складних виразів. if-elseФорми, хоча більше, безперечно ясніше. Мові потрібна лише одна конструкція потоку умовного керування.

- Часті питання (FAQ) - Мова програмування Go


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

58

Припустимо, у вас є такий потрійний вираз (в С):

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

Це гарантує, що обидві галузі також не оцінюються.


Добре знати, що оцінюється лише одна гілка вбудованої функції анона. Але зауважте, що подібні випадки виходять за рамки потрійного оператора C.
Вовк

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

Я не впевнений, що я мав на увазі, можливо, що функції anon забезпечують область (локальний простір імен), що не стосується потрійного оператора в C / C ++. Дивіться приклад використання цієї сфери
Вовк

39

Потрійну карту легко читати без дужок:

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

Не зовсім впевнений, чому він отримав -2 ... так, це спосіб вирішення, але він працює і є безпечним для типу.
Алессандро Сантіні

30
Так, це працює, є безпечним для типу та навіть творчим; проте є й інші показники. Ternary ops - це час, еквівалентний if / else (див., Наприклад, цю посаду S / O ). Ця відповідь не тому, що 1) обидві гілки виконуються, 2) створює карту 3) викликає хеш. Все це є "швидким", але не таким швидким, як "if". Також я б заперечив, що це не читабельніше, ніж var r T, якщо умова {r = foo ()} else {r = bar ()}
лицар

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

Я вважаю, що ви хочете використовувати для цієї моделі перемикач. Мені подобається, що перемикачі перемикаються автоматично, навіть якщо це час від часу незручно.
Макс Мерфі

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

Це не буде перевершувати, якщо / else і вимагає акторського складу, але працює. 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викликається лише для додатних чисел. І навпаки, завжди виконуючи одну гілку, тоді "виправлення" значення за допомогою виконання іншої гілки не скасовує побічні ефекти першої гілки .
eold

Я бачу, але досвід програмістів зазвичай знає про побічні ефекти. У такому випадку я вважаю за краще очевидне рішення Cassy Foesch щодо вбудованої функції, навіть якщо компільований код може бути однаковим: він коротший і виглядає очевидним для більшості програмістів. Не зрозумійте мене неправильно: я дуже люблю закриття Go;)
Вовк

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

Відповідь eold цікава та творча, можливо, навіть розумна.

Однак рекомендується замість цього зробити:

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

Так, вони обоє складаються до однієї і тієї ж збірки, однак цей код набагато розбірливіший, ніж викликати анонімну функцію просто для повернення значення, яке могло б бути записане в змінну в першу чергу.

В основному, простий і зрозумілий код краще, ніж творчий код.

Окрім того, будь-який код, що використовує літеральну карту, не є гарною ідеєю, оскільки в Go Go карти зовсім не легкі. Починаючи з Go 1.3, порядок ітерації для випадкових малих карт гарантується, і для забезпечення цього він стає досить менш ефективним для пам'яті для малих карт.

Як результат, виготовлення та вилучення численних малих карт є трудомістким та трудомістким. У мене був фрагмент коду, який використовував невелику карту (два-три ключі, ймовірно, але звичайний випадок використання був лише одним записом) Але код був собачим повільним. Ми говоримо щонайменше на 3 порядки повільніше, ніж той самий код, переписаний для використання подвійного ключа зрізу [index] => data [index] map. І, ймовірно, було більше. Оскільки деякі операції, які раніше займали пару хвилин, почали завершуватися через мілісекунди. \


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 разів.
Cassy Foesch

Бачу, дякую за роз’яснення. (Можливо, саме цю відповідь можна було б покращити в цьому пункті.)
Вовк

1
-.- ... touché, логіка ... touché ... Я з цим вийду зрештою ...;)
Cassy Foesch

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.