因此,众所周知,臭名昭着的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>
和空合并运算符等方面提供了哪些优势(如果有的话)?
答案 0 :(得分:7)
如果您将Option
视为Nullable
的名称不同,那么您绝对正确 - Option
只是Nullable
的参考类型。
Option
模式在您查看as a monad或a 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);
,predictCustomerBehavior
,chooseIncentiveBasedOnPredictedBehavior
)都很昂贵 - 但只有在我们有客户开始时才会发生这些行为!
虽然变得更好 - 经过一些研究后我们意识到我们无法始终预测客户行为。因此,我们更改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
。此外,我们可以利用多态性并创建两个子类型IEnumerable
,Optional
和Some
来表示完整列表和空列表大小写。然后我们的方法可以放弃None
检查,使它们更容易阅读。
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
的许多类型的查询存在的问题是,这可能意味着两件事:
我知道您已经特别询问了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