65.9K
CodeProject 正在变化。 阅读更多。
Home

模拟 C# 中的安全导航运算符

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.60/5 (8投票s)

2014年11月20日

CPOL

11分钟阅读

viewsIcon

18817

downloadIcon

106

安全导航运算符在最终进入 C# 后,可能会成为一个非常实用的功能。它包含在 C# 6 的计划发布版本中。在此之前,您可能需要模拟它。

引言

您是否希望在访问嵌套在其他对象中的对象时,编写安全、简洁且易于维护的代码?安全导航运算符可以为您排忧解难。但是,它尚未在 C# 中实现。在本文中,我将与您分享我*模拟它的*方法。

背景

安全导航运算符在其他一些语言中可用,例如 Groovy 语言。

String lname = person.name?.ToLowerCase();

运算符 ?. 允许您安全地调用 person 对象成员 name 的方法 ToLowerCase。“安全地”意味着即使 name 成员为 null,也不会抛出 NullReferenceException。它将允许您在访问对象的深层层次结构时安全地链接方法调用。

仔细审视问题

考虑以下(在 C# 中)有些牵强的例子。

class A
{
  public B b;
}

class B
{
  public C c;
}

class C
{
  public string name;
}

并且我们有一个类 A 的对象赋给变量 a

var a = new A() { b = new B() }

上面的表达式创建了一个类 A 的对象,其成员 b 被设置为一个新实例化的类 B 的对象。然而,由 b 引用的对象的成员 c 被设置为默认值 null,因为它没有显式赋值。现在,假设在程序后面的某个地方,我们希望通过这个表达式来引用 a 对象中 b 对象中的 c 对象的 name 成员。

a.b.c.name

这肯定会在运行时因 NullReferenceException 而失败,因为成员 cnull。我们需要采用以下使用三元运算符 ?: 的条件表达式。

(a != null && a.b != null && a.b.c != null ? a.b.c.name : null)

这不仅看起来笨拙,而且容易出错。当涉及的类成员是方法而不是变量时,情况会更糟。我将举一个实用的例子。前几天,我在处理 XML 文件时使用了 XElement 类。文件的相关部分如下所示。

<family>
  <person gender="female">
    ...
  </person>
  <person>
    ...
  </person>
</family>

我的 C# 代码的相关部分是:

if (person.Attribute("gender").Value == "male")
{
  ... // do something with the male members of the family
}

上面的代码正确处理了 XML 中的第一个 person 元素,但在运行时尝试处理第二个元素时因 NullReferenceException 而崩溃。为什么?第二个元素上没有 gender 属性,代码中也没有对其进行检查。安全编码的方式应该是:

if (person.Attribute("gender") != null 
&& person.Attribute("gender").Value == "male")
{
    ...
}

这只是一个示例,用来说明编写安全代码必须采取的笨拙方法——必须重复调用 Attribute 方法。在此特定情况下,.NET API 提供了一个特定类型的转换,使得以更简洁的方式编写相同的代码更加方便。但,我跑题了。如果 C# 的当前版本具备了安全导航运算符,那么相同的代码可以这样重写:

if (person.Attribute("gender")?.Value == "male")
{
    ...
}

在极力称赞了安全导航运算符的优点,以及它在当前 C# 版本(版本 5 及以下)中缺失的事实之后,是否有模拟它的可能性?是的,我们可以!事实上,我下面将要使用的方法甚至超越了安全导航运算符提供的便利性。

解决方案

定义一个扩展方法,如下所示。

static class Utils
{
  public static Tresult NotNull<Tin, Tresult>(this Tin a, Func<Tin, Tresult> whenNotNull)
  {
    return a != null ? whenNotNull(a) : default(Tresult);
  }
}

如您所见,扩展方法包含在一个 static 类中(如编译器所必需),我称之为 Utils。扩展方法,顾名思义,是 static 的,因此可以在不实例化 Utils 类的情况下调用它。此外,在第一个尖括号 ('<') 之前,隐藏在定义中的是方法的名称——'NotNull'。这个名称不是很描述性,但我找不到一个更简洁、更具表现力的名称。正是这个方法模拟了安全导航运算符的功能。该方法是泛型的,接受一对类型参数,称为 TinTresultTin 是将调用该方法进行操作的对象的类型,如方法参数 a 所表示。请注意,出现在 a 的声明之前的 this 关键字在参数列表中,这使得该方法成为一个扩展方法。这是根据定义扩展方法的规则,与讨论的主题无关。现在,有趣的部分来了:第二个参数是 .NET 类型 System.Func<Tin, Tresult> 的委托。这意味着我可以传递任何“指向”一个方法、该方法接受一个类型为 Tin 的参数并返回一个类型为 Tresult 的对象的委托。它的返回类型与 NotNull 方法的返回类型相同。

NotNull 方法本身的主体只有一行。我们检查 a 是否不为 null,如果是,则返回委托参数指向的方法返回的同一个对象。如果不是这种情况(即,如果 anull),我们则返回 Tresult 类型的默认值。对于引用(类)类型,此默认值是 null;对于值类型,则是“空”值。在后面的部分,我将向您展示如何修改上述解决方案,以考虑在 anull 的情况下返回自定义值。好了,解释够多了,现在我们来看看 NotNull 方法的实际应用。查看演示也将有助于阐明这个冗长的解释。

让我们将该方法应用于前面几段讨论过的 XML 读取问题。我们重写代码。

if (person.Attribute("gender").NotNull(x => x.Value) == "male")
{
  ...
}

如果您仔细查看上面的代码,您会看到 NotNull 方法正在对 Xattribute 类型(Attribute 方法的返回类型)的对象进行调用。与 NotNull 方法的定义进行比较,我们发现这就是 Tin 参数。在此特定情况下,Tin 类型参数是 XAttributeNotNull 方法的参数 aXattribute 类型。接下来,我使用 lambda 表达式将一个匿名方法传递给 NotNull 方法的委托参数。匿名方法所做的只是返回 XAttribute 对象的 Value 成员。您能告诉我 NotNull 方法的返回类型是什么吗?它与匿名方法返回的类型相同,即 System.String。您可以通过上面匿名方法返回的 Value 成员的类型来双重检查这一点。它们是相同的。如您所见,NotNull 方法兼作对 null 引用的安全检查。如果 Attribute 方法返回的 XAttribute 对象为 null,则委托永远不会被调用。在这种情况下,NotNull 方法返回的值将是 XAttribute 类型的默认值,即 null。正是这个 null 被检查是否等于字符串 "male"。现在,这段代码难道不显得优雅吗?没有重复调用 Attribute 方法;它可读且安全。

现在,让我们回到本文开头讨论的牵强的例子。该示例包含一组三个类,ABC。我们试图使用类 A 的对象访问 name 成员。这是使用 NotNull 方法的等效解决方案。

a.NotNull(x => x.b).NotNull(x => x.c).NotNull(x => x.name)

在上面的表达式中,第一次调用 NotNull 时,返回对象的类型为 B;第二次调用产生类型为 C 的对象;最后一次调用产生类型为 System.Stringname 成员的类型)的对象。您怎么看,这种技术难道不很巧妙吗?

