为什么我们更喜欢?

why do we prefer ? to ?? operator in c#?

我最近发现我们可以用??操作员检查空值。请检查以下代码示例:

1
   var res = data ?? new data();

这和

1
   var res = (data==null) ? new data() : data ;

我检查了整个项目源代码库和其他一些开放源代码项目。这个??操作符从未被使用过。

我只是想知道这背后有什么原因,比如性能问题或者其他什么?

编辑:

我刚刚根据递归安东的注释更新了我的示例代码。粗心是个错误。:(


当检查空值时,空合并运算符更清晰,这是它的主要用途。它也可以用链子锁起来。

1
2
3
4
object a = null;
object b = null;
object c = new object();
object d = a ?? b ?? c; //d == c.

虽然该运算符仅限于空检查,但三元运算符不是。例如

1
2
bool isQuestion = true;
string question = isQuestion ?"Yes" :"No";

我认为人们只是不知道空合并运算符,所以他们使用三元运算符。在大多数C语言中,三元存在于C之前,因此如果您不知道C的内部和外部以及/或者您用另一种语言编程,三元是一种自然的选择。但是,如果要检查空值,请使用空合并运算符,它是为此而设计的,并且IL是稍微优化的(比较??到if-then-else)。

下面是一个比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
object a = null;
object b = null;
object c = null;

object nullCoalesce = a ?? b ?? c;

object ternary = a != null ? a : b != null ? b : c;

object ifThenElse;

if (a != null)
    ifThenElse = a;
else if (b != null)
    ifThenElse = b;
else if (c != null)
    ifThenElse = c;

首先,只需看看空合并的语法,它就更清楚了。三元确实令人困惑。现在让我们看看IL

仅空合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.entrypoint
.maxstack 2
.locals init (
    [0] object a,
    [1] object b,
    [2] object c,
    [3] object nullCoalesce)
L_0000: ldnull
L_0001: stloc.0
L_0002: ldnull
L_0003: stloc.1
L_0004: newobj instance void [mscorlib]System.Object::.ctor()
L_0009: stloc.2
L_000a: ldloc.0
L_000b: dup
L_000c: brtrue.s L_0015
L_000e: pop
L_000f: ldloc.1
L_0010: dup
L_0011: brtrue.s L_0015
L_0013: pop
L_0014: ldloc.2
L_0015: stloc.3
L_0016: ldloc.3
L_0017: call void [mscorlib]System.Console::WriteLine(object)
L_001c: ret

仅三进制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.entrypoint
.maxstack 2
.locals init (
    [0] object a,
    [1] object b,
    [2] object c,
    [3] object ternary)
L_0000: ldnull
L_0001: stloc.0
L_0002: ldnull
L_0003: stloc.1
L_0004: newobj instance void [mscorlib]System.Object::.ctor()
L_0009: stloc.2
L_000a: ldloc.0
L_000b: brtrue.s L_0016
L_000d: ldloc.1
L_000e: brtrue.s L_0013
L_0010: ldloc.2
L_0011: br.s L_0017
L_0013: ldloc.1
L_0014: br.s L_0017
L_0016: ldloc.0
L_0017: stloc.3
L_0018: ldloc.3
L_0019: call void [mscorlib]System.Console::WriteLine(object)
L_001e: ret

如果那样的话,只有其他的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
.entrypoint
.maxstack 1
.locals init (
    [0] object a,
    [1] object b,
    [2] object c,
    [3] object ifThenElse)
L_0000: ldnull
L_0001: stloc.0
L_0002: ldnull
L_0003: stloc.1
L_0004: newobj instance void [mscorlib]System.Object::.ctor()
L_0009: stloc.2
L_000a: ldloc.0
L_000b: brfalse.s L_0011
L_000d: ldloc.0
L_000e: stloc.3
L_000f: br.s L_001a
L_0011: ldloc.1
L_0012: brfalse.s L_0018
L_0014: ldloc.1
L_0015: stloc.3
L_0016: br.s L_001a
L_0018: ldloc.2
L_0019: stloc.3
L_001a: ldloc.3
L_001b: call void [mscorlib]System.Console::WriteLine(object)
L_0020: ret

IL不是我的强项之一,所以也许有人可以编辑我的答案并扩展它。我本来打算解释我的理论,但我不想把自己和别人搞混了。所有三个loc的数目都相似,但并非所有il操作符都需要相同的执行时间。


这个??运算符(也称为空合并运算符)不如三元运算符知名,因为它首次使用.NET 2.0和可空类型。不使用它的原因可能包括不了解它的存在,或者更熟悉三元运算符。

这就是说,检查空值并不是三元运算符的唯一优点,因此它不是它的替代品,更像是满足特定需求的更好的替代品。:)


我能想到的一个原因是这个运算符是在.NET 2.0中引入的,因此.NET 1.1的代码不能包含它。

我同意你的看法,我们应该经常使用这个。

参考链接


根据鲍勃的回答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public object nullCoalesce(object a, object b, object c)
{
    return a ?? b ?? c;
}
public object ternary(object a, object b, object c)
{
    return a != null ? a : b != null ? b : c;
}
public object ifThenElse(object a, object b, object c)
{
    if (a != null)
        return a;
    else if (b != null)
        return b;
    else
        return c;
}

…这是来自发布版本的IL…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
.method public hidebysig instance object nullCoalesce(
    object a,
    object b,
    object c) cil managed
{
    .maxstack 8
    L_0000: ldarg.1
    L_0001: dup
    L_0002: brtrue.s L_000b
    L_0004: pop
    L_0005: ldarg.2
    L_0006: dup
    L_0007: brtrue.s L_000b
    L_0009: pop
    L_000a: ldarg.3
    L_000b: ret
}

.method public hidebysig instance object ternary(
    object a,
    object b,
    object c) cil managed
{
    .maxstack 8
    L_0000: ldarg.1
    L_0001: brtrue.s L_000a
    L_0003: ldarg.2
    L_0004: brtrue.s L_0008
    L_0006: ldarg.3
    L_0007: ret
    L_0008: ldarg.2
    L_0009: ret
    L_000a: ldarg.1
    L_000b: ret
}

.method public hidebysig instance object ifThenElse(
    object a,
    object b,
    object c) cil managed
{
    .maxstack 8
    L_0000: ldarg.1
    L_0001: brfalse.s L_0005
    L_0003: ldarg.1
    L_0004: ret
    L_0005: ldarg.2
    L_0006: brfalse.s L_000a
    L_0008: ldarg.2
    L_0009: ret
    L_000a: ldarg.3
    L_000b: ret
}

一个原因(其他人已经接触过)可能是缺乏意识。它也可能是(在我自己的例子中),希望尽可能减少在代码库中执行类似操作的方法的数量。所以我倾向于对所有紧凑的if-a-condition-is-met-do-this-others-do-that情况使用三元运算符。

例如,我发现以下两个陈述在概念层面上非常相似:

1
2
return a == null ? string.Empty : a;    
return a > 0 ? a : 0;

我认为这只是其他语言的习惯。是吗??操作员不使用任何其他语言。


我本以为相当于

1
var res = data ?? data.toString();

将是

1
var res = (data!=null) ? data : data.toString();