我一直在研究选项类型的使用。
这意味着将函数转换为:
Customer GetCustomerById(Int32 customerID) {...}
Customer c = GetCustomerById(619);
DoStuff(c.FirstName, c.LastName);
返回一个 Maybe
类型的选项:
Maybe<Customer> GetCustomerById(Int32 customerID) {...}
在我的非功能语言中,我需要检查返回值是否存在:
Maybe<Customer> c = GetCustomerById(619);
if (c.HasValue)
DoStuff(c.Value.FirstName, c.Value.LastName);
这样做已经足够好了:
- 从函数签名中,你可以知道它是否能返回
null
(而不是引发异常) - 在盲目使用返回值之前,你被提示要检查返回的值
但没有垃圾回收机制
但我不是在 C#、Java 或具有 RAII 的 C++ 中。我在 Delphi 中,这是一种具有手动内存管理的本地语言。我将继续在类似于 C# 的语言中展示代码示例。
在手动内存管理下,我的原始代码如下:
Customer c = GetCustomerById(619);
if (c != nil)
{
try
{
DoStuff(c.FirstName, c.LastName);
}
finally
{
c.Free();
}
}
会被转换成类似以下的内容:
Maybe<Customer> c = GetCustomerById(619);
if (c.HasValue)
{
try
{
DoStuff(c.Value.FirstName, c.Value.LastName);
}
finally
{
c.Value.Free();
}
}
我现在有一个Maybe<>
持有一个无效的引用;它比null更加糟糕,因为现在Maybe
认为它具有有效的内容,并且这些内容确实具有指向内存的指针,但该内存无效。
我已经将可能的NullReferenceException
交换成了随机的数据损坏崩溃错误。
是否有人考虑过这个问题,并有技术来解决它?
为Maybe添加.Free方法
我考虑过向结构体中添加一个名为Free
的方法:
void Free()
{
if (this.HasValue())
{
_hasValue = false;
T oldValue = _value;
_value = null;
oldValue.Free();
}
}
如果人们知道该如何调用它,知道为什么要调用它,以及知道不应该调用什么,那么它就可以起作用。
避免一个危险的错误需要很多微妙的知识,我只是试图使用一个选项类型而引入了这个错误。
当被包装在 Maybe<T>
中的对象实际上是通过一个非规范命名的方法间接销毁时,它也会崩溃:
Maybe<ListItem> item = GetTheListItem();
if item.HasValue then
begin
DoStuffWithItem(item.Value);
item.Value.Delete;
//item still thinks it's valid, but is not
item.Value.Selected := False;
end;
额外内容
Nullable
/Maybe
/Option
类型对于处理没有内置非值的类型(例如记录、整数、字符串,其中没有内置的非值)非常有用。
如果函数返回一个不可为空的值,则没有办法在不使用一些特殊的哨兵值的情况下传达返回结果不存在的信息。
function GetBirthDate(): TDateTime; //returns 0 if there is no birth date
function GetAge(): Cardinal; //returns 4294967295 if there is no age
function GetSpouseName: string; //returns empty string if there is no spouse name
选项用于避免特殊的哨兵值,并向调用者传达实际情况。
function GetBirthDate(): Maybe<TDateTime>;
function GetAge(): Maybe<Integer>;
function GetSpouseName: Maybe<string>;
不仅适用于非空类型
Option
类型也越来越受欢迎,作为避免NullReferenceExceptions
(或EAccessViolation
在地址$00000000处)的一种方式,通过将有物与无物分离。
返回特殊、有时危险的标记值的函数
function GetBirthDate(): TDateTime; //returns 0 if there is no birth date
function GetAge(): Cardinal; //returns 4294967295 if there is no age
function GetSpouseName: string; //returns empty string if there is no spouse name
function GetCustomer: TCustomer; //returns nil if there is no customer
将特殊且有时危险的哨兵值转换为不可能出现的表单:
function GetBirthDate(): Maybe<TDateTime>;
function GetAge(): Maybe<Integer>;
function GetSpouseName: Maybe<string>;
function GetCustomer: Maybe<TCustomer>;
需要调用者意识到函数可能返回无值,并且必须经过检查是否存在。对于已经支持null的类型,Option
让我们有机会尝试阻止人们引起NullReference异常。
在函数式编程语言中,它更加健壮; 返回类型可以构造为不可能返回nil
- 编译器不允许这样做。
在过程式编程语言中,我们能做的最好的事情就是将nil
锁起来,使其无法被接触到。这样一来,调用者的代码更加健壮。
有人可能会说“为什么不告诉开发者永远不要犯错误”:
不好的例子:
customer = GetCustomer();
Print(customer.FirstName);
好:
customer = GetCustomer();
if Assigned(customer)
Print(customer.FirstName);
只要变得更好。
问题是我希望编译器能够捕捉这些错误。我希望错误在第一次发生时就更难发生。我想要一个成功的陷阱。它强制调用者理解函数可能会失败。函数签名本身就解释了该怎么做,并使处理变得容易。
在这种情况下,我们隐式地返回两个值:
- 一个客户
- 指示客户是否真正存在的标志
函数式编程语言中的人们采用了这个概念,而且人们正在试图将其带回过程式语言中,您需要一个新类型来传达该值是否存在。盲目使用它的尝试将导致编译时错误:
customer = GetCustomer();
Print(customer.FirstName); //syntax error: Unknown property or method "FirstName"
额外阅读
如果您想了解在过程式语言中尝试使用函数式Maybe
单子的更多内容,可以查阅以下主题上的更多思考:
- Oracle Java: 厌倦了空指针异常吗?考虑使用Java SE 8的Optional!
- Delphi Sorcery: 永远不要返回nil?也许吧!
- 从C#中移除Null
Nullable<T>
最初旨在允许不可为空的类型(例如字符串、整数、记录)变为伪可空。但它可以接受任何<T>
类型。在我的情况下,我想要一个Maybe<T:class>
,以便编译器能够捕获我可能没有检查可能的nil引用的地方。 - Ian Boyd