当Option类型内部的对象被销毁时会发生什么?

7
我是一名有用的助手,可以为您翻译文本。

我一直在研究选项类型的使用。

这意味着将函数转换为:

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单子的更多内容,可以查阅以下主题上的更多思考:


@DavidHeffernan 这更像是 Swift 的可选项。 - Dalija Prasnikar
1
@DavidHeffernan Nullable和Optional具有重叠的功能。但也存在一些微妙的差异。在这种情况下,不仅仅是关于将null作为潜在值,还可以防止意外使用null值-增加了一层额外的保护,这是Delphi编译器默认未提供的。 - Dalija Prasnikar
@DavidHeffernan 就像我说的,有一些微妙的差别。例如,Swift Optional 实际上是 Nullable,但具有完全的保护,Ian 寻求的是这种保护。我不能说 C# Nullable 的确切行为是什么,因为我不使用 C#。 - Dalija Prasnikar
@DalijaPrasnikar C#中的Nullable<T>最初旨在允许不可为空的类型(例如字符串、整数、记录)变为伪可空。但它可以接受任何<T>类型。在我的情况下,我想要一个Maybe<T:class>,以便编译器能够捕获我可能没有检查可能的nil引用的地方。 - Ian Boyd
3
C#,Java和Delphi中的引用类型已经是可空的,这就是为什么人们正在尝试在Java,C#和Delphi中实现“Maybe”的原因。我们并不是要向引用类型添加可空性;你完全误解了问题领域。 - Ian Boyd
显示剩余3条评论
2个回答

5

唯一能够保护您免受在Maybe包装的Value上调用任何内容的影响,就是从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;

3

在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;

"因此,您可以使用nil对象指针",这就是我们想要摆脱的东西;人们经常忘记检查nil。" - Ian Boyd
这在Delphi中很难避免,因为classinterface类型是引用类型。因此,除非您对所有内容都切换到record(类似于C/C++中的struct),否则您必须处理nil。另一种选择是使GetCustomerById()永远不返回nil,如果未找到customerID,则抛出异常。 - Remy Lebeau
代码在非异常情况下不应抛出异常。一种改进方法是TryGetCustomerByID(customerID)。这样做的好处是调用者被迫认识到没有返回值的现实。Maybe<T:class>是另一种强制调用者认识到没有返回值的方式。C#,Java和C ++也有“nil”引用的现实;这就是为什么我的问题与语言无关。 C#和Java通过垃圾回收已经解决了我的问题。C ++以及Delphi具有手动分配和空值。 - Ian Boyd
@IanBoyd:C++通过RAII和智能指针处理自动内存管理。Delphi使用ARC(自动引用计数)处理自动内存管理,但不适用于对象,除非你使用接口或者nextgen编译器(iOS、Android、Linux)。 - Remy Lebeau

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接