【问题标题】:Optional Design Pattern, Advantages [closed]可选的设计模式,优势[关闭]
【发布时间】:2014-07-30 18:04:02
【问题描述】:

因此,众所周知,臭名昭著的NullReferenceException 是软件产品中最常见的例外。我一直在阅读一些文章,发现自己采用 Optional 方法。

其目的是围绕可空值创建某种封装

public sealed class Optional<T> where T : class {

    private T value;

    private Optional(T value) {
        this.value = value;
    }

    //Used to create an empty container
    public static Optional<T> Empty() {
        return new Optional(null);
    }

    //Used to create a container with a non-null value
    public static Optional<T> For(T value) {
        return new Optional(value);
    }

    //Used to check if the container holds a non-null value
    public bool IsPresent {
        get { return value != null; }
    }

    //Retrieves the non-null value
    public T Value {
        get { return value; }
    }
}

之后,现在的可选值可以这样返回:

public Optional<ICustomer> FindCustomerByName(string name)
{
    ICustomer customer = null;

    // Code to find the customer in database

    if(customer != null) {
        return Optional.Of(customer);
    } else {
        return Optional.Empty();
    }
}

并像这样处理:

Optional<ICustomer> optionalCustomer = repository.FindCustomerByName("Matt");

if(optionalCustomer.IsPresent) {
     ICustomer foundCustomer = optionalCustomer.Value;
     Console.WriteLine("Customer found: " + customer.ToString());
} else {
     Console.WriteLine("Customer not found");
}

我没有看到任何改进,只是改变了复杂性。 程序员必须记住检查值是否为IsPresent,就像他必须记住检查是否为value != null

如果他忘记了,他会在这两种方法中得到NullReferenceException

我错过了什么?与 Nullable&lt;T&gt; 和 null 合并运算符相比,Optional 模式有哪些优势(如果有)?

【问题讨论】:

  • 优点是可读性。如果您觉得代码更好理解,请使用该方法。有时,空检查会让开发人员感到害怕,并将其包裹在检查中以防万一,但是如果您有明显故意的 Optional .. customer.IsPresent,则可能更容易遵循。
  • 这段代码就像一半 Java 和一半 C#,在任何一种语言中都会出现许多编译错误...
  • 我不同意@Hammerstein。我发现包装器的可读性远低于直接使用!= null。对于发现此类错误,我更倾向于使用 ReSharper 之类的工具,它可以执行代码分析来查找问题。这允许装饰方法可能返回 null 并带有指示此类的属性。我确信还有其他工具以类似的方式工作。另一种选择是使用Try... 方法,例如Dictionary.TryGetValue
  • 您在此处介绍的内容没有任何优势 - 使用 Option/Maybe 的全部意义在于您无需检查值是否存在。为此,您可以定义return\map\bind 运算符,然后要求客户端在尝试解压缩内部值时必须提供默认值。您也不会将T 限制为引用类型,而是统一对待T 的所有可能类型。
  • LOL java.... 我已经用 C# 编码多年了,我得到的NullReferenceExceptions 的数量可以用右手的手指数出来。完全同意@JohnSaunders,找到并修复该死的错误,而不是创建无穷无尽的抽象和模式来隐藏它。

标签: c# design-patterns nullpointerexception nullreferenceexception optional


【解决方案1】:

解放你的思想

如果您将 Option 视为 Nullable 的名称不同,那么您是绝对正确的 - 对于引用类型,Option 只是 Nullable

Option 模式在您查看as a monada specialized collection that contain either one or zero values 时更有意义。

Option 作为一个集合

考虑一个简单的foreach 循环,其中的列表不能是null

public void DoWork<T>(List<T> someList) {
    foreach (var el in someList) {
        Console.WriteLine(el);
    }
}

如果您将空列表传递给DoWork,则不会发生任何事情:

DoWork(new List<int>());

如果你传递一个包含一个或多个元素的列表,就会发生工作:

DoWork(new List<int>(1));
// 1

让我们将空列表别名为None,并将其中包含一个条目的列表别名为Some

var None = new List<int>();
var Some = new List(1);

我们可以将这些变量传递给DoWork,我们会得到与以前相同的行为:

DoWork(None);

DoWork(Some);
// 1

当然,我们也可以使用LINQ扩展方法:

Some.Where(x => x > 0).Select(x => x * 2);
// List(2)
// Some -> Transform Function(s) -> another Some

None.Where(x => x > 0).Select(x => x * 2);
// List()
// None -> None

Some.Where(x => x > 100).Select(x => x * 2);
// List() aka None
// Some -> A Transform that eliminates the element -> None

有趣的旁注:LINQ is monadic.

等等,刚刚发生了什么?

通过将我们想要的值包装在一个列表中,我们突然能够只对这个值应用一个操作,如果我们首先确实有一个值!

扩展Optional

考虑到这一点,让我们向Optional 添加一些方法,让我们像处理一个集合一样使用它(或者,我们可以将其设为IEnumerable 的专用版本,只允许一个条目) :

// map makes it easy to work with pure functions
public Optional<TOut> Map<TIn, TOut>(Func<TIn, TOut> f) where TIn : T {
    return IsPresent ? Optional.For(f(value)) : Empty();
}

// foreach is for side-effects
public Optional<T> Foreach(Action<T> f) {
    if (IsPresent) f(value);
    return this;
}

// getOrElse for defaults
public T GetOrElse(Func<T> f) {
    return IsPresent ? value : f();
}

public T GetOrElse(T defaultValue) { return IsPresent ? value: defaultValue; }

