Go语言中类似于C语言三目运算符的惯用写法是什么?

564

在C/C++(以及许多类似语言)中,常用的一种声明和初始化变量的习惯用法是使用三元条件运算符:

int index = val > 0 ? val : -val

Go语言没有三目运算符。实现与上述代码相同的功能最惯用的方式是什么?我想到了下面的解决方案,但它似乎过于冗长。

var index int

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

有更好的选择吗?


16
@hyc,你的示例代码远不如Go的惯用代码或者使用三目运算符的C版本易读。无论如何,据我所知,在Go中无法将布尔值用作数值。因此,这种解决方案无法在Go中实现。 - Fabien
3
想知道为什么Go语言没有提供这样的运算符? - Eric
3
@Fabien的回答中除了最后几个词语外,所有内容都存在缺陷逻辑。如果你不需要三目运算符,则不需要switch,然而他们包括了它,因此显然这不是一个同样被考虑过的答案。它往往比复杂的if语句条件少被滥用,所以它不应该是那样的。设计师们可能不喜欢它--这听起来更有可能。一些开发人员格式化代码不当或使用括号不应该使有用的语言特性失去资格,特别是当需要"gofmt"时,它可以做这项工作。 - user1775138
2
可能Go语言将来会添加三元运算符 - Eric
1
如果我没记错的话,从阅读 Github 的问题中可以得知,三元运算符没有被包含在 Go 语言中,因为它不能(或者说太混乱了)被 Go 的单通编译器解析。 - Ronald Currier
显示剩余3条评论
15个回答

459

正如所指出的(也许并不令人惊讶),在Go中使用if+else确实是条件语句的惯用方式,详情请参阅官方文档

除了完整的var+if+else代码块外,这种简写形式也经常被使用:

index := val
if val <= 0 {
    index = -val
}

如果您有一段重复的代码块,例如等效于int value = a <= b ? a : b的代码块,则可以创建一个函数来保存它:

并且如果你有足够重复的代码块,如等同于int value = a <= b ? a : b,你可以创建一个函数持有它:

func min(a, b int) int {
    if a <= b {
        return a
    }
    return b
}

...

value := min(a, b)
编译器将内联这种简单函数,因此它更快、更清晰、更短。

282
大家看看!我刚刚将_三元_运算符移植到了golang中!http://play.golang.org/p/ZgLwC_DHm0.非常高效! - thwd
45
@tomwilde的解决方案看起来相当有趣,但它缺少了三元运算符的主要特征之一——条件评估。 - Vladimir Matveev
18
@VladimirMatveev 将数值封装在闭包中 ;) - nemo
95
我认为 c := (map[bool]int{true: a, false: a - 1})[a > b] 是一个混淆代码的例子,尽管它能运行。 - Rick-777
67
如果if/else是惯用的方法,那么Golang或许可以考虑让if/else语句返回一个值:x = if a {1} else {0}。这种做法并非Golang独有,Scala等主流编程语言也支持类似写法。参考链接:http://alvinalexander.com/scala/scala-ternary-operator-syntax - Max Murphy
显示剩余10条评论

169

Go语言没有三元运算符,使用if/else语法是惯用方法。

为什么Go语言没有 ?: 运算符?

在Go语言中没有三元测试操作。您可以使用以下语法来实现相同的结果:

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

Go语言中不包括?:的原因是,该语言的设计者已经看到过太多使用此操作符创建难以理解的复杂表达式的情况。虽然if-else形式更长,但无疑更清晰。一种语言只需要一种条件控制流结构。


