我一直在研究选项类型的使用。
这意味着转换如下函数:
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或C++的RAII。我在德尔福;具有手动内存管理的母语。我将继续以类似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
换成了随机数据损坏崩溃错误。
有没有人想过这个问题,以及解决它的技巧?
加。免费到也许
我考虑过在结构中添加一个名为Free
的方法:
void Free()
{
if (this.HasValue())
{
_hasValue = false;
T oldValue = _value;
_value = null;
oldValue.Free();
}
}
如果人们称呼它,它就会起作用;并且知道称呼它;知道为什么称呼它;并且知道他们不应该称呼什么。
许多微妙的知识可以避免我只是通过尝试使用选项类型引入的危险错误。
当包裹在Maybe<T>
中的对象实际上通过未命名为规范Free
的方法间接销毁时,它也会分崩离析:
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
(或地址 $000000000 处EAccessViolation
)的方法。
返回特殊(有时是危险的)哨兵值的函数
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
monad的尝试,你可以咨询更多关于这个主题的想法:
- Oracle Java:厌倦了空指针异常?考虑使用 Java SE 8 的可选!
- 德尔菲巫术:永不归零?或!
- 从 C# 中删除空 值
唯一可以保护您免受Value
上任何包裹在Maybe
中的内容的方法是从Maybe
变量中提取值,清除Maybe
内容并使用结果,就像通常使用任何对象引用一样。
像这样:
TMaybe<T> = record
strict private
FValue: T;
public
...
function ExtractValue: T;
end;
function TMaybe<T>.ExtractValue: T;
begin
if not _hasValue then raise Exception.Create('Invalid operation, Maybe type has no value');
Result := FValue;
_hasValue = false;
FValue := Default(T);
end;
然后您将被迫提取值以使用它。
Maybe<ListItem> item = GetTheListItem();
if item.HasValue then
begin
Value := item.ExtractValue;
DoStuffWithItem(Value);
Value.Free;
end;
在 Delphi 中不需要Maybe
,因为对象是引用类型,因此您可以使用nil
对象指针,例如:
type
TCustomer = class
public
customerID: Int32;
FirstName, LastName: string;
end;
function GetCustomerById(customerID: Int32): TCustomer;
begin
...
if (customerID is found) then
Result := ...
else
Result := nil;
end;
var
c: TCustomer;
begin
c := GetCustomerById(619);
if c <> nil then
DoStuff(c.FirstName, c.LastName);
end;
如果函数需要分配一个新对象进行返回,例如:
function GetCustomerById(customerID: Int32): TCustomer;
begin
...
if (customerID is found) then
begin
Result := TCustomer.Create;
...
end else
Result := nil;
...
end;
然后,对于生存期管理,您有两种选择(假设调用方需要获取对象的所有权,因为它不在其他地方拥有)。
1) 使用完对象后,可以调用Free
:
var
c: TCustomer;
begin
c := GetCustomerById(619);
if c <> nil then
try
DoStuff(c.FirstName, c.LastName);
finally
c.Free;
end;
end;
2) 您可以使用引用计数接口:
type
ICustomer = interface
['{2FBD7349-340C-4A4E-AA72-F4AD964A35D2}']
function getCustomerID: Int32;
function getFirstName: string;
function getLastName: string;
property CustomerID: Int32 read getCustomerID;
property FirstName: string read getFirstName;
property LastName: string read getLastName;
end;
TCustomer = class(TInterfacedObject, ICustomer)
public
fCustomerID: Int32;
fFirstName, fLastName: string;
function getCustomerID: Int32;
function getFirstName: string;
function getLastName: string;
end;
function TCustomer.getCustomerID: Int32;
begin
Result := fCustomerID;
end;
function TCustomer.getFirstName: string;
begin
Result := fFirstName;
end;
function TCustomer.getLastName: string;
begin
Result := fLastName;
end;
function GetCustomerById(customerID: Int32): ICustomer;
begin
...
if (customerID is found) then
begin
Result := TCustomer.Create as ICustomer;
...
end else
Result := nil;
end;
var
c: ICustomer;
begin
c := GetCustomerById(619);
if c <> nil then
DoStuff(c.FirstName, c.LastName);
end;