// orElse for taking actions when dealing with `None`
public void OrElse(Action<T> f) { if (!IsPresent) f(); }

那么你的代码就变成了:

Optional<ICustomer> optionalCustomer = repository.FindCustomerByName("Matt");

optionalCustomer
    .Foreach(customer =>
        Console.WriteLine("Customer found: " + customer.ToString()))
    .OrElse(() => Console.WriteLine("Customer not found"));

那里节省不多,对吧?还有两个匿名函数——那么我们为什么要这样做呢?因为,就像 LINQ 一样,它使我们能够建立一个行为链,只要我们有我们需要的输入,它就会执行。例如:

optionalCustomer
    .Map(predictCustomerBehavior)
    .Map(chooseIncentiveBasedOnPredictedBehavior)
    .Foreach(scheduleIncentiveMessage);

这些操作(predictCustomerBehaviorchooseIncentiveBasedOnPredictedBehaviorscheduleIncentiveMessage)中的每一个都是昂贵的 - 但只有在我们有客户开始时才会发生!

但它变得更好 - 经过一些研究,我们意识到我们不能总是预测客户行为。因此,我们将predictCustomerBehavior 的签名更改为返回Optional&lt;CustomerBehaviorPrediction&gt;,并将链中的第二个Map 调用更改为FlatMap

optionalCustomer
    .FlatMap(predictCustomerBehavior)
    .Map(chooseIncentiveBasedOnPredictedBehavior)
    .Foreach(scheduleIncentiveMessage);

定义为:

public Optional<TOut> FlatMap<TIn, TOut>(Func<TIn, Optional<TOut>> f) where TIn : T {
    var Optional<Optional<TOut>> result = Map(f)
    return result.IsPresent ? result.value : Empty();
}

这开始看起来很像 LINQ(例如FlatMap -> Flatten)。

进一步可能的改进

为了从Optional 中获得更多实用性,我们应该真正实现IEnumerable。此外,我们可以利用多态性,创建OptionalSomeNone 两个子类型来表示完整列表和空列表情况。然后我们的方法可以删除IsPresent 检查,使它们更易于阅读。

TL;DR

LINQ 对于昂贵操作的优势是显而易见的:

someList
    .Where(cheapOp1)
    .SkipWhile(cheapOp2)
    .GroupBy(expensiveOp)
    .Select(expensiveProjection);

Optional,当被视为 collection of one or zero values 时提供了类似的好处(并且没有理由不能实现 IEnumerable 以便 LINQ 方法也可以处理它):

someOptional
    .FlatMap(expensiveOp1)
    .Filter(expensiveOp2)
    .GetOrElse(generateDefaultValue);

进一步建议阅读

【讨论】:

    【解决方案2】:

    如果你使用这样的东西可能会更有意义

    interface ICustomer {
    
        String name { get; }
    }
    
    public class OptionalCustomer : ICustomer {
    
         public OptionalCustomer (ICustomer value) {
              this.value = value;
         }
         public static OptionalCustomer Empty() {
              return new OptionalCustomer(null);
         }
    
         ICustomer value;
    
         public String name { get { 
             if (value == null ) {
                 return "No customer found";
             }
             return value.Name;
          }
        }
    
    }
    

    现在,如果您传递“空”可选客户对象,您仍然可以调用 .Name 属性(不获取空指针)

    【讨论】:

      【解决方案3】:

      Optional 的优点是你知道某些东西可能不存在。

      许多返回null 的查询类型的问题在于,这可能意味着两件事:

      1. 查询未返回结果
      2. 查询返回值为空的结果。

      我知道您专门询问 C#,但 Java 刚刚在 Java 8 中引入了Optionals,因此有很多关于它的文章,所以我将使用 Java 作为示例。但这与 C# 中的想法完全一样:

      考虑 Java Map.get(key) 方法

      Object value = map.get(key);
      if(value ==null){
          //is there an entry in the map key =>null or does key not exist?
      }
      

      要解决这个问题,您必须有一个额外的方法containsKey( k)

      使用 optional,你只需要一种方法

       Optional<Object> result = map.get(key);
       if(result.isPresent()){
          Object value = result.get();
          //if value is null, then we know that key =>null
       }
      

      更多信息请参阅这篇 Java 文章:http://www.oracle.com/technetwork/articles/java/java8-optional-2175753.html

      【讨论】:

        【解决方案4】:

        你的意思是:Null Object pattern

        在 cmets 中链接到我的文章包含一个解释该编程工具的结论部分。

        ... Optional 的目的不是替换代码库中的每一个空引用,而是帮助设计更好的 API,在这些 API 中——只需读取方法的签名——用户就可以判断是否期望一个可选值。 .... 处理值的缺失;因此,您可以保护您的代码免受意外空指针异常的影响。

        不管怎样,让它崩溃,找到原因。如果您不想无休止地嵌入 if 语句而不是使用实现模式 Guard Clause 模式,它说明如下:

        虽然程序有主流程,但某些情况需要偏离主流程 主流。守卫子句是一种表达简单和局部例外的方式 具有纯粹局部后果的情况。

        【讨论】:

        • 不,这与空对象模式不同,空对象模式是一种提供默认值的方式。有关更多信息,请参阅此oracle.com/technetwork/articles/java/…
        • 请编辑您的答案以包含链接中列出的相关优势。仅链接的答案是不可以的。
        猜你喜欢
        • 1970-01-01
        • 2012-10-05
        • 1970-01-01
        • 1970-01-01
        • 2020-01-16
        • 2012-09-27
        • 2010-10-11
        • 1970-01-01
        • 2011-01-20
        相关资源
        最近更新 更多