C#的 ?? 运算符有多大用处?

24

所以我一直对 ?? 运算符很感兴趣,但是我仍然无法使用它。通常,当我正在做类似以下的事情时,我会考虑到它:

var x = (someObject as someType).someMember;

如果someObject是有效的且someMember为null,我可以执行以下操作:

var x = (someObject as someType).someMember ?? defaultValue;

然而,当someObject为空时,我几乎总是遇到问题,而 ?? 并不能帮助我比手动进行空检查更加简洁。

你们在实际场景中发现了哪些使用 ?? 的用途呢?


10
FYI:?? 被称为 null 合并运算符。 - Ben S
是的,正是我所思考的。 - Ruben
我认为这应该是一个“社区维基”。 - Nathan Campos
我同意,有时候很诱人,但是我意识到我需要使用其他东西。我几乎从来没有机会使用它。 - Jason Kleban
13个回答

32

?? 运算符类似于 SQL 的 coalesce 方法,它会返回第一个非空值。

var result = value1 ?? value2 ?? value3 ?? value4 ?? defaultValue;

1
不错,从没想过可以这样用! - TheSean
我无法想象在这种方式下使用“??”的情况,而更好的设计不会消除。一个好的用例会很有趣。 - jphofmann
2
@jphofmann:我目前工作中的一个很好的用例。我必须通过调用函数来搜索一个复杂的业务对象。如果搜索失败,搜索函数将返回null。我必须使用不同的搜索参数重复搜索,直到找到一个对象为止。从这样的代码中可以很容易地理解搜索规则:var result = Find("A") ?? Find("Y") ?? Find("Q"); 使用 ?? 可以使您的代码更紧凑和可读。 - Martin Liversage

27

我同意您的观点,?? 运算符通常用处有限 - 当某些东西为 null 时,使用它提供一个后备值很有用,但在你想继续深入访问有时为空引用的属性或方法的情况下,它并不适用于防止 Null Reference Exception。

在我看来,比 ?? 更需要的是一个“null-dereference”运算符,它允许您链式连接长的属性和/或方法链,例如 a.b().c.d().e,而无需针对每个中间步骤进行 null 测试。 Groovy 语言中有一个 Safe Navigation operator,非常方便。

幸运的是,C#团队意识到了这个功能差距。请参见此 connect.microsoft.com suggestion ,了解 C# 团队将向 C# 语言添加 null-dereference 运算符的建议。

我们收到了不少类似这个的功能请求。社区讨论中提到的"?." 版本最接近我们的心意,它允许您在每个“点”处测试null,并与现有的??运算符很好地组合: a?.b?.c ?? d 意思是如果a、a.b或a.b.c中的任何一个为空,则使用d。 我们正在考虑将其用于未来的发布版本,但不会出现在C# 4.0中。 再次感谢, Mads Torgersen,C#语言PM
如果您也想在C#中添加此功能,请在connect网站上的建议中投票! :-)
我使用的一个解决方法是使用扩展方法,就像这篇博客文章中描述的那样,允许像这样编写代码:
string s = h.MetaData
            .NullSafe(meta => meta.GetExtendedName())
            .NullSafe(s => s.Trim().ToLower())

这段代码返回h.MetaData.GetExtendedName().Trim().ToLower(),如果h、h.MetaData或h.MetaData.GetExtendedName()为null,则返回null。我还扩展了此功能以检查null或空字符串,或null或空集合。以下是我用于定义这些扩展方法的代码:

public static class NullSafeExtensions
{
    /// <summary>
    /// Tests for null objects without re-computing values or assigning temporary variables.  Similar to 
    /// Groovy's "safe-dereference" operator .? which returns null if the object is null, and de-references
    /// if the object is not null.
    /// </summary>
    /// <typeparam name="TResult">resulting type of the expression</typeparam>
    /// <typeparam name="TCheck">type of object to check for null</typeparam>
    /// <param name="check">value to check for null</param>
    /// <param name="valueIfNotNull">delegate to compute if check is not null</param>
    /// <returns>null if check is null, the delegate's results otherwise</returns>
    public static TResult NullSafe<TCheck, TResult>(this TCheck check, Func<TCheck, TResult> valueIfNotNull)
        where TResult : class
        where TCheck : class
    {
        return check == null ? null : valueIfNotNull(check);
    }

