可选的设计模式,优点

Mat*_*ero 3 c# design-patterns nullpointerexception optional nullreferenceexception

因此,众所周知,臭名昭着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; }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

并像这样处理:

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");
}
Run Code Online (Sandbox Code Playgroud)

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

如果他忘了,他会得到NullReferenceException两种方法.

我错过了什么?Optional模式提供了什么样的优点(如果有的话)Nullable<T>和null合并运算符?

Sea*_*ira 8

释放你的心灵

如果你想OptionNullable不同的名字,那么你是完全正确的- Option仅仅是Nullable引用类型.

Option如果您将其视为monad包含一个或零值的专用集合,则该模式更有意义.

Option 作为一个集合

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

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

如果您将空列表传递给DoWork,则没有任何反应:

DoWork(new List<int>());
Run Code Online (Sandbox Code Playgroud)

如果您传递包含一个或多个元素的列表,则会发生以下情况:

DoWork(new List<int>(1));
// 1
Run Code Online (Sandbox Code Playgroud)

让我们将空列表None替换为列表,并在其中包含一个条目Some:

var None = new List<int>();
var Some = new List(1);
Run Code Online (Sandbox Code Playgroud)

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

DoWork(None);

DoWork(Some);
// 1
Run Code Online (Sandbox Code Playgroud)

当然,我们也可以使用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
Run Code Online (Sandbox Code Playgroud)

有趣的旁注:LINQ是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(); }
Run Code Online (Sandbox Code Playgroud)

然后你的代码变成:

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

optionalCustomer
    .Foreach(customer =>
        Console.WriteLine("Customer found: " + customer.ToString()))
    .OrElse(() => Console.WriteLine("Customer not found"));
Run Code Online (Sandbox Code Playgroud)

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

optionalCustomer
    .Map(predictCustomerBehavior)
    .Map(chooseIncentiveBasedOnPredictedBehavior)
    .Foreach(scheduleIncentiveMessage);
Run Code Online (Sandbox Code Playgroud)

所有这些行动(predictCustomerBehavior,chooseIncentiveBasedOnPredictedBehavior,scheduleIncentiveMessage)是昂贵的-但如果我们有一个客户开始与他们才会发生!

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

optionalCustomer
    .FlatMap(predictCustomerBehavior)
    .Map(chooseIncentiveBasedOnPredictedBehavior)
    .Foreach(scheduleIncentiveMessage);
Run Code Online (Sandbox Code Playgroud)

其定义为:

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();
}
Run Code Online (Sandbox Code Playgroud)

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

进一步可能的改进

为了获得更多的实用性,Optional我们应该真正实现它IEnumerable.另外,我们可以利用多态性并创建两个子类型Optional,SomeNone表示完整列表和空列表情况.然后我们的方法可以放弃IsPresent检查,使它们更容易阅读.

TL; DR

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

someList
    .Where(cheapOp1)
    .SkipWhile(cheapOp2)
    .GroupBy(expensiveOp)
    .Select(expensiveProjection);
Run Code Online (Sandbox Code Playgroud)

Optional,当被视为一个或零值集合时,提供了类似的好处(并且没有理由它无法实现,IEnumerable因此LINQ方法也适用于它):

someOptional
    .FlatMap(expensiveOp1)
    .Filter(expensiveOp2)
    .GetOrElse(generateDefaultValue);
Run Code Online (Sandbox Code Playgroud)

进一步建议阅读