没想到第一篇文章就是这么水的内容…
——我才不会承认其实我只是想随便写点什么来测试一下古腾堡的代码高亮区块
运算优先级这种非常基础的东西即使对于新手来说也不是什么难点。打开百度输入“C# 运算优先级”你能看到一万篇内容大同小异的文章,为你从运算符的名称含义到结合方向进行全方位科普。但是这些面向新人的文章里基本上都不会提到C#中几个用法相对比较高级一些的null运算符(?. ?? ?[]等)。我之前也是从来没有特地去关注过,但是今天忽然碰到了,于是便在此记录下来。
先说结论
- Null 条件运算符
?.
和?[]
和其他基本运算符(如. [] new ->
等)相同,均为最高优先级。 - Null 合并运算符
??
优先级极低,不但低于加减乘除、位运算等算术运算符,甚至低于&& ||
这种逻辑运算符。仅高于下面列出的三元条件运算符?:
和赋值操作。 - 条件运算符
?:
虽然带着问号但是其实并不属于null运算符。列在这里单纯因为也带问号。优先级低于Null 合并运算符??
- 空合并赋值运算符
??=
C# 8.0后才支持的新特性,属于赋值操作,因此优先级垫底。
其他的都很好理解,只有Null 合并运算符 ??
比较特别。可以看到,Null合并运算符在C#中的优先级是非常低的,不但低于加减乘除、位运算等算术运算符,甚至低于&& ||
的逻辑运算符。之所以会对其不敏感主要是因为它不像 ?. ?[]
这些基本运算符一样有 . []
等作为原版参照物。我们通常只会在可能为空的变量上使用Null合并运算符,对于可能为空的变量,我们几乎不会直接将它和其他变量进行运算操作,这就使得??
与其他运算符很少同时出现。
进行验证
思考一下,哪些场景下可能会出现Null合并运算符和其他运算符同时出现的的情况呢?我们现在设计一个Person类代表一个人,他有一个Agreed属性,代表他是否支持我的某个意见,True表示支持,False表示反对。
现在我想要通过遍历找出所有反对这个意见的人,并将他们批判一番。那么我需要依次去判断他们每个人的Agreed是否为False。但是问题来了,有一种情况是我遍历到的这个人可能压根不在场(没有初始化),相当于跟这件事情无关,不存在Agreed与否的态度,自然也不会发出反对的声音,因此我们可以把他们和Agreed==true的支持者归为一类。这里我们就可以使用Null合并运算符来达成目的。逻辑如下
if (!p?.Agreed??true) //那么我们就需要深♂入♂交♂流一下了
好家伙,又是问号又是叹号的,一看就是出自阴间逻辑带师之手。我们来根据需求逻辑把它分拆一下。
if (! p?.Agreed ?? true)
,首先通过?.来判断p是否存在,如果存在,那么获取p的态度Agreed,如果p为null,则直接将其态度视为true即支持。因此下划线部分的逻辑筛选出来的即为判断的即是所有不在场(null)或者在场并且支持的人(.Agreed == true
),不论如何,下划线部分内容只可能是true或false。最后对其进行取非即可获得我们想要促膝谈心的对象啦。
这个逻辑看上去并没有什么问题对吧?VS也不会报任何错。但是实际运行一下就会发现我们看似完美形势一片大好的提案居然收获了高达97%的反对票!真有你们的啊背刺人!
且慢,这还真不是背刺人的锅。当然一路看下来想必你已经注意到了问题出在哪里了。是的,前面特地标红划线的if (!p?.Agreed??true)
的优先级居然是错的!
实例测试
class Person
{
public bool Agreed { get; set; }
}
class Program
{
static void Main(string[] args)
{
Person p1 = null;
Person p2 = new Person { Agreed = false };
Person p3 = new Person { Agreed = true };
Console.WriteLine(!(p1?.Agreed) ?? true); // True
Console.WriteLine( (p1?.Agreed) ?? true); // True
Console.WriteLine(!(p2?.Agreed) ?? false); // True
Console.WriteLine(!(p3?.Agreed) ?? false); // False
Console.ReadLine();
}
}
得益于VS直观明了的自动缩进规则,其实在按下F5之前我们差不多也已经能把答案猜得八九不离十了。第一行的!(p1?.Agreed) ?? true
结果为True说明左侧的非运算并没有与右侧的true结合,运算符的结合顺序是!(p1?.Agreed) ?? true
而非! (p1?.Agreed??true)
,非运算的结合优先级在null合并运算符??
之前。从t2与t3的测试结果也可以看出,表达式在对左侧的值取非后直接就忽略掉了右侧的false
。
这里存在一个很反常识的地方,为什么我会下意识的觉得上面的例子中??
会优先于!
结合呢?思路其实很简单,我需要取出左侧的p1?.Agreed
这个bool值,但是由于这里得到的结果可能是null,我不能直接就对它进行操作,而是要先为null的情况指定一个bool类型的默认值,然后才能对它进行取非的操作——好,到这里,我就已经踩进坑了。
实际是正如上面前两行所示,bool?类型的变量其实也是可以直接进行取非操作的(当然这并不意味着null可以进行取非操作)。如果对应的变量不为null,则正常对其bool内容进行取非,而如果对应的变量为null,则取非后的结果依然还是null不变!
理解了这点以后,就会发现!优先于??其实是再正常不过的事情了。虽然你觉得在取非之前我们应该先对变量为null的情况进行处理,但是其实取非操作从一开始就是可以兼容可空的bool?类型的,因此它优先于??和bool?类型进行结合乃是理所当然,天经地义。这波啊,C#是在第五层。
杂谈:语法糖
那么这个没用的小知识有什么用呢?实际遇到这个问题的场景是,我有一个操作其他进程内存的Memory类,其中保存了它所属的进程Process。在退出时,我需要释放Memory类在Process中所申请的内存等资源。这个时候就会有3种情况
- Memory类尚未初始化,其值为null。这个时候调用Memory.Dispose()毫无疑问的会出错。
- Memory类已经注入进程Process,并且Process进程正常存活,此时应当正常执行.Dispose()方法。
- Memory类已经注入进程Process,但是Process已经提前退出了,此时调用.Dispose()自然也会出现问题。
如果只有前两种情况,那么只需要简单的直接使用Null条件运算符Memory?.Dispose();
即可。但是加入了第三种情况后,我们就必须在确认Memory!=null
的前提下,在Memory.Dispose()
之前额外判断Memory.Process.HasExited
是否为false。
if (Memory!=null && !Memory.Process.HasExited)
Memory.Dispose();
诶呀这下用不了语法糖了,好气哦!
如果一定要使用炫酷的语法糖怎么办呢?这时候就需要用到Null合并运算符了。
if (!(Memory?.Process.HasExited) ?? false)
Memory.Dispose();
是的,这就是前面所说的 ??
和 !
同台竞技的场景了,但是对比前面朴素的代码,这样写也还是2行,这不是根本没区别吗!?(摔)
那么我们还可以更进一步——
(!Memory?.Process.HasExited ?? false ? Memory : null)?.Dispose();
真是吃饱了撑的…
代码行数缩减了,但是你也被项目经理开除了,这一切,值得吗?
语法糖在很多时候确实非常方便(例如?. ?:等),但是大多数情况下在一条语句中出现一次就是极限了。当语法糖嵌套的时候,代码的可读性往往就会变成噩梦。而且不论你写得如何花哨,最后编译器在编译时候都会将其还原为最基础的结构,实际并不能提升程序的运行效率。
所以说,珍爱生命,远离炫技啊。
题外话
如果你装了ReShaper的话就完全不会有这种烦恼了。人家直接就给你把这些地方标得清清楚楚。所以说我在其实一开始就知道结果的情况下居然还能水这么多字,真是忽然佩服起自己来了,芜湖~
关于运算符优先级相关的资料,如同开头所说网上找到的普遍不全,如果真的想要了解的话推荐还是看微软的官方文档最靠谱。看完以后上面的问题应该就都不复存在了。
- 运算符优先级:https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/operators/#operator-precedence
- Null 条件运算符 ?. 和 ?[]:https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/operators/member-access-operators#null-conditional-operators--and-
- Null 合并运算符 ??:https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/language-specification/expressions#the-null-coalescing-operator
- ?? 和 ??= 运算符:https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/operators/null-coalescing-operator
写的有意思 哈哈 学习了