可选的设计模式,优点

时间:2014-07-30 18:04:02

标签: c# design-patterns nullpointerexception nullreferenceexception optional

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

它的目标是围绕可以为空的值创建某种封装

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<T>和空合并运算符等方面提供了哪些优势(如果有的话)?

4 个答案:

答案 0 :(得分:7)

解放思绪

如果您将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添加一些方法让我们使用它就好像它是一个集合(或者,我们可以使它成为{{1的专用版本只允许一个条目):

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(); }

那里没有多少积蓄,对吧?还有两个匿名函数 - 那我们为什么要这样做呢?因为,就像LINQ一样,它使我们能够建立一个只有在我们需要输入时才执行的行为链。例如:

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

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

这些操作中的每一项(optionalCustomer .Map(predictCustomerBehavior) .Map(chooseIncentiveBasedOnPredictedBehavior) .Foreach(scheduleIncentiveMessage); predictCustomerBehaviorchooseIncentiveBasedOnPredictedBehavior)都很昂贵 - 但只有在我们有客户开始时才会发生这些行为!

虽然变得更好 - 经过一些研究后我们意识到我们无法始终预测客户行为。因此,我们更改scheduleIncentiveMessage的签名以返回predictCustomerBehavior并将链中的第二个Optional<CustomerBehaviorPrediction>调用更改为Map

FlatMap

定义为:

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

这开始看起来很像LINQ(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(); } - &gt; FlatMap,例如)。

进一步可能的改进

为了从Flatten中获得更多效用,我们应该真正实现Optional。此外,我们可以利用多态性并创建两个子类型IEnumerableOptionalSome来表示完整列表和空列表大小写。然后我们的方法可以放弃None检查,使它们更容易阅读。

TL; DR

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

IsPresent

someList .Where(cheapOp1) .SkipWhile(cheapOp2) .GroupBy(expensiveOp) .Select(expensiveProjection); ,当被视为collection of one or zero values时,提供了类似的好处(并且没有理由不能实现Optional,以便LINQ方法可以使用它也是):

IEnumerable

进一步建议阅读

答案 1 :(得分:1)

您的意思是:Null Object pattern

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

  

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

无论如何,让它崩溃并找到原因。如果您不希望无限嵌入if语句而不是使用实现模式Guard Clause模式,该模式说明如下:

虽然程序有主流,但有些情况需要偏离 主流。保护条款是表达简单和本地特殊情况的一种方式 纯粹地方后果的情况。

答案 2 :(得分:1)

如果你使用像这样的东西,它会更有意义

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属性(不会获得nullpoin)

答案 3 :(得分:1)

Optional的优点是你知道是否存在某些东西。

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

  1. 查询未返回结果
  2. 查询返回值为null的结果。
  3. 我知道您已经特别询问了C#,但Java刚刚在Java 8中引入了Optional,因此有很多关于它的文章,所以我将以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<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