Go 是一门十分精简的语言。对于每一个引入的语言特性,Go 核心团队都慎之又慎,有时候甚至让人觉得有点死脑筋。

三目表达式条件表达式

什么是三目运算符?

什么是三目运算符

首先,看一下不支持三目表达式特性的 Go 语言的写法

var genderDesc string
if gender == 1{
    genderDesc = "男"
} else {
    genderDesc = "女"
}
fmt.Println(genderDesc)

再看一下支持三木表达式特性的 C++ 语言的写法

std::string genderDesc = (gender == 1 ? "男" : "女");
std::cout << genderDesc << std::endl;
?:

Go为什么不支持三目运算符?

支持三目运算符的编程语言有很多,C/C++、C#、Java、JavaScript、Python、Ruby等。但 Go 为什么不支持这个主流语言普遍都支持的特性呢?从 Go 语言的 FAQ 中可以略知一二。

The reason ?: is absent from Go is that the language's designers had seen the operation used too often to create impenetrably complex expressions. The if-else form, although longer, is unquestionably clearer. A language needs only one conditional control flow construct.

Go 核心团队认为,程序员常常会利用三目运算符构建及其复杂的表达式,而这么复杂的表达式一定都可以通过拆解成一个或者多个 if 语句来实现,并且 if 语句的可读性更好。顺便猜测一下,因为 Go 核心团队成员都是拥有多年经验的 C/C++ 大师,对于 C++ 那不断膨胀的语言特性一定也是心有余悸。他们不想让 Go 走上 C++ 的老路,不想在 Go 语言中出现做同一件事却有10种炫技式的不同写法的现象,他们希望在 Go 中有且只有一种写法。

对于官方给出的这么官方的回答,肯定有人同意,也有人会反对。显然我属于后者,否则也不会有这篇吐槽文章。至于原因,请继续往下看。

三目运算符有哪些常见的使用场景?

同一个语言特性,不同的人会有不同的使用方式。我无法穷举三目运算符的所有使用方式,下面我按照是否嵌套的的三目运算符划分了两个常见的场景。

  • 场景一:无三目运算符嵌套
#include <iostream>

struct Person {
    unsigned char gender;
};

int main() {
    Person p;
    std::cout << (p.gender == 1 ? "男" : "女") << std::endl;
}

可见,在没有三目运算符嵌套的情况下,并不会对代码可读性产生任何影响。当然,我也见过极个别网友说这种代码的可读性也不高。对于这种例外,我只能建议其去看眼科了。

  • 场景二:三目运算符嵌套
#include <iostream>

struct Person {
    unsigned char gender;
};

int main() {
    Person *p = new Person;
    p->gender = 1;
    std::cout << (p == nullptr ? "未知" : (p->gender == 1 ? "男" : "女")) << std::endl;
    delete p;
}

对于三目运算符嵌套的场景,最常见的也是示例中的两层嵌套,多于两层的嵌套就会对代码可读性产生较大影响。但我认为,这不能成为否定三目运算符积极意义的理由,if 的多层嵌套同样会影响代码可读性。下面再看一下没有三目运算符特性的 Go 语言的写法。

package main

import "fmt"

type Person struct {
	gender uint8
}

func main() {
	var genderDesc string

	p := new(Person)
	if p == nil {
		genderDesc = "未知"
	} else {
		if p.gender == 1 {
			genderDesc = "男"
		} else {
			genderDesc = "女"
		}
	}

	fmt.Println(genderDesc)
}
代码可读性
genderDesc

如何造一个三目运算符的轮子?

既然 Go 不支持三目运算符,而我又需要它,那只能尝试去造一个这样的轮子。我会使用三种方式来造这个轮子,代码也没几行,实现起来也不难,那为什么还要官方作为语言特性提供呢?那些认为三目运算符没必要的人可能也会拿这个当作理由。看完下面再来评价这个轮子好不好。

  • 版本一:为每种基本数据类型添加一个类似三目运算符的条件表达式函数
package condexpr

// Str string类型的条件表达式函数
func Str(expr bool, a, b string) string {
	if expr {
		return a
	}
	return b
}

// Int int类型的条件表达式函数
func Int(expr bool, a, b int) int {
	if expr {
		return a
	}
	return b
}

// Float64 float64类型的条件表达式函数
func Float64(expr bool, a, b float64) float64 {
	if expr {
		return a
	}
	return b
}
genderDesc := condexpr.Str(p.gender == 1, "男", "女")
interface{}
package condexpr

// Interface interface{}类型的条件表达式函数
func Interface(expr bool, a, b interface{}) interface{} {
	if expr {
		return a
	}
	return b
}
genderDesc := condexpr.Interface(p.gender == 1, "男", "女").(string)interface{}
  • 版本三:使用泛型特性定义一个万能条件表达式函数
package condexpr

// Any 若expr成立,则返回a;否则返回b。
func Any[T any](expr bool, a, b T) T {
	if expr {
		return a
	}
	return b
}
genderDesc := condexpr.Any(p.gender == 1, "男", "女")
var p *Person
genderDesc := Any(p == nil, "未知", Any(p.gender == 1, "男", "女")) // panic
fmt.Println(genderDesc)
p == nilp.gender
p == nilp.gender == 1

总结

我个人极为期待三目运算符这个语言特性,因为它能让代码更加简洁,它能减少非必要变量的定义,降低程序员在命名方面的负担。虽然,可以人为地通过函数方式模拟三目运算符,但这种方式依然做不到语言特性级别的惰性计算,无法真正取代三目运算符。

参考

本文使用 Zhihu On VSCode 创作并发布