超越安全导航运算符的能力

我提到我将要展示的方法甚至会超越安全导航运算符的便利性。现在,我将兑现我的承诺。具体来说,我将针对 NotNull 方法的一个“缺点”——它只能返回 Tresult 类型的默认实例。为了解决这个问题,让我再介绍一个 NotNull 方法的重载。

public static Tresult NotNull<Tin, Tresult>
(this Tin a, Func<Tin, Tresult> whenNotNull, Func<Tresult> whenNull)
{
  return a != null ? whenNotNull(a) : whenNull();
}

此重载的 NotNull 方法接受 3 个参数。前两个参数与第一个重载相同,而第三个参数赋予了该方法特殊的能力,使其在便利性方面超越了安全导航运算符。此第三个参数也是一个委托,但它是标准 .NET 类型 System.Func<Tresult>,它“指向”一个不接受参数并返回类型为 Tresult 的对象的函数。与第一个重载相比,唯一改变的部分是方法的正文。我们现在返回从第二个委托参数指向的函数返回的对象。为了区分第一个委托参数和第二个委托参数,我试图为两者想出描述性的名称。最终,我决定将第一个委托参数命名为 whenNotNull,将第二个委托命名为 whenNull。根据 a 参数的值是否为 null,我将调用其中一个或另一个委托返回结果。同样,让我们看看此重载的 NotNull 方法的实际应用。

包含第二个 NotNull 方法重载的完整 Utils 类现在如下所示:

static class Utils
{
  public static Tresult NotNull<Tin, Tresult>
    (this Tin a, Func<Tin, Tresult> whenNotNull) // the first overload
  {
    return a != null ? whenNotNull(a) : default(Tresult);
  }

  public static Tresult NotNull<Tin, Tresult>(this Tin a, Func<Tin, 
  Tresult> whenNotNull, Func<Tresult> whenNull) // the second overload
  {
    return a != null ? whenNotNull(a) : whenNull();
  }
}

