在围棋中,C's三元运算符的惯用等价是什么?

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

int index = val > 0 ? val : -val

Go没有条件运算符。实现上面同一段代码的最惯用的方法是什么?我想出了下面的解决方案,但它似乎相当啰嗦

var index int


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

还有更好的办法吗?

375793 次浏览

正如所指出的(希望并不令人意外),在Go中使用if+else确实是惯用的方法来执行条件语句。

然而,除了完整的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)

编译器将内联这些简单的函数,所以它更快,更清楚,更简短。

没有括号,map三元很容易读:

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

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

这确保了两个分支都不会被计算。

如果所有你的分支是使副作用计算量大,下面将会进行semantically-preserving重构:

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程序会打印一个非正值,而它不应该!(类似地,如果反转分支,就会不必要地调用缓慢的函数,从而引入开销。)

埃尔德的回答既有趣又有创意,甚至可以说很聪明。

但是,建议改为:

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

是的,它们都编译成本质上相同的程序集,但是这段代码比调用匿名函数只返回一个可以首先写入变量的值要清晰得多。

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

此外,任何使用map文字的代码都不是一个好主意,因为在Go中映射根本不是轻量级的。自Go 1.3以来,小型地图的随机迭代顺序得到了保证,为了加强这一点,小型地图的内存效率大大降低。

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

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更好,需要施放但有效。仅供参考:

benchmarkabsternally -8 100000000 18.8 ns/op

BenchmarkAbsIfElse-8 2000000000 0.27 ns/op

在没有争论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之前的答案如下:


下面的If构造可以在我的github.com/icza/gox库中与许多其他方法一起使用,是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) },
)

去操场上试试这些例子。

虽然被创作者所回避,但俏皮话也有自己的一席之地。

这个函数解决了惰性求值的问题,它允许你在必要时可选地传递函数来求值:

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那样包装它。

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

正如其他人所注意到的,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

我从中学到的是多利用一点围棋;在函数(result int)中使用命名的结果可以节省一行,将其声明为函数(你可以对capture做同样的事情),但编译器也识别这种习惯用法(只赋值IF),并在可能的情况下将其替换为条件指令。

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

关于三元运算符的惯用方法,还有一个建议:

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

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

/*
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://stackoverflow.com/a/45886594/9935654
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://stackoverflow.com/a/34636594/9935654
func BenchmarkTernaryOperatorWithFunc(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = func() int {
if i%2 == 0 {
return i
} else {
return -i
}
}
}
}


// https://stackoverflow.com/a/31483763/9935654
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

现在随着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语句,如果它是假的就不会调用该section)


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

操场上 < / p >

他已经回答了这个回答

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

我喜欢的是这样一个明确的界面:

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🤷‍♂️的返回类型自动推断出来