173
仅仅因为语言设计者看到了某些东西,他们就省略了整个“if-else”块的一行代码?谁说“if-else”不能被滥用呢?我不是在攻击你,只是觉得设计者的借口不够充分。 - Alf Moh
48
我同意。难看的三目运算符是一种编码问题,而不是语言问题。三目运算符在许多语言中都很常见,因此它们是正常的,没有它们会让人感到惊讶,这违反了“原则最少惊讶”(POLA/PLA)。 - jcollum
4
但是从语言设计者的角度来看,他们需要使用额外的语法扩展语言规范、解析器、编译器等,以获得一些潜在的可读性问题的语法糖。Go 旨在提高代码的可读性,虽然大多数 C 开发人员可能已经足够熟悉三元运算符,能够快速阅读它们,但这并不是普遍真理,当人们开始嵌套它们时,情况会变得非常糟糕。"其他语言有它" 不是添加语言特性的有效论据。 - cthulhu
11
语言设计者的解释看起来很奇怪,因为它与另一个语言特性相矛盾:如果包含由分号分隔的2个语句(请参见https://tour.golang.org/flowcontrol/6)。我怀疑第二个语句是否会使代码更清晰。他们本可以实现三目运算符,并限制每个语句中只能有一个“?”。 - Yury Kozlov
9
“一种语言只需要一个条件控制流结构。”- 当然,但为什么不让if成为表达式,并允许像Rust或Scala那样使用诸如if cond { value1 } else { value3 }这样的结构呢? - user14611663
显示剩余7条评论

89
假设你有以下的三元表达式(在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 } return 2 }()

这样可以确保两个分支都不会被评估。

很高兴知道只有内联匿名函数的一个分支被评估。但请注意,像这样的情况超出了C三元运算符的范围。 - Wolf
4
C语言中的条件表达式(通常称为三元运算符)有三个操作数:expr1 ? expr2 : expr3。如果expr1的评估结果为true,则评估expr2并将其作为表达式的结果。否则,评估expr3并将其作为结果提供。这段话出自K&R的《ANSI C编程语言》第2.11节。我的Go解决方案保留了这些特定的语义。 - Peter Boyer
我不确定自己当时想的是什么,也许是匿名函数提供了一个作用域(本地命名空间),而在C/C++中三元运算符则不是这种情况。可以查看一个使用此作用域的示例 - Wolf
3
在这种情况下,“简单”更像是“复杂化”的。 - micahhoover
8
为什么要加上“else”?a := func() int { if test { return 1 } return 2 }() 应该可以工作,还是我错了? - Olivier Pons
我删除了else块。谢谢。我还删除了“simply”,因为它听起来有些学究气。 - undefined

68

前言:虽然我们不能否认if else这种方式的优越性,但我们仍然可以尝试使用语言特性来实现代码构造,从而找到乐趣。

Go 1.18 泛型更新:Go 1.18 加入了泛型支持,现在可以像这样创建一个通用的 If() 函数。请注意:此功能可在github.com/icza/gog中使用,方法名为gog.If()(声明:作者本人)。

func If[T any](cond bool, vtrue, vfalse T) T {
    if cond {
        return vtrue
    }
    return vfalse
}

您可以像这样使用:

min := If(i > 0, i, 0)

以下是1.18版本之前的答案:


在我的github.com/icza/gox库中有许多其他方法,其中包括以下If结构: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()
}

使用方法:假设我们有以下函数来计算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上尝试这些示例。


2
请注意,通用的 If 不能与 nil 检查和解引用一起使用,例如 If(p != nil, *p, defaultValue),因为参数(包括 *p)会立即被评估。 - blackgreen
@blackgreen 是的,对于除了传递延迟评估函数之外的所有解决方案都是正确的。 - icza

66

三元运算符不需要括号即可轻松阅读:

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

