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个回答

4

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

然而,建议改为:

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

是的,它们都可以编译成基本相同的汇编代码,但是这段代码比调用匿名函数返回可以在第一次写入变量中的值更易读。

基本上,简单清晰的代码比创意代码更好。

此外,在Go语言中使用地图字面量的任何代码都不是一个好主意,因为在Go语言中地图根本不是轻量级的。自Go 1.3以来,小地图的随机迭代顺序是保证的,并且为了实现这一点,对于小地图而言,内存使用效率大大降低。

因此,创建和删除许多小地图既消耗空间又耗时。我有一段代码使用了一个小的地图(可能有两个或三个键,但常见用例只有一个条目),但该代码运行得非常缓慢。我们正在谈论至少比重写为使用双切片键[index]=>数据[index]映射的相同代码慢3个数量级。而且很可能更多,因为一些以前需要几分钟才能完成的操作现在在毫秒内完成。


1
简单明了的代码比创意代码更好。我非常喜欢这句话,但在 dog slow 后面的最后一部分有点困惑,也许其他人也会感到困惑? - Wolf
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
我明白了,感谢澄清。(也许这个答案本身在这一点上可以改进。) - Wolf

3

现在有了泛型,我们可以将这个愚蠢的函数改进一下...

func ternary[RV any](cmp bool, rvt RV) RV {
    if cmp {
        return rvt
    }
    var rvf RV
    return rvf
}

...然后可以这样使用...

    for i, data := range trials {
        ps.Pages = append(ps.Pages, PDFPage{
            Content:   data,
            ClassName: ternary(i > 0, "pagebreak"),
        })
    }

可能错误地运用了泛型的力量 D:

2

我已经编译了一些项目并比较了速度。

/*
go test ternary_op_test.go -v -bench="^BenchmarkTernaryOperator" -run=none -benchmem
*/
package _test

import (
    "testing"
)

func BenchmarkTernaryOperatorIfElse(b *testing.B) {
    for i := 0; i < b.N; i++ {
        if i%2 == 0 {
            _ = i
        } else {
            _ = -i
        }
    }
}

// https://dev59.com/zmIj5IYBdhLWcg3ws3Ff#45886594
func Ternary(statement bool, a, b interface{}) interface{} {
    if statement {
        return a
    }
    return b
}

func BenchmarkTernaryOperatorTernaryFunc(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = Ternary(i%2 == 0, i, -i).(int)
    }
}

// https://dev59.com/zmIj5IYBdhLWcg3ws3Ff#34636594
func BenchmarkTernaryOperatorWithFunc(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = func() int {
            if i%2 == 0 {
                return i
            } else {
                return -i
            }
        }
    }
}

// https://dev59.com/zmIj5IYBdhLWcg3ws3Ff#31483763
func BenchmarkTernaryOperatorMap(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = map[bool]int{true: i, false: -i}[i%2 == 0]
    }
}

输出

goos: windows
goarch: amd64
cpu: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz
BenchmarkTernaryOperatorIfElse
BenchmarkTernaryOperatorIfElse-8                1000000000               0.4460 ns/op          0 B/op          0 allocs/op
BenchmarkTernaryOperatorTernaryFunc
BenchmarkTernaryOperatorTernaryFunc-8           1000000000               0.3602 ns/op          0 B/op          0 allocs/op
BenchmarkTernaryOperatorWithFunc
BenchmarkTernaryOperatorWithFunc-8              659517496                1.642 ns/op           0 B/op          0 allocs/op
BenchmarkTernaryOperatorMap
BenchmarkTernaryOperatorMap-8                   13429532                82.48 ns/op            0 B/op          0 allocs/op
PASS
ok      command-line-arguments  4.365s

感谢提供真实数据。然而,当BenchmarkTernaryOperatorTernaryFunc被报告为比BenchmarkTernaryOperatorIfElse更快时,我有点担心。显然,该函数可以完全内联,但这仍然不能解释它可能为什么会更快。您的内核是否运行激进的节能模式(这意味着只有在100%的正常运行时间后才会增加时钟速度)?改变测试顺序是否有任何影响? - Martin Kealey

1

我正在尝试一个不使用三个参数函数的解决方案。 别误会,三个参数的解决方案很好用,但我个人更喜欢明确地命名事物。

我想要的是一个明确的接口,就像这样:

When(<condition>).Then(<true value>).Else(<false value>)

我是这样实现的:
type Else[T any] interface {
    ElseDo(fn func() T) T
    Else(value T) T
}

type Then[T any] interface {
    ThenDo(fn func() T) Else[T]
    Then(value T) Else[T]
}

type Condition[T any] struct {
    condition bool
    thenValue T
    thenFn    func() T
}

func When[T any](condition bool) Then[T] {
    return &Condition[T]{condition: condition}
}

func (c *Condition[T]) ThenDo(fn func() T) Else[T] {
    c.thenFn = fn
    return c
}

func (c *Condition[T]) Then(value T) Else[T] {
    c.thenValue = value
    return c
}

func (c *Condition[T]) ElseDo(fn func() T) T {
    if c.condition {
        return c.then()
    }

    return fn()
}

func (c *Condition[T]) Else(value T) T {
    if c.condition {
        return c.then()
    }

    return value
}

func (c *Condition[T]) then() T {
    if c.thenFn != nil {
        return c.thenFn()
    }
    return c.thenValue
}

使用方法:

When[int](something == "expectedValue").Then(0).Else(1)

When[int](value > 0).Then(value).Else(1)

When[int](value > 0).ThenDo(func()int {return value * 4}).Else(1)

When[string](boolean == true).Then("it is true").Else("it is false")

很不幸,我没有找到一种方法可以在调用When函数时摆脱显式类型。该类型不能通过 Then/Else 的返回类型自动推断出来 ‍♂️


1

关于 Go 语言中三元操作符的惯用方式,我有一个建议:

package main

import (
    "fmt"
)

func main() {
    val := -5

    index := func (test bool, n, d int) int {
        if test {
            return n
        }
        return d
    }(val > 0, val, -val)
    
    fmt.Println(index)
}

Go Playground


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