条件运算符通常叫作三元运算符,它在一个表达式里从两个值中选出一个:condition ? valueIfTrue : valueIfFalse。
输出:
0 items, shipping 4.99
1 item, shipping 4.99
5 items, shipping 0
条件必须是 bool。条件求值之后,两个分支中只有一个会运行,另一个根本不会被求值。所以这个运算符可以安全地用于带保护的访问,比如 list.Count > 0 ? list[0] : "empty",如果对空列表求值 list[0] 就会抛出异常。
三元运算符与 if/else
三元运算符是一个表达式:它产生一个值,供你赋值、传参或返回。if 语句则是执行语句。下面这两个方法做的是同一件事:
// with if/else
static string Status(int stock)
{
if (stock > 0)
return "in stock";
else
return "sold out";
}
// with the ternary
static string Status(int stock) => stock > 0 ? "in stock" : "sold out";
当两个分支都是同类的简短值时,用三元运算符。当某个分支要执行多条语句、带有副作用,或者条件长到整行放不下时,用 if。
三元运算符不能替代只执行动作的 if。下面的代码无法编译:
stock > 0 ? Ship(order) : Refund(order);
// error CS0201: Only assignment, call, increment, decrement, await, and new object expressions can be used as a statement
也不存在没有 else 的三元运算符:? 和 : 总是必需的,因为这个表达式在两种情况下都必须有值。
两个分支需要共同的类型
编译器会根据两个分支推断出整个表达式的类型。其中一个分支必须能隐式转换为另一个分支的类型。如果两边都不行,就会得到 CS0173:
var discount = isMember ? 10 : null;
// error CS0173: Type of conditional expression cannot be determined because
// there is no implicit conversion between 'int' and '<null>'
var label = inStock ? 1 : "none";
// error CS0173: ... between 'int' and 'string'
修复方法是把一个分支强制转换为你想要的类型,这样另一个分支就能转换过去:
输出:
no discount
7
3.5
points / 2.0 : 0 不需要强制转换:0 是 int,可以隐式转换为 double,所以整个表达式是 double。
C# 9 新增了目标类型推断:当表达式被赋值给一个已声明的类型时,编译器会使用那个类型。int? discount = isMember ? 10 : null; 在 C# 9 及更高版本中可以编译。用 var 时没有目标类型,所以 var discount = isMember ? 10 : null; 在所有版本中都会失败。
优先级与括号
条件运算符的优先级很低,比 +、== 和 && 都低。这在 a > b ? a : b 中很方便,但放进字符串拼接里就会让人意外:
Console.WriteLine("Status: " + isActive ? "on" : "off");
// error CS0029: Cannot implicitly convert type 'string' to 'bool'
编译器把条件读成了 "Status: " + isActive,这是一个字符串。给三元表达式加上括号就能修复:"Status: " + (isActive ? "on" : "off")。
字符串插值也有自己的陷阱。在 { } 里面,冒号表示格式说明符的开始({price:F2}),所以不加括号的三元表达式会让解析器困惑。用括号把它括起来:
输出:
2 users online
You have 1 unread message
Status: online
嵌套三元表达式
这个运算符是右结合的,所以 a ? x : b ? y : z 会被分组为 a ? x : (b ? y : z)。每行写一个条件的小型链条是可读的:
输出:
512 B
20 KB
5 MB
超过两三层,或者各个条件检查的是不同的东西时,就改用 if/else if 或 switch 表达式。在真分支里嵌套(a ? (b ? x : y) : z)是最先变得难以理解的写法。
用 ?? 和 ?. 替代常见的三元表达式
很多三元表达式只是为了处理 null。C# 为这些情况提供了更简短的运算符:
| 三元表达式 | 更简短的写法 | 含义 |
|---|---|---|
name != null ? name : "guest" | name ?? "guest" | 为 null 时使用备用值 |
customer != null ? customer.Email : null | customer?.Email | 允许 null 的成员访问 |
c != null && c.Address != null ? c.Address.City : "unknown" | c?.Address?.City ?? "unknown" | 两者结合 |
value != null ? value : throw new ArgumentNullException(...) | value ?? throw new ArgumentNullException(...) | 尽早失败 |
输出:
Lena from Berlin
guest from unknown
guest from unknown
throw 表达式也可以作为三元表达式的任意一个分支:int age = input >= 0 ? input : throw new ArgumentOutOfRangeException(nameof(input));。
ref 条件表达式
从 C# 7.2 开始,三元表达式可以在两个变量(而不是两个值)之间做选择,并且你可以通过结果进行赋值:
// C# 7.2 and later
int wins = 0, losses = 0;
bool won = true;
ref int counter = ref (won ? ref wins : ref losses);
counter++; // increments wins
两个分支都必须是对同一类型变量的 ref。它在应用代码中很少见,主要用于性能敏感的代码,在处理大型结构体或数组时避免复制。
常见问题
C# 中的三元运算符是什么?
它就是条件运算符 condition ? valueIfTrue : valueIfFalse,也是 C# 中唯一有三个操作数的运算符。它先求出条件的值,然后只求值并返回两个值中的一个:string label = age >= 18 ? "adult" : "minor";。
C# 中三元运算符可以不写 else 部分吗?
不可以。? 和 : 两部分都是必需的,因为这个表达式必须始终产生一个值。如果你只想在条件成立时做某件事,请用 if 语句。三元表达式也不能单独作为一条语句:x > 0 ? A() : B(); 会报错 CS0201。
为什么我会遇到“Type of conditional expression cannot be determined”错误?
两个分支没有共同的类型,例如 var x = found ? 42 : null;(int 和 null)或 found ? 1 : "none"。把其中一个分支强制转换一下,让共同类型存在:found ? (int?)42 : null。从 C# 9 开始,声明了目标类型也可以:int? x = found ? 42 : null; 能通过编译,但用 var 仍然会失败。
如何在字符串插值中使用三元运算符?
用括号把它括起来:$"{(count == 1 ? "item" : "items")}"。不加括号的话,: 会被当成格式说明符的开头,代码就无法编译。
C# 中可以嵌套三元运算符吗?
可以。这个运算符是右结合的,所以 a ? x : b ? y : z 的意思是 a ? x : (b ? y : z)。分行书写的一层嵌套读起来没问题;更深的链条写成 if/else if 链或 switch 表达式会更清楚。