2
不太确定为什么它得到了-2...是的,这是一个解决方法,但它有效并且类型安全。 - Alessandro Santini
52
是的,它可以工作,是类型安全的,甚至具有创意;但是,还有其他指标。三元操作符在运行时等同于if / else(参见例如[this S / O post](https://dev59.com/83A65IYBdhLWcg3w6DJO))。这种回答不是因为1)两个分支都被执行,2)创建一个映射3)调用哈希。所有这些都很“快”,但不如if / else快。此外,我认为这并不比以下代码更易读 var r T if condition { r = foo() } else { r = bar() } - knight
在其他语言中,当我有多个变量和闭包、函数指针或跳转时,我使用这种方法。随着变量数量的增加,编写嵌套if语句容易出错,而例如{(0,0,0) => {code1}, (0,0,1) => {code2} ...}[(x>1,y>1,z>1)](伪代码)随着变量数量的增加变得越来越吸引人。闭包使该模型快速。我希望类似的权衡也适用于Go。 - Max Murphy
15
正如Cassy Foesch指出的:简单明了的代码比创意代码更好。 - Wolf
3
你能不能用简单的方式重写这段代码:fmt.Println("Operation %s; reverting to normal form.", (map[bool]string{true: "skipped", false: "failed"})[opkip])?可以,以下是较为通俗易懂的重写版本:var status string if opkip { status = "skipped" } else { status = "failed" } fmt.Printf("Operation %s; reverting to normal form.", status) - kavadias
显示剩余6条评论

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

这种方式不如if/else语句高效,并且需要进行类型转换,但是可以正常工作。FYI:

BenchmarkAbsTernary-8 100000000 18.8 ns/op

BenchmarkAbsIfElse-8 2000000000 0.27 ns/op


4
我认为这个处理方式不能进行条件评估,对吗?如果分支没有副作用,那就无所谓了(就像你的例子一样),但如果有副作用,你会遇到问题。 - Ashton Wiersdorf
是的,实际上根据阿什顿所说,它确实不提供条件评估。因此在其他情况下,可以只写 test = function1(); if condition {test = function2()},这将是相同的,并且不需要类型断言(更快)。至于涉及返回的答案,我不知道。还要看两个评估中是否都很昂贵。还是感谢您的回答!尽管如此,这似乎是一个好主意。 - Edw590

8

正如其他人所提到的,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) 中使用命名返回值可以节省一行在函数内部声明(你也可以对捕获变量采用相同的方式),编译器还可以识别这种惯用法(只有当条件满足时才赋值),并在可能的情况下用条件语句替换它。
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

干得好!这是一个学习和使用这个成语的动力。 - Spike0xff

6
如果您的所有分支都具有副作用或计算成本较高,则以下是一种保留语义的重构方案:
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函数(好吧,我会将<=改为<)。在你的例子中,我看到了一个初始化,在某些情况下是多余的,并且可能很昂贵。您能否请澄清一下:更详细地解释您的想法? - Wolf
主要的区别在于,如果在任一分支之外调用函数,则会产生副作用,即使该分支不应该被执行。在我的情况下,只有正数才会被打印,因为函数printPositiveAndReturn仅对正数进行调用。相反,始终执行一个分支,然后通过执行另一个分支来“修复”值,并不能撤消第一个分支的副作用 - eold
我明白了,但是有经验的程序员通常会意识到副作用。在这种情况下,即使编译后的代码可能相同,我更喜欢Cassy Foesch的显而易见的解决方案而不是嵌入式函数:它更短,对大多数程序员来说更容易理解。别误会:我真的很喜欢Go的闭包 ;) - Wolf
1
有经验的程序员通常知道副作用。不,避免评估术语是三元运算符的主要特征之一。 - Jonathan Hartley

5

现在随着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指针传递到函数中,而if语句如果是false则不会调用该部分)


var a *string
fmt.Println(Ternary(a != nil, *a, "some thing else"))

解决方案是将其放在函数中调用,这样如果其为 false,则不会被执行。

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 在未来添加箭头函数)

playground

感谢这个答案提供者已经回答过了


5

虽然被创建者避免使用,但单行代码也有它的用处。

这个代码通过让你可选地传递需要评估的函数来解决惰性求值问题:

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一样对其进行包装。

这里提供的独立解决方案也很好,但对于某些用途可能不够清晰。


即使这不是学术性的,但还是相当不错的。 - Fabien
1
嘿!实际上你不需要在那里使用反射包。此外,Go会在编译后的二进制文件中非常积极地内联帮助函数,因此子程序调用最终基本上是免费的...而且二进制文件的大小令人惊讶。以下可能更易读:https://play.golang.org/p/9z1GoskyKLL - Sam Hughes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接