C#中问号(?)用法总结
三元运算符(?:)
三元运算符应该都很熟,通常我们也可以使用if-else来代替三元运算,这一点就不多说了,一个简单的例子:
//取一个0.5-1之间的随机值 var value = new Random().NextDouble(); value = value < 0.5 ? 0.5 + value : value;
可为空的值类型(T?)
C#中的值类型指的是结构体类型和枚举类型,他们有一个特点,就是拥有默认值(有时候也叫零值),而且它们不允许被赋予null值,因此C#在此基础上,引入了可空的值类型:
//T是值类型,T?本质上是Nullable<T>的简写,可以认为是T值类型加上null的组合类型 T?
可空值类型是Nullable<T>的对象,可以认为是T值类型加上null的组合类型,比如bool?,表示它的值只能是true、false或者null。
需要注意,Nullable<T>也是值类型,但是于普通的值类型不一样,它在未初始化时并非是你泛型参数的默认值,而是null,我们也可以认为Nullable<T>的默认值是null。
Nullable<T>有两个很常用的属性:
HasValue:表示当前可空值类型是否有值,如果有值(即不等于null),那么返回true,否则返回false Value:如果HasValue=true,则返回这个可空值类型对应的那个值类型类型的值,否则抛出 InvalidOperationException。
值运算的一元运算符和二元运算符也使用于可空值类型,当运算符所有运算对象都不为null时,它就等价于运算符作用于它们对应的值类型对象,否则返回null,也就是说可空类型的一元运算符和二元运算符返回的也是可空类型:
int? a = 10; int? b = null; int? c = 10; a++; //等价于a=a+1 b++; //等价于b=b+1,因为b=null,因此自增后b还是null var d = a * c; //虽然a和c都不等于null,但d还是可空类型 var e = a + b; //因为b=null,因此e=null
注:bool?的 & 和 | 运算时,不遵循这个规则,详细见如下:
x | y | x & y | x|y |
true | true | true | true |
true | false | false | true |
true | null | null | true |
false | true | false | true |
false | false | false | false |
false | null | false | null |
null | true | null | true |
null | false | false | null |
null | null | null | null |
可空引用类型(T?)
可空引用类型是为可空上下文而生的,具体可以参考:C# 可空上下文、可空引用类型(?)与可空容忍(!)
null 条件运算符(?.和?[])
null 条件运算符是一种成员访问符的拓展,从C#6.0开始出现:
?.表示成员访问运算符,主要用于对象属性、方法等成员访问,?[]表示元素访问运算符,主要用于集合中元素访问
null 条件运算符主要用于简化我们的代码:
a?.b 或 a?[b]的运算逻辑是: 1、如果a是null,那么a.b和a[b]都会抛出空指针异常,而a?.b和a?[b]都返回null,而不会抛出异常 2、如果a不是null,那么a?.b和a?[b]等价于a.b和a[b],如果a.b和a[b]抛出异常,a?.b和a?[b]也会抛出相同的异常
例如:
static void Method(Exception exception,string[] array) { //这里我们不知道exception和array会不会是null,所以我们用它前做if判断,否则会抛出空指针异常 if (exception != null) { Console.WriteLine(exception.Message); } if (array != null) { Console.WriteLine(array[0]); } //可以简写成 Console.WriteLine(exception?.Message); Console.WriteLine(array?[0]); }
需要注意的是,null 条件运算符的返回值是可空类型,如果我们原本值是值类型,那么经过null 条件运算符将会变成可空的值类型。例如:
//Length是int类型的值类型 int? length=array?.Length;//经过null 条件运算符,变成了可空的值类型
特别是在级联式调用时,null 条件运算符会很有用:
A?.B?.C?.D() E?.F?.G?[0]
这样,我们就不需要写很多的if判断了。
其实很多时候,我们是将null 条件运算符和null 合并运算符(??)一起使用的。
null 合并运算符(??和??=)
合并运算符(??)比较简单,它表示:
expr1 ?? expr2 1、如果表达式expr1的结果是null,那么会计算表达式expr2,并返回表达式expr2的结果 2、如果表达式expr1的结果不是null,那么会直接返回表达式expr1的结果,此时并不会计算表达式expr2的结果
例如:
List<int> numbers = null; int? a = null; //如果numbers=null,则重新初始化一个List,这样不用使用if判断了,list始终不为null var list = numbers ?? new List<int>(); list.Add(a ?? 1);//如果a=null,则返回1 int? m = 1, n = 2, k = 10; var i = (m + n) ?? ++k;//因为m+n!=null,所以++k并不会被计算 Console.WriteLine(k);//10
合并运算符(??)常常和null 条件运算符(?.),用于实现一种实现默认值得情况:
public class MyClass { public int Value { get; set; } } void Method(MyClass myClass) { //如果只是用?.运算,那么value将会值int?类型,这时可以结合??运算,value就会是int类型 var value = myClass?.Value ?? 1;//相当于说:如果myClass=null,那么myClass?.Value返回null,??就会返回1给value变量 }
此外,合并运算符(??)也可以类似级联的使用,与null 条件运算符(?.)一起使用时,可能会有意想不到的简洁:
//??合并运算符类似级联 var a = A ?? B ?? C ?? D ?? default; //与null 条件运算符(?.)一起使用,想想如果是使用if会有多么繁琐 var b = A?.B?.C() ?? D?.E?.F() ?? G?.H?.I() ?? default;
合并运算符(??)还经常和throw表达式结合,用于简化空指针判断,它常出现在构造函数和属性构造器中:
public class MyClass { public MyClass(string name) { this.name = name ?? throw new ArgumentNullException(nameof(name)); } string name; public string Name { get => name; set => name = value ?? throw new ArgumentNullException(nameof(value)); } }
??=也称为合并赋值运算符,是合并运算符(??)的一个特殊情况,它类似于+=、-=等运算,用于简化赋值运算:
a ??= b //等价于 a = a ?? b //等价于 if (a is null) { a = b; }
同样的,??=也具有赋值运算符的一些特性,如:
a ??= b ??= c
总结
有关问号(?)的这些语法糖可以方便我们的开发,而且像ECMAScript也提出可选链操作符(?.)和空值合并运算符(??)的语法,所以这些语法糖是值得我们学习。
此外,不知道你发现没有,除了三元运算符,包含?的运算符多多少少都和null有关,可能以后还会出现和?结合的运算符,但是不出意外的话也会和null有关吧。
参考文档:
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/nullable-value-types
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-coalescing-operator