【问题标题】:Unique ways to use the null coalescing operator [closed]使用空合并运算符的独特方法 [关闭]
【发布时间】:2026-02-08 11:50:01
【问题描述】:

我知道在 C# 中使用 null coalescing operator 的标准方法是设置默认值。

string nobody = null;
string somebody = "Bob Saget";
string anybody = "";

anybody = nobody   ?? "Mr. T"; // Returns Mr. T
anybody = somebody ?? "Mr. T"; // Returns "Bob Saget"

但是?? 还能用来做什么呢?它似乎没有ternary operator 有用,除了更简洁和更易于阅读之外:

nobody = null;
anybody = nobody == null ? "Bob Saget" : nobody; // Returns Bob Saget

所以考虑到甚至很少有人知道空合并运算符...

  • 您是否将?? 用于其他用途?

  • ?? 是必要的,还是应该只使用三元运算符(即 大多数人都熟悉)

【问题讨论】:

    标签: c# coding-style null conditional-operator null-coalescing-operator


    【解决方案1】:

    是??必要的,或者你应该只使用三元运算符(大多数人都熟悉)

    您应该使用最能表达您意图的内容。由于一个空合并运算符,使用它

    另一方面,由于它非常专业,我认为它没有其他用途。我会更喜欢 || 运算符的适当重载,就像其他语言一样。这在语言设计中会更加简洁。不过还好……

    【讨论】:

      【解决方案2】:

      嗯,首先,它比标准的三元运算符更容易链接:

      string anybody = parm1 ?? localDefault ?? globalDefault;
      

      对比

      string anyboby = (parm1 != null) ? parm1
                     : ((localDefault != null) ? localDefault
                     : globalDefault);
      

      如果可能为 null 的对象不是变量,它也能很好地工作:

      string anybody = Parameters["Name"]
                    ?? Settings["Name"]
                    ?? GlobalSetting["Name"];
      

      对比

      string anybody = (Parameters["Name"] != null ? Parameters["Name"]
                       : (Settings["Name"] != null) ? Settings["Name"]
                       :  GlobalSetting["Name"];
      

      【讨论】:

      • 链接对操作者来说是一个很大的优势,删除了一堆冗余的 IF
      • 我今天刚刚用它来替换我在了解三元或空合并运算符之前编写的一个简单的 IF 块。原始 IF 语句的 true 和 false 分支调用相同的方法,如果某个输入为 NULL,则将其参数之一替换为不同的值。使用空合并运算符,这是一个调用。当您有一个需要两个或多个此类替换的方法时,这真的很强大!
      【解决方案3】:

      我将它用作延迟加载单行:

      public MyClass LazyProp
      {
          get { return lazyField ?? (lazyField = new MyClass()); }
      }
      

      可读吗?自己决定。

      【讨论】:

      • 嗯,你找到了一个反例“为什么有人想用它作为一个混淆的 IF”......这对我来说实际上非常易读。
      • 这是我对 Null Coalescing 的主要用法。
      • 我可能遗漏了一些东西(我主要使用 Java),但那里没有竞争条件吗?
      • @Justin K - 如果多个线程正在访问同一对象的 LazyProp 属性,则只有竞争条件。如果需要每个实例的线程安全,它很容易用锁修复。显然,在这个例子中,它不是必需的。
      • 不一定非得是 Singleton 才有竞争条件。只是包含 LazyProp 的类的共享实例,以及访问 LazyProp 的多个线程。 Lazy 是做这种事情的更好方法,并且默认是线程安全的(你可以选择修改 Lazy 的线程安全)。
      【解决方案4】:

      酷!把我算作一个不知道空合并运算符的人——这真是太棒了。

      我发现它比三元运算符更容易阅读。

      我首先想到的可能是使用它的地方是将所有默认参数保存在一个地方。

      public void someMethod(object parm2, ArrayList parm3)
      {
        someMethod(null, parm2, parm3);
      }
      
      public void someMethod(string parm1, ArrayList parm3)
      {
        someMethod(parm1, null, parm3);
      }
      
      public void someMethod(string parm1, object parm2)
      {
        someMethod(parm1, parm2, null);
      }
      
      public void someMethod(string parm1)
      {
        someMethod(parm1, null, null);
      }
      
      public void someMethod(object parm2)
      {
        someMethod(null, parm2, null);
      }
      
      public void someMethod(ArrayList parm3)
      {
        someMethod(null, null, parm3);
      }
      
      public void someMethod(string parm1, object parm2, ArrayList parm3)
      {
        // Set your default parameters here rather than scattered 
        // through the above function overloads
        parm1 = parm1 ?? "Default User Name";
        parm2 = parm2 ?? GetCurrentUserObj();
        parm3 = parm3 ?? DefaultCustomerList;
      
        // Do the rest of the stuff here
      }
      

      【讨论】:

        【解决方案5】:

        我发现它在两个“有点奇怪”的方面很有用:

        • 作为在编写TryParse 例程时使用out 参数的替代方法(即,如果解析失败,则返回空值)
        • 作为比较的“不知道”表示

        后者需要更多信息。通常,当您创建与多个元素的比较时,您需要查看比较的第一部分(例如年龄)是否给出了明确的答案,然后只有在第一部分没有帮助的情况下才能查看下一部分(例如姓名)。使用 null 合并运算符意味着您可以编写非常简单的比较(无论是排序还是相等)。例如,在MiscUtil 中使用几个帮助类:

        public int Compare(Person p1, Person p2)
        {
            return PartialComparer.Compare(p1.Age, p2.Age)
                ?? PartialComparer.Compare(p1.Name, p2.Name)
                ?? PartialComparer.Compare(p1.Salary, p2.Salary)
                ?? 0;
        }
        

        诚然,我现在在 MiscUtil 中拥有 ProjectionComparer,以及一些扩展,这使得这种事情变得更加容易 - 但它仍然很整洁。

        在开始实现 Equals 时检查引用相等(或无效)也可以这样做。

        【讨论】:

        • 我喜欢您对 PartialComparer 所做的事情,但正在寻找需要保留评估表达式变量的情况。我不精通 lambdas 和扩展,所以你能看看以下是否遵循类似的模式(即它是否有效)? *.com/questions/1234263/#1241780
        【解决方案6】:

        我在 IDataErrorInfo 的实现中使用了??

        public string Error
        {
            get
            {
                return this["Name"] ?? this["Address"] ?? this["Phone"];
            }
        }
        
        public string this[string columnName]
        {
            get { ... }
        }
        

        如果任何单个属性处于“错误”状态,我会收到该错误,否则我会收到 null。效果很好。

        【讨论】:

        • 有趣。您正在使用“this”作为属性。我从来没有这样做过。
        • 是的,它是 IDataErrorInfo 工作原理的一部分。通常,该语法仅对集合类有用。
        • 您将错误消息存储在this["Name"]this["Address"] 等??
        【解决方案7】:

        另一个优点是三元运算符需要双重评估或临时变量。

        考虑一下,例如:

        string result = MyMethod() ?? "default value";
        

        使用三元运算符时,您只剩下:

        string result = (MyMethod () != null ? MyMethod () : "default value");
        

        两次调用 MyMethod,或者:

        string methodResult = MyMethod ();
        string result = (methodResult != null ? methodResult : "default value");
        

        无论哪种方式,null 合并运算符都更简洁,而且我猜想效率更高。

        【讨论】:

        • +1。这是我喜欢 null 合并运算符的一大原因。当调用 MyMethod() 有任何副作用时,它特别有用。
        • 如果MyMethod()除了返回一个值之外没有任何影响,编译器知道不会调用它两次,所以在大多数情况下你真的不必担心效率。
        • MyMethod() 是点状对象的链式序列时,恕我直言,它也使事情更具可读性。例如:myObject.getThing().getSecondThing().getThirdThing()
        • @TinyTimZamboni,您对编译器的这种行为有参考吗?
        • @KubaWyrostek 我不了解 C# 编译器的具体工作原理,但我对 llvm 的静态编译器理论有一些经验。编译器可以采用多种方法来优化这样的调用。 Global Value Numbering 会注意到在此上下文中对 MyMethod 的两次调用是相同的,假设 MyMethod 是一个纯函数。另一种选择是自动记忆或仅在缓存中关闭该功能。另一方面:en.wikipedia.org/wiki/Global_value_numbering
        【解决方案8】:

        唯一的问题是 null-coalesce 运算符不检测空字符串。


        string result1 = string.empty ?? "dead code!";
        
        string result2 = null ?? "coalesced!";
        

        输出

        result1 = ""
        
        result2 = coalesced!
        

        我目前正在考虑覆盖 ??操作员来解决这个问题。将它内置到框架中肯定会很方便。

        【讨论】:

        • 您可以使用扩展方法来做到这一点,但我同意,这将是对代码的一个很好的补充,并且在网络环境中非常有用。
        • 是的,这是一个常见的场景......甚至还有一个特殊的方法String.IsNullOrEmpty(string)......
        • "null-coalesce 运算符未检测到空字符串。"好吧,它是 null -coalescing 运算符,而不是 nullOrEmpty -coalescing 运算符。就个人而言,我鄙视在区分两者的语言中混合空值和空值,这使得与不太烦人的事物交互。而且我有点强迫症,所以当语言/实现无论如何都不能区分这两者时,我会很恼火,即使我理解其中的原因(就像在 [大多数实现?] SQL 中一样)。
        • ?? 不能被重载:msdn.microsoft.com/en-us/library/8edha89s(v=vs.100).aspx——不过,这将是一个很好的重载。我使用了一个组合:s1.Nullify() ?? s2.Nullify() 其中string Nullify(this s) 在字符串为空的情况下返回null
        • 唯一的问题?我只是发现自己想要 ??= 并在查看是否有办法做到这一点时找到了这个线程。 (情况:第一遍加载了异常情况,现在我想返回并将默认值加载到尚未加载的任何内容中。)
        【解决方案9】:

        是??必要的,或者你应该只使用三元运算符(大多数人都熟悉)

        实际上,我的经验是,很少有人熟悉三元运算符(或者更准确地说,是 条件 运算符;?: 是“三元”,与 || 的含义相同是二元或+ 是一元或二元;但它确实是许多语言中唯一的三元运算符),所以至少在那个有限的示例中,你的语句在那里失败了。

        此外,如前所述,当空合并运算符非常有用时,有一种主要情况,即当要计算的表达式有任何副作用时。在这种情况下,您不能在不 (a) 引入临时变量或 (b) 更改应用程序的实际逻辑的情况下使用条件运算符。 (b) 显然在任何情况下都不合适,虽然这是个人喜好,但我不喜欢用大量无关紧要的变量来混淆声明范围,即使是短暂的变量,所以 (a) 也在其中特定场景。

        当然,如果您需要对结果进行多次检查,条件运算符或一组if 块可能是完成这项工作的工具。但是对于简单的“如果 this 为 null,则使用 that,否则使用它”,null 合并运算符 ?? 是完美的。

        【讨论】:

        • 我的评论很晚 - 但很高兴看到有人提到三元运算符是一个具有三个参数的运算符(现在 C# 中不止一个)。
        【解决方案10】:

        我发现?? 运算符的最大优势是您可以轻松地将可空值类型转换为不可空值类型:

        int? test = null;
        var result = test ?? 0; // 'result' is int, not int?
        

        我经常在 LINQ 查询中使用它:

        Dictionary<int, int?> PurchaseQuantities;
        // PurchaseQuantities populated via ASP .NET MVC form.
        var totalPurchased = PurchaseQuantities.Sum(kvp => kvp.Value ?? 0);
        // totalPurchased is int, not int?
        

        【讨论】:

        • 我在这里可能有点晚了,但是如果kvp == null,第二个例子会抛出。实际上Nullable&lt;T&gt; 有一个我通常使用的GetValueOrDefault 方法。
        • KeyValuePair 是 .NET 框架中的值类型,因此访问它的任何属性都不会引发空引用异常。 msdn.microsoft.com/en-us/library/5tbh8a42(v=vs.110).aspx
        【解决方案11】:

        您可以使用空合并运算符使其更清晰地处理未设置可选参数的情况:

        public void Method(Arg arg = null)
        {
            arg = arg ?? Arg.Default;
            ...
        

        【讨论】:

        • 如果这行写成arg ?= Arg.Default岂不是很好?
        【解决方案12】:

        我喜欢使用空合并运算符来延迟加载某些属性。

        一个非常简单(和人为)的例子只是为了说明我的观点:

        public class *
        {
            private IEnumerable<string> _definitions;
            public IEnumerable<string> Definitions
            {
                get
                {
                    return _definitions ?? (
                        _definitions = new List<string>
                        {
                            "definition 1",
                            "definition 2",
                            "definition 3"
                        }
                    );
                }
            } 
        }
        

        【讨论】:

        • Resharper 实际上会建议将此作为“传统”延迟加载的重构。
        【解决方案13】:

        要考虑的另一件事是,coalesce 运算符不会像三元那样调用属性的 get 方法两次。

        所以有些场景你不应该使用三元运算符,例如:

        public class A
        {
            var count = 0;
            private int? _prop = null;
            public int? Prop
            {
                get 
                {
                    ++count;
                    return _prop
                }
                set
                {
                    _prop = value;
                }
            }
        }
        

        如果你使用:

        var a = new A();
        var b = a.Prop == null ? 0 : a.Prop;
        

        getter 将被调用两次,count 变量将等于 2,如果你使用:

        var b = a.Prop ?? 0
        

        count 变量应该等于 1。

        【讨论】:

        • 这值得更多的支持。我已经读过很多次了,?? 相当于?:
        • 关于 getter 被调用两次的有效点。但是这个例子我会认为一个糟糕的设计模式有这样一个误导性的命名 getter 来实际对对象进行更改。
        【解决方案14】:

        我最近经常做的一件事是使用空合并来备份到as。例如:

        object boxed = 4;
        int i = (boxed as int?) ?? 99;
        
        Console.WriteLine(i); // Prints 4
        

        这对于备份可能会失败的长链 ?. 也很有用

        int result = MyObj?.Prop?.Foo?.Val ?? 4;
        string other = (MyObj?.Prop?.Foo?.Name as string)?.ToLower() ?? "not there";
        

        【讨论】:

          【解决方案15】:

          这是一个有点奇怪的用例,但我有一个方法,其中 IDisposable 对象可能作为参数传递(因此由父级处理),但它也可能为 null(因此应该是在本地方法中创建和处理)

          要使用它,代码看起来像

          Channel channel;
          Authentication authentication;
          
          if (entities == null)
          {
              using (entities = Entities.GetEntities())
              {
                  channel = entities.GetChannelById(googleShoppingChannelCredential.ChannelId);
                  [...]
              }
          }
          else
          {
              channel = entities.GetChannelById(googleShoppingChannelCredential.ChannelId);
              [...]
          }
          

          但是使用空合并它会变得更加整洁:

          using (entities ?? Entities.GetEntities())
          {
              channel = entities.GetChannelById(googleShoppingChannelCredential.ChannelId);
              [...]
          }
          

          【讨论】:

            【解决方案16】:

            我是这样使用的:

            for (int i = 0; i < result.Count; i++)
            {
                object[] atom = result[i];
            
                atom[3] = atom[3] ?? 0;
                atom[4] = atom[4] != null ? "Test" : string.Empty;
                atom[5] = atom[5] ?? "";
                atom[6] = atom[6] ?? "";
                atom[7] = atom[7] ?? "";
                atom[8] = atom[8] ?? "";
                atom[9] = atom[9] ?? "";
                atom[10] = atom[10] ?? "";
                atom[12] = atom[12] ?? false;
            }
            

            【讨论】: