delphi - 这是 Rio 上 System.Net.HttpClient 中的错误吗?
问题描述
这是在 Delphi Rio 中找到的功能System.Net.HttpClient
THTTPClientHelper = class helper for THTTPClient
....
procedure THTTPClientHelper.SetExt(const Value);
var
{$IFDEF AUTOREFCOUNT}
LRelease: Boolean;
{$ENDIF}
LExt: THTTPClientExt;
begin
if FHTTPClientList = nil then
Exit;
TMonitor.Enter(FHTTPClientList);
try
{$IFDEF AUTOREFCOUNT}
LRelease := not FHTTPClientList.ContainsKey(Self);
{$ENDIF}
LExt := THTTPClientExt(Value);
FHTTPClientList.AddOrSetValue(Self, LExt);
{$IFDEF AUTOREFCOUNT}
if LRelease then __ObjRelease;
{$ENDIF}
finally
TMonitor.Exit(FHTTPClientList);
end;
end;
这家伙想在LRelease
这里做什么?
{$IFDEF AUTOREFCOUNT}
LRelease := not FHTTPClientList.ContainsKey(Self);
{$ENDIF}
LExt := THTTPClientExt(Value);
FHTTPClientList.AddOrSetValue(Self, LExt);
{$IFDEF AUTOREFCOUNT}
if LRelease then __ObjRelease;
{$ENDIF}
因此,如果FHTTPClientList
不包含将其THTTPClient
添加到 中FHTTPClientList
,然后将其引用计数减少一。为什么将它的引用计数减少一个?THTTPClient
仍然活着并使用了为什么要打破它的引用计数?他们是这里的一个错误,也许那个人打错了,但我不明白他最初想做什么......
有关如何从字典中删除项目的信息:
procedure THTTPClientHelper.RemoveExt;
begin
if FHTTPClientList = nil then
Exit;
TMonitor.Enter(FHTTPClientList);
try
FHTTPClientList.Remove(Self);
finally
TMonitor.Exit(FHTTPClientList);
end;
end;
解决方案
上述代码中 ARC 编译器的手动引用计数的目的是模拟具有弱引用的字典。Delphi 泛型集合由泛型数组支持,该数组将持有对添加到 ARC 编译器集合中的任何对象的强引用。
有几种方法可以实现弱引用 - 使用指针,在对象被声明为弱引用的对象周围使用包装器,并在适当的位置手动引用计数。
使用指针会失去类型安全性,包装器需要更多的代码,所以我猜上述代码的作者选择了手动引用计数。那部分没有错。
但是,正如您所注意到的,该代码中有一些可疑之处 - 虽然SetExt
正确编写了例程,但RemoveExt
有一个错误导致稍后崩溃。
让我们在 ARC 编译器的上下文中查看代码(为简洁起见,我将省略编译器指令和不相关的代码):
由于将对象添加到集合(数组)中会增加引用计数,为了实现弱引用,我们必须减少添加的对象实例的引用计数——这样实例的引用计数在存储到集合后将保持不变。接下来,当我们从此类集合中删除对象时,我们必须恢复引用计数平衡并增加引用计数。此外,我们必须确保对象在销毁之前从此类集合中删除 - 这样做的好地方是析构函数。
添加到收藏:
LRelease := not FHTTPClientList.ContainsKey(Self);
FHTTPClientList.AddOrSetValue(Self, LExt);
if LRelease then __ObjRelease;
我们将对象添加到集合中,然后在集合持有对我们对象的强引用后,我们可以释放它的引用计数。如果对象已经在集合中,这意味着它的引用计数已经减少了,我们不能再减少它——这就是LRelease
标志的目的。
从集合中移除:
if FHTTPClientList.ContainsKey(Self) then
begin
__ObjAddRef;
FHTTPClientList.Remove(Self);
end;
如果对象在集合中,我们必须在从集合中删除对象之前恢复平衡并增加引用计数。这是RemoveExt
方法中缺少的部分。
确保对象在销毁时不在列表中:
destructor THTTPClient.Destroy;
begin
RemoveExt;
inherited;
end;
注意:为了使此类伪造的弱集合正常工作,必须仅通过上述负责平衡引用计数的方法添加和删除项目。使用任何其他原始收集方法Clear
都会导致引用计数损坏。
错误与否?
在System.Net.HttpClient
代码中,破坏 RemoveExt
方法仅在析构函数中调用,也是FHTTPClientList
私有变量,不会以任何其他方式更改。乍一看,该代码可以正常工作,但实际上包含相当微妙的错误。
为了揭示真正的错误,我们需要涵盖可能的使用场景,从几个既定事实开始:
- 只有改变内容和
FHTTPClientList
字典中项目的引用计数的方法才是SetExt
和RemoveExt
方法 SetExt
方法是正确的RemoveExt
不调用的损坏方法__ObjAddRef
仅在THTTPClient
析构函数中调用,这就是这个微妙错误的来源。
当对任何特定对象实例调用析构函数时,这意味着对象实例已达到其生命周期,并且任何后续引用计数触发器(在析构函数执行期间)都不会影响代码的正确性。
这是通过应用变量更改其值来确保的objDestroyingFlag
,FRefCount
并且任何进一步的计数增加/减少都不会再导致0
启动销毁过程的特殊值 - 因此对象是安全的并且不会被销毁两次。
在上面的代码中,当THTTPClient
调用析构函数时,这意味着对对象实例的最后一个强引用已经超出范围或被设置为nil
,此时唯一剩余的可以触发引用计数机制的活动引用是FHTTPClientList
. 如前所述,该引用已通过RemoveExt
方法(是否损坏)清除,这无关紧要。一切正常。
但是,代码的作者忘记了一个很小的事情——DisposeOf
触发析构函数的方法,但此时对象实例还没有达到它的引用计数生命周期。换句话说 - 如果析构函数由 调用DisposeOf
,任何后续引用计数触发器都必须平衡,因为在析构函数链调用完成后,仍然存在对将触发引用计数机制的对象的实时引用。如果我们在那个时候打破计数,结果将是灾难性的。
由于THTTPClient
不是TComponent
后代,因此 DisposeOf
很容易进行疏忽并忘记某人,某处DipsoseOf
无论如何都可以调用这样的变量 - 例如,如果您制作拥有的THTTPClient
实例列表,清除此类列表将调用DisposeOf
它们并愉快地打破它们的引用计数,因为RemoveExt
方法最终被打破。
结论:是的,这是一个BUG。
推荐阅读
- java - 使用 Java Spring Boot 创建 MySQL JSON 列
- chromium - 使用 Puppeteer 移除 kiosk 模式 Chromium 中的灰条
- loops - 谷歌表格循环/迭代仅可见的表格
- javascript - 单击更改颜色
- c++ - 如何在考虑对象切片的同时通过传入单个 Base 对象来打印数组中的对象?
- python - AttributeError: 'bool' 对象没有属性
- paho - pythonanywher 上的 paho-mqtt 身份验证错误
- r - rbind 具有不同名称的数据帧
- c# - 带有复选框布尔值和分页的 ViewModel
- reactjs - 在 clojurescript 中,如何评估列表