C的三元运算符的惯用Go等效项是什么?


295

在C / C ++(以及该家族的许多语言)中,根据条件声明和初始化变量的常见用法使用三元条件运算符:

int index = val > 0 ? val : -val

Go没有条件运算符。实现与上述相同代码的最惯用方式是什么?我来到以下解决方案,但似乎很冗长

var index int

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

有更好的东西吗?


您可以使用else部分初始化值,并仅检查条件是否有所更改,但不确定是否更好
x29a 2013年

无论如何,应该消除很多if / thens。从35年前我编写第一个BASIC程序开始,我们就一直这样做。您的示例可能是: int index = -val + 2 * val * (val > 0);
hyc 2014年

8
@hyc您的示例远不如go的惯用代码可读,甚至不如使用三元运算符的C版本可读。无论如何,AFAIK不可能在Go中实现此解决方案,因为布尔值不能用作数值。
Fabien 2014年

想知道为什么go没有提供这样的运营商?
埃里克·王

@EricWang AFAIK有两个原因:1-您不需要它,他们想保持语言尽可能的小。2-它容易被滥用,即用于复杂的多行表达式中,而语言设计人员则不喜欢它。
法比恩'18

Answers:


242

正如指出的那样(并希望毫不奇怪),使用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高效了!
thwd

28
@tomwilde您的解决方案看起来很有趣,但是缺少三元运算符的主要功能之一-条件评估。
弗拉基米尔·马特维耶夫(Fladimir Matveev)2013年

12
@VladimirMatveev将值包装在闭包中;)
nemo

55
c := (map[bool]int{true: a, false: a - 1})[a > b]是混淆IMHO的示例,即使它可行。
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没有?:运算符?

Go中没有三元测试操作。您可以使用以下方法获得相同的结果:

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

?:Go缺席的原因是该语言的设计人员发现该操作过于频繁地用于创建难以理解的复杂表达式。的if-else形式,虽然较长,无疑是更清晰。一种语言仅需要一个条件控制流构造。

—常见问题解答(FAQ)-Go编程语言


1
因此,仅因为语言设计者所看到的,他们就为整个程序if-else块省略了一条直线?谁说if-else不是以同样的方式受到虐待?我不是在攻击您,我只是觉得设计师的借口不够有效
Alf Moh

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函数只有一个分支被求值。但是请注意,此类情况超出了C的三元运算符的范围。

1
C条件表达式(通常称为三元运算符)具有三个操作数:expr1 ? expr2 : expr3。如果expr1trueexpr2则为,是表达式的结果。否则,expr3将评估并提供结果。这来自K&R的ANSI C编程语言第2.11节。My Go解决方案保留了这些特定的语义。@Wolf您能澄清您的建议吗?
彼得·博耶

我不确定我的想法,也许anon函数提供的作用域(本地名称空间)在C / C ++中不是三元运算符。看到一个例子,使用此范围

39

映射三进制很容易阅读,没有括号:

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

不能完全确定为什么会得到-2 ...是的,这是一种解决方法,但它可以工作并且类型安全。
亚历山德罗·桑蒂尼

30
是的,它有效,类型安全,甚至具有创造力;但是,还有其他指标。三元操作在运行时等效于if / else(例如,请参见此S / O post)。此响应不是因为1)两个分支都被执行,2)创建映射3)调用哈希。所有这些都是“快速的”,但没有if / else快。另外,我认为如果条件{r = foo()} else {r = bar()}
Knight

在其他语言中,当我有多个变量并且带有闭包或函数指针或跳转时,我会使用这种方法。随着变量数量的增加,编写嵌套的ifs容易出错,而例如{{(0,0,0)=> {code1},(0,0,1)=> {code2} ...} [(x> 1 ,y> 1,z> 1)](伪代码)随着变量数量的增加而变得越来越有吸引力。闭合使该模型保持快速。我希望类似的权衡取舍。
Max Murphy

我想您会在该模型中使用一个开关。我喜欢转到开关自动断开的方式,即使偶尔会很不方便。
Max Murphy

8
正如Cassy Foesch所指出的: simple and clear code is better than creative code.
Wolf

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

如果/其他情况下,它不会表现出色,并且需要强制转换但可以。仅供参考:

基准Abs三进制8 100000000 18.8 ns / op

基准AbsIfElse-8 2000000000 0.27 ns / op


恭喜,这是最好的解决方案!一号线是处理所有可能的情况
亚历山德罗奥利维拉

2
我不认为这可以处理条件评估,还是可以?带有自由副作用的分支无关紧要(就像您的示例中一样),但是如果它具有副作用,您将遇到问题。
阿什顿·维尔斯多夫

7

如果您的所有分支都产生副作用或在计算上昂贵,则以下内容将在语义上进行重构:

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

通常没有开销(内联),最重要的是,不会使用仅使用一次的辅助函数来使名称空间混乱(这会影响可读性和维护性)。现场例子

请注意,如果您要天真地应用Gustavo的方法

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

你会得到一个行为不同的程序; 万一val <= 0程序会打印一个非正值而它不应该打印!(类似地,如果您反向分支,则会通过不必要地调用慢速函数来引入开销。)


1
有趣的读物,但我对您对古斯塔沃(Gustavo)方法的批评并不真正理解。我abs在原始代码中看到了(一种)函数(好吧,我将更<=改为<)。在您的示例中,我看到一个初始化,这在某些情况下是多余的,并且可能是扩展的。您能否澄清一下:多解释一下您的想法?

主要区别在于,即使不应该采用分支,在任一分支之外调用函数也会产生副作用。在我的情况下,仅会打印正数,因为该函数仅被调用为正数。相反,始终执行一个分支,然后通过执行不同的分支来“固定”值并不能消除第一个分支的副作用printPositiveAndReturn
eold 2016年

我知道了,但是经验丰富的程序员通常都知道副作用。在那种情况下,即使编译的代码可能相同,我还是希望Cassy Foesch为嵌入式函数提供明显的解决方案:它更短并且对于大多数程序员而言都是显而易见的。不要误会我的意思:我真的很喜欢 Go的闭包;)
Wolf

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

使用它:假设我们具有这些函数来计算ab

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 Playground上尝试这些示例。


4

eold的答案很有趣而且很有创意,甚至可能很聪明。

但是,建议改为:

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

是的,它们都可以编译为基本相同的程序集,但是,此代码比调用匿名函数只是为了返回最初可能已写入变量的值更清晰。

基本上,简单明了的代码要比创意代码好。

另外,使用地图文字的任何代码都不是一个好主意,因为在Go中地图根本不是轻量级的。从Go 1.3开始,可以确保小地图的随机迭代顺序,并且要强制执行此顺序,对于小地图,它在内存方面的效率要低得多。

结果,制作和删除大量小地图既费时又费时。我有一段使用小地图的代码(可能有两个或三个键,但是常见的用例只是一个条目),但是代码速度很慢。我们谈论的速度至少比使用双切片key [index] => data [index]映射重写的同一代码慢3个数量级。而且可能更多。由于某些操作以前需要花费几分钟才能运行,因此开始在毫秒内完成。\


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倍一样。
卡西·福斯

我明白了,谢谢您的澄清。(也许在这一点上答案本身可能会有所改善。)
沃尔夫(Wolf

1
-.- ...touché,逻辑...touché...我最终会继续...;)
Cassy Foesch 2016年

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
  • 传入的函数必须返回一个,interface{}以满足内部转换操作。
  • 根据上下文,您可以选择将输出转换为特定类型。
  • 如果要从中返回一个函数,则需要用包裹它c

这里的独立解决方案也不错,但是对于某些用途可能不太清楚。


即使这绝对不是学术性的,这也很好。
法比恩
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.