    /// <summary>
    /// Tests for null/empty strings without re-computing values or assigning temporary variables
    /// </summary>
    /// <typeparam name="TResult">resulting type of the expression</typeparam>
    /// <param name="check">value to check for null</param>
    /// <param name="valueIfNotNullOrEmpty">delegate to compute non-null value</param>
    /// <returns>null if check is null, the delegate's results otherwise</returns>
    public static TResult CheckNullOrEmpty<TResult>(this string check, Func<string, TResult> valueIfNotNullOrEmpty)
        where TResult : class
    {
        return string.IsNullOrEmpty(check) ? null : valueIfNotNullOrEmpty(check);
    }
    /// <summary>
    /// Tests for null/empty collections without re-computing values or assigning temporary variables
    /// </summary>
    /// <typeparam name="TResult">resulting type of the expression</typeparam>
    /// <typeparam name="TCheck">type of collection or array to check</typeparam>
    /// <param name="check">value to check for null</param>
    /// <param name="valueIfNotNullOrEmpty">delegate to compute non-null value</param>
    /// <returns>null if check is null, the delegate's results otherwise</returns>
    public static TResult CheckNullOrEmpty<TCheck, TResult>(this TCheck check, Func<TCheck, TResult> valueIfNotNullOrEmpty)
        where TCheck : ICollection
        where TResult : class
    {
        return (check == null || check.Count == 0) ? null : valueIfNotNullOrEmpty(check);
    }
}

我喜欢辅助方法的想法,但很遗憾它必须是这样的hack。 - captncraig
没错。如果你想将这个转换成C#,有一个简单的方法可以加快这个过程:去https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=432830投票支持这个建议! :-) - Justin Grant
“safe-null-dereferencing operator”链接出现404错误。Microsoft Connect链接也是如此。 - Jonathon Reinhart
@JonathonReinhart - 感谢您报告损坏的链接,我已经使用更新的URL修复了它们。另外,如果要投票支持将此功能添加到C#中,请不要使用我上面评论中的链接,因为它也是损坏的。请转到这里:http://connect.microsoft.com/VisualStudio/feedback/details/432830。 - Justin Grant
看起来我们可能终于在C# 6中得到了这个功能。耶! - captncraig

22

我通常将其用于字符串或可空类型。

string s = someString ?? "Default message";
比...更容易
   string s;
   if(someString == null)
     s = "Default Message";         
   else
     s = someString;
或者
  string s = someString != null ? someString : "Default Message";

2
非常正确,但如果someString == string.Empty,那么它并不等同于您的中间语句,因为??运算符不会触发。 - Michael La Voie
1
@The Lame Duck,你说得对。一个空字符串和一个null字符串是不同的,这只是一个例子。实际上,我可以用null检查来替换字符串检查。我应该这样做... - Brandon
1
另请参阅 string.IsNullOrEmpty(s)。 - TrueWill
@TrueWill,这就是我在编辑之前的代码,但正如The Lame Duck所指出的那样,String.IsNUllOrEmpty并不等同于null合并运算符。而@Ed Swangren,则感谢您。当我进行编辑时,我混淆了大小写。今天是星期五,所以我有点乱 :P - Brandon

12

我经常在属性中使用它来惰性实例化对象,例如:

public MyObject MyProperty
{
    get
    {
        return this.myField ?? (this.myField = new MyObject());
    }
}

