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

链式空值检查和 Maybe Monad

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (142投票s)

2010年9月12日

CPOL

5分钟阅读

viewsIcon

238769

展示了几个扩展方法如何解决“重复的空值检查”问题。

引言

很多程序员都遇到过这样的情况:在访问嵌套对象属性(例如:person.Address.PostCode)时,需要进行多次空值检查。在 XML 解析中,当缺失的元素和属性在尝试访问时返回 null(随后尝试访问 Value 会引发 NullReferenceException)时,这种需求经常出现。在本文中,我将展示如何在 C# 中通过对 Maybe Monad 的一种实现,结合使用扩展方法,来提高代码的可读性。

问题描述

那么,首先,让我们看看如何获取一个人的邮政编码(就想象你在处理 XML 或其他东西)。下面展示的代码进行了多次空值检查,并且仅在值可用时才进行赋值。

string postCode = null;
if (person != null && person.Address != null && 
    person.Address.PostCode != null)
{
  postCode = person.Address.PostCode.ToString();
}

你上面看到的代码相当难读(且难以维护)。实际上,我们很幸运地将所有代码都包含在一个 if 语句中——在更复杂的情况下,这可能是不可能的。让我们想象一个更复杂的情况——比如说,我们需要在 if 条件评估之间执行某些操作。我们会得到什么?没错——一连串的 if 语句。

string postCode;
if (person != null)
{
  if (HasMedicalRecord(person) && person.Address != null)
  {
    CheckAddress(person.Address);
    if (person.Address.PostCode != null)
      postCode = person.Address.PostCode.ToString();
    else
      postCode = "UNKNOWN";
  }
}

上面提供的代码包含了很多冗余信息——例如,person.Address.PostCode 被提到了两次。代码本身并没有什么错误,只是符号有点太多了。总而言之,我们希望我们的代码能更好地传达以下信息:

  • 如果值为 null,则不应进行进一步的评估;如果值为 null,则这是我们将要处理的值。
  • 如果我们执行某个操作,那么它只会在有效对象上发生。

那么我建议什么呢?我建议我们创建一个流畅的接口,它能在没有任何嵌套的情况下满足上述条件。为此,我们将采用Maybe Monad

对于熟悉 F# 的人来说,Maybe Monad 会以 Option 类型的形式出现。对于 C# 开发者来说,我们只需要假定一个变量可能有一个,或者没有值()。当然,C# 不直接支持这种“有值-无值”的二元性,除非使用 null。这正是我提出下面链式扩展解决方案的原因。

有了

我们的首要任务是简化空值检查,让它们不至于污染我们的代码。为此,我们将定义一个名为 With() 的扩展方法。

public static TResult With<TInput, TResult>(this TInput o, 
       Func<TInput, TResult> evaluator)
       where TResult : class where TInput : class
{
  if (o == null) return null;
  return evaluator(o);
}

上述方法可以附加到任何类型(因为 TInput 实际上是 object)。作为参数,此方法接受一个函数,该函数定义了“链中的下一个值”。如果我们传入 null,我们将返回 null。让我们使用此方法重写第一个示例。

string postCode = this.With(x => person)
                      .With(x => x.Address)
                      .With(x => x.PostCode);

我想,在上面的示例中,我们可以用 Expression<> 替换 Func<> 并尝试获取属性,但我见过这样做,结果代码速度太慢,而且也有一定的局限性——它假定你只处理一个对象,而我的 Maybe 链可以(也确实)引入多个对象。

返回

接下来是另一个语法糖——Return() 方法。此方法将返回“当前”值,就像 Where() 一样,但如果传入的是 null,它将返回我们提供的不同值。你可以将其视为一种“带回退的 Where()”方法。

public static TResult Return<TInput,TResult>(this TInput o, 
       Func<TInput, TResult> evaluator, TResult failureValue) where TInput: class
{
  if (o == null) return failureValue;
  return evaluator(o);
}

所以,现在我们假设,在没有邮政编码的情况下,我们想返回,比如 string.Empty。方法如下:

string postCode = this.With(x => person).With(x => x.Address)
                      .Return(x => x.PostCode, string.Empty);

顺便说一句,你可以重写扩展方法,使 failureValue 也通过 Func<> 计算——尽管我还没有遇到需要这种做法的场景。通常情况下,我们永远不知道链在哪个阶段失败(并返回 null),因此终端的 Return() 通常是一个指示器(可能是 true/falsenull/非 null)。

If & Unless

