使用Null合并运算符的独特方法


164

我知道在C#中使用Null合并运算符的标准方法是设置默认值。

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

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

但是还有什么??用呢?它看起来不像三元运算符有用,除了比以下内容更简洁和易于阅读之外:

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

因此,考虑到甚至更少了解空合并运算符...

  • 你用过??别的吗?

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

Answers:


216

好吧,首先,它比标准三元链更容易链接:

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"];

12
对于运营商而言,链接是一大优势,它消除了一堆多余的IF
chakrit

1
今天,我只是用它替换了我在了解三元或空合并运算符之前编写的一个简单的IF块。原始IF语句的true和false分支称为同一方法,如果某个输入为NULL,则用不同的值替换其参数之一。使用null合并运算符,这只是一个调用。当您的方法需要两个或多个这样的替换时,这真的很强大!
David A. Gray

177

我将其用作延迟加载单线:

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

可读吗?自行决定。


6
嗯,您发现了一个反例,“为什么有人要把它用作混淆的IF”……实际上我很容易理解。
Godeke

6
我可能会缺少某些东西(我主要使用Java),但是那里没有竞争条件吗?
贾斯汀·K 2010年

9
@Justin K-如果多个线程正在访问同一对象的LazyProp属性,则只有竞争条件。如果需要每个实例的线程安全性,则可以使用锁轻松修复它。显然,在此示例中,它不是必需的。
Jeffrey L Whitledge 2010年

5
@杰弗里:如果很清楚,我不会问这个问题。:)当我看到该示例时,我立即想到了一个单例成员,并且由于我碰巧在多线程环境中进行了大多数编码工作……但是,是的,如果我们认为代码是正确的,那么就不需要多余的了。
贾斯汀·卡2010年

7
不必一定要具有竞争条件的Singleton。只是包含LazyProp的类的共享实例,以及访问LazyProp的多个线程。Lazy <T>是执行此类操作的更好方法,默认情况下是线程安全的(您可以选择更改Lazy <T>的线程安全性)。
Niall Connaughton

52