在C#中,赋值是一个表达式,所以你可以在同一语句中赋值并使用表达式的结果(分配的值)。我对这种代码模式仍然感到有点不爽,但它比下面的替代方案更简洁,所以它差不多是胜出的。

public MyObject MyProperty
{
    get
    {
        if (this.myField == null)
        {
            this.myField = new MyObject();
        }

        return this.myField;
    }
}

1
不错。如果您能使其线程安全,同时仍然保持在一行中,那么将获得额外的分数 :-) - Daniel Fortunov

8

提醒一下,三元运算符和空合并运算符生成的IL序列不同。前者大致如下:

// string s = null;
// string y = s != null ? s : "Default";
    ldloc.0
    brtrue.s notnull1
    ldstr "Default"
    br.s isnull1
    notnull1: ldloc.0
    isnull1: stloc.1

而后者看起来像这样:
// y = s ?? "Default";
    ldloc.0
    dup
    brtrue.s notnull2
    pop
    ldstr "Default"
    notnull2: stloc.1

基于这一点,我认为空合并运算符针对其目的进行了优化,不仅仅是语法糖。

7

?? 是一个空值检查运算符。

你的代码出现问题的原因是,如果 "someObject" 为空,则 ".someMember" 是一个空对象上的属性。你需要先检查 "someObject" 是否为空。

编辑:

我应该补充说明,是的,我发现 ?? 在处理数据库输入时非常有用,特别是来自 LINQ 的输入,但也适用于其他情况。我经常使用它。


3

我一直在使用它与App.Config相关的东西

string somethingsweet = ConfigurationManager.AppSettings["somesetting"] ?? "sugar";

2

最有用的是在处理可空类型时。

bool? whatabool;
//...
bool realBool = whatabool ?? false;

没有 ??,你需要编写以下内容,这会更加混乱。
bool realbool = false;
if (whatabool.HasValue)
    realbool = whatabool.Value;

我发现这个运算符对可为空的类型非常有用,但除此之外,我并没有经常使用它。绝对是一个很酷的快捷方式。


一种简洁的表示方法是:bool realBool = (whatabool == null) ? false : whatabool; - Jason Williams

1
在您的情况下,您可以创建静态属性DefaultObject并像下面这样使用...
var x = (someObject as someType ?? someType.DefaultObject).someMember;

你的DefaultObject将返回一个静态的只读对象。

就像EventArgs.Empty,String.Empty等等...


这对我来说似乎是危险的业务。如果我想要一个对象的属性只要它不是空的,这段代码将导致我甚至不知道对象是否存在;也许它只是恰好与“DefaultObject”共享一个属性,或者它是空的。 - Dan Tao
@Dan,如果你关心对象是否存在,请检查someObject == null。如果你只想向用户显示可读的内容,请使用SomeType.DefaultObject。顺便说一下,这种重构被称为引入Null对象。 - Don Kirkby

1

我在处理那些在正常情况下可能返回空值的方法时使用它。

例如,假设我有一个容器类,其中包含一个 Get 方法,如果该容器不包含键(或者 null 是有效关联),则会返回 null。然后我可能会这样做:

string GetStringRep(object key) {
    object o = container.Get(key) ?? "null";
    return o.ToString();
}

显然,这两个序列是等价的:

foo = bar != null ? bar : baz;
foo = bar ?? baz;

第二个更加简洁。

实际上,这并不是100%正确的。三元运算符从空合并运算符生成不同的IL序列。第一个看起来像这样:ldloc.0 brfalse.s isnull1 ldloc.0 br.s notnull1 isnull1: ldstr "Default" notnull1: stloc.1而后者看起来像这样:ldloc.0 dup brtrue.s notnull2 pop ldstr "Default" notnull2: stloc.1基于此,我认为空合并运算符针对其目的进行了优化,而不仅仅是纯粹的语法糖。 - Jesse C. Slicer
我非常抱歉格式混乱。唉,我会将其发布为答案,以使其看起来更整洁。 - Jesse C. Slicer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接