在调用链中,有时你需要进行与 null 无关的检查。理论上,你可以暂停链并使用 if,或者在某个委托中使用 if,但是……你可以简单地定义一个 If() 扩展方法(如果你愿意,也可以定义一个 Unless())并将其插入链中。

public static TInput If<TInput>(this TInput o, Func<TInput, bool> evaluator) 
       where TInput : class
{
  if (o == null) return null;
  return evaluator(o) ? o : null;
}
 
public static TInput Unless<TInput>(this TInput o, Func<TInput, bool> evaluator)
       where TInput : class
{
  if (o == null) return null;
  return evaluator(o) ? null : o;
}

Do

既然我们在这里开派对,那就再添加一个简单调用委托的方法——仅此而已。当然,这个方法最适合单行调用,而不是评估包含复杂逻辑的 20 行算法。尽管如此,这种调用在实践中非常有用。

public static TInput Do<TInput>(this TInput o, Action<TInput> action) 
       where TInput: class
{
  if (o == null) return null;
  action(o);
  return o;
}

好了,我们完成了:我们有了使邮政编码提取更具可读性所需的基础架构。这是最终结果。

string postCode = this.With(x => person)
    .If(x => HasMedicalRecord(x))
    .With(x => x.Address)
    .Do(x => CheckAddress(x))
    .With(x => x.PostCode)
    .Return(x => x.ToString(), "UNKNOWN");

如你所见,嵌套深度降至零——没有更多的花括号了!

讨论

我在我的 R2P 软件产品中使用了这些 Maybe-monadic-chain-null-extension-methods(你可以随意称呼它们)。这里有一个这些结构在实际使用中的例子。

public override void VisitInvocationExpression(IInvocationExpression expression)
{
  base.VisitInvocationExpression(expression);
  string typeName = this.With(x => expression)
    .With(x => x.InvokedExpression)
    .With(x => x as IReferenceExpression)
    .With(x => x.Reference)
    .With(x => x.Resolve())
    .With(x => x.DeclaredElement)
    .With(x => x.GetContainingType())
    .Return(x => x.CLRName, null);
  this.If(x => Array.IndexOf(types, typeName) != -1)
    .With(x => ExpressionStatementNavigator.GetByExpression(expression))
    .Do(x =>
          {
            var suggestion = new SideEffectSuggestion(typeName);
            var highlightInfo = new HighlightingInfo(
              expression.GetDocumentRange(),
              suggestion);
            context.HighlightingInfos.Add(highlightInfo);
          });
}

我需要指出的是,在任何时候,你都可以停止链并开始一个新的链。你为什么要这样做?嗯,例如,你不能在链内定义共享变量(除非你将其重构为具有类似 Dictionary<string,object> 的参数)。

顺便说一句,我经常发现自己创建额外的、特定于域的方法来插入这个链中。例如:

public static IElement IsWithin<TContainingType>(this IElement self) 
       where TContainingType: class, IElement
{
  if (self == null) return self;
  var owner = self.GetContainingElement<TContainingType>(false);
  return owner == null ? self : null;
}

还有一件事:这种类型的表示法实际上是一种轻微的混淆,因为,我敢肯定你已经猜到了,每个扩展方法的调用在 Reflector 中都会显示为静态方法调用。

public override void VisitInvocationExpression(IInvocationExpression expression)
{
    base.VisitInvocationExpression(expression);
    string typeName = this.With<SideEffectAnalyser, IInvocationExpression>(
    delegate (SideEffectAnalyser x) {
        return expression;
    }).With<IInvocationExpression, ICSharpExpression>(delegate (IInvocationExpression x) {
        return x.InvokedExpression;
    }).With<ICSharpExpression, IReferenceExpression>(delegate (ICSharpExpression x) {
        return (x as IReferenceExpression);
    }).With<IReferenceExpression, IReference>(delegate (IReferenceExpression x) {
        return x.Reference;
    })
    .
    .
    .
    // and so on
}

这种方法很容易扩展——例如,我的一个同事也在他的链中进行了 try-catch 检查。嘿,这有点像 AOP,但没有后构建或动态代理。哦,与 if 语句相比,这些链的性能损失微不足道

就是这样!评论,一如既往,欢迎!

更新 1:我收到了关于如何在此层次结构中传播值类型的提问。这很简单:你所要做的就是创建一个另一个链方法,它不执行空值检查。

public static TResult WithValue<TInput, TResult>(this TInput o, 
       Func<TInput, TResult> evaluator)
       where TInput:struct
{
  return evaluator(o);
}
© . All rights reserved.