在 XML 处理代码示例中,如果我想表达这样一个事实:person 元素上缺少 gender 属性意味着该人是 male。为了使事情更清楚,如果我有以下 XML:

<person gender="female">
  ...
</person>
<person>
  ...
</person>

意味着第二个 person 元素将 gender 属性设置为“male”。为了使用我们的 NotNull 方法简洁地表达这一点,我会将 C# 代码重写如下:

if (person.Attribute("gender").NotNull
(x => x.Value, () => "male") == "male")
{
  ...
}

现在,这段代码难道不显得优雅且富有表现力吗?代码调用了 NotNull 方法的第二个重载——那个接受 3 个参数的。特别是,我向 whenNull 参数传递了一个指向匿名方法的委托。匿名方法不接受任何参数,它所做的只是返回字符串 "male"。处理 XML 文件中的第二个 person 元素时,将调用此委托。由于该元素没有 gender 属性,C# 表达式 person.Attribute("gender") 为 null。当调用 NotNull 方法时,其 a 参数为 null,因此返回值是调用 whenNull 委托的结果。因此,整个表达式 person.Attribute("gender").NotNull(x => x.Value, () => "male") 的结果是字符串 "male"。

回到本文开头讨论的类 ABC 的牵强例子,我将引入一个“人为”要求。如果类 B 对象 c 成员的值为 null,我们希望使用以下 C 实例:

readonly C defC = new C { name = "Krishna" };

这意味着给定以下 C# 语句:

var a = new A { b = new B() };

表达式 a.b.c.name 必须返回字符串 "Krishna"。

显然,如前所述,上面的表达式将在运行时因 NullReferenceException 而失败。我们可以使用我们的 NotNull 方法以优雅的方式解决这个问题。

a.NotNull(x => x.b).NotNull(x => x.c).NotNull(x => x, () => defC).name

在上面的表达式中,第三次调用 NotNull 使用第二个重载,而前两次调用使用第一个重载。在此特定情况下,第二次调用 NotNull 的返回值是 null,它被传递给第三次调用 NotNull。由于 NotNull 方法的 a 参数为 null,因此调用了由 whenNull 参数引用的委托。在上面的表达式中,我们将 lambda 表达式 () => defC 传递给此委托,该委托简单地返回由 defC 变量引用的对象。最后,引用了这个对象的 name 成员。因此,表达式的结果是字符串 "Krishna"。如果 c 成员不为 null 呢?在这种情况下,第三次调用 NotNull 使用第一个 lambda 表达式:x => x,它简单地返回 c 引用的对象。那么整个表达式的结果将是当时 c 引用的对象 name 成员的值。

顺便说一句,上面所示的访问 name 成员的表达式即使在方法链中的任何对象为 null 的情况下也会返回 "Krishna"!例如,如果 a 的定义如下:

A a = null;

那么表达式

a.NotNull(x => x.b).NotNull(x => x.c).NotNull(x => x, () => defC).name

仍然会返回 "Krishna"!为什么?第一次调用 NotNull 将返回 null,因为这是 A 类型对象的默认值。因此,第二次调用 NotNull 也将返回 null。因此,第三次调用将调用其第二个委托参数,这将返回 "Krishna"。因此,链中的 null 会产生传播效应。这可能是为什么有些人将安全导航运算符称为 Null Propagation Operator 的原因。

结论

在本文中,我们看到了使用链式 "." 运算符可能会导致可怕的 NullReferenceException。我介绍了安全导航运算符。C# 中没有这样的运算符。但它已计划在即将发布的版本中推出。如果我们想确保异常安全,我们是否注定要使用三元运算符或类似的构造编写“丑陋”的代码?本文介绍了一种使用泛型扩展方法的优雅方法,该方法为此问题提供了一个优雅的解决方案。我们还看到了当安全导航运算符进入 C# 后,这些方法甚至可以超越它的能力。

编程愉快!

Using the Code

附带的源代码是一个 Visual Studio 2010 项目。该项目包含一个 C# 文件,您也可以从命令行进行编译和运行。

关注点

泛型和 lambda 表达式在赋予我们程序员表达意图的能力方面是值得注意的。

历史

这是本文的第一个版本。如果我的读者中有些人不理解使用 .NET XML API 的 XElementXAttribute 的示例,我可以提供有用的指导。本文献给我的亲爱精神导师 His Divine Grace A. C. Bhaktivedanta Swami Prabhupada。

© . All rights reserved.