我发现它以两种“稍微奇怪”的方式有用:

  • 作为out编写时具有参数的替代方法TryParse例程(即,如果解析失败,则返回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所做的操作,但是在寻找需要保留评估表达式变量的情况。我不熟悉lambda和扩展名,所以您可以查看以下内容是否遵循类似的模式(即是否起作用)? stackoverflow.com/questions/1234263/#1241780
maxwellb

33

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

考虑一下,例如:

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

而使用三元运算符时,您可能会遇到以下任何一种情况:

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

它两次调用MyMethod,或者:

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

无论哪种方式,空合并运算符都更干净,我想更有效。


1
+1。这是为什么我喜欢null合并运算符的一个重要原因。当调用MyMethod()有任何副作用时,它特别有用。
CVn

如果MyMethod()在返回值之外没有任何影响,则编译器知道不会两次调用它,因此在大多数情况下,您实际上不必担心效率。
TinyTimZamboni

MyMethod()点缀对象的链接序列时,它还使事物更易于阅读。例如:myObject.getThing().getSecondThing().getThirdThing()
xdhmoore

@TinyTimZamboni,您对此编译器行为有参考吗?
库巴Wyrostek

@KubaWyrostek我不了解C#编译器的具体工作原理,但是我对llvm的静态编译器理论有一定的经验。编译器可以采用多种方法来优化这样的调用。全局值编号将注意到,MyMethod在这种情况下,对的两个调用是相同的,假定这MyMethod是一个纯函数。另一个选项是“自动记忆”或仅在缓存中关闭该功能。另一方面:en.wikipedia.org/wiki/Global_value_numbering
TinyTimZamboni

23

要考虑的另一件事是,合并运算符不会像三元数那样两次调用属性的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。


4
这值得更多的投票。我读过SOOO很多次,??相当于?:
库巴Wyrostek

1
关于两次调用吸气剂的有效点。但是,在这个示例中,我会认为一个糟糕的设计模式具有误导性的名为getter的名称,实际上无法对对象进行更改。
利纳斯'18

15

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

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?

我在这里可能有点晚,但是第二个示例将抛出if kvp == null。并且实际上Nullable<T>有一种GetValueOrDefault我通常使用的方法。
CompuChip

6
KeyValuePair是.NET框架中的值类型,因此访问其任何属性都不会引发空引用异常。msdn.microsoft.com/en-us/library/5tbh8a42(v=vs.110).aspx
Ryan

9

我用过?? 在我的IDataErrorInfo实现中:

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

public string this[string columnName]
{
    get { ... }
}

如果任何单个属性都处于“错误”状态,则会收到该错误,否则将返回null。真的很好。


有趣。您正在使用“ this”作为属性。我从来没有那样做。
阿姆斯特朗(Armstrongest)

是的,这是IDataErrorInfo工作方式的一部分。通常,该语法仅对集合类有用。
马特·汉密尔顿,

4
您在存储错误信息this["Name"]this["Address"]等等?
安德鲁(Andrew)

7

您可以使用null合并运算符使它更整洁,以处理未设置可选参数的情况:

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

如果将这行写成不是很棒arg ?= Arg.Default吗?
杰西·德·威特

6

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

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

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

Resharper实际上会建议将此作为“传统”惰性负载的重构。
理查兹(Arichards)

5

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

其实,我的经验是,所有的人太少熟悉的三元操作符(或者更准确地说,在有条件的经营者,?:是“三元”在同一意义上||是二进制或+是一元或二元;但是碰巧是仅是多种语言中的三元运算符),因此至少在该有限示例中,您的语句就此失败。

另外,如前所述,在一种主要情况下,空合并运算符非常有用,也就是说,每当要求值的表达式有任何副作用时。在这种情况下,您必须先使用(a)引入临时变量,或者(b)更改应用程序的实际逻辑,才能使用条件运算符。(b)显然在任何情况下都不适合,虽然这是个人喜好,但我不喜欢在声明范围内添加很多无关紧要的变量,即使是短暂的变量也是如此,因此(a)也不适合特定情况。

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


我的评论很晚-但很高兴看到有人掩饰三元运算符是带有三个参数的运算符(C#中现在有多个)。
史蒂夫·基德

5

我最近一直在做的一件事是使用null合并来备份到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";

4

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


string result1 = string.empty ?? "dead code!";

string result2 = null ?? "coalesced!";

输出:

result1 = ""

result2 = coalesced!

我目前正在研究覆盖?? 操作员来解决此问题。将其内置到框架中肯定很方便。

有什么想法吗?


您可以使用Extension方法来做到这一点,但我同意,这将是对代码的不错补充,并且在Web上下文中非常有用。
阿姆斯特朗(Armstrongest)2009年

是的,这是一个经常发生的情况...甚至还有一个特殊的方法String.IsNullOrEmpty(string)...
Max Galkin 09年

12
“ null-coalesce运算符不会检测到空字符串。” 好吧,它是null -coalescing运算符,而不是nullOrEmpty -coalescing运算符。就我个人而言,我鄙视将空值和空值混合使用区分两者的语言,这使得与不太烦人的事物进行交互。而且我有点强迫症,所以即使我理解了原因(例如在[大多数实现?] SQL中),语言/实现也无法区分两者,这让我很烦。
JAB

3
??不能重载:msdn.microsoft.com/zh-cn/library/8edha89s(v=vs.100).aspx-拥有重载性将是一个很好的选择。我使用一个组合:s1.Nullify() ?? s2.Nullify()在字符串为空的情况下,其中string Nullify(this s)返回null
Kit

唯一的问题?我只是发现自己想要?? =,并在查看是否有方法的同时找到了该线程。(情况:第一遍加载了异常情况,现在我想回顾一下并将默认值加载到尚未加载的任何内容中。)
Loren Pechtel

3

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

您应该使用最能表达您意图的内容。由于一个空COALESCE操作,使用它

另一方面,由于它是如此专业,所以我认为它没有其他用途。我希望||像其他语言一样适当地重载运算符。这将在语言设计中更加节俭。但是...


3

凉!算我一个不了解null合并运算符的人-这真是太漂亮了。

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

我想到的第一个可能使用的地方是将所有默认参数都放在一个地方。

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
}

2

有点奇怪的用例,但是我有一种方法,其中一个IDisposable对象可能作为arg传递(并因此由父对象传递),但也可能为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);
    [...]
}

0

我已经这样使用:

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; 
            }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.