学海无涯: Delphi的接口陷阱

本文详细探讨了Delphi中接口的两大陷阱:类型转换陷阱和生存期管理陷阱,并提供了相应的解决方案,包括使用TList管理和避免非法地址访问错误的技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

http://server-pcc/Lists/Discussion5/DispForm.aspx?ID=20&Source=http%3A%2F%2Fserver%2Dpcc%2FLists%2FDiscussion5%2FAllItems%2Easpx  转自:公司论坛

 

 

张贴者: 2HC-18 贾延平
张贴时间: 2004-9-14 16:18
主题:
Delphi的接口陷阱
文本:
Delphi的接口陷阱
现在我所知的有两大陷阱:
陷阱一、接口的类型转换陷阱
a) 不能把一个对象引用强制转换成这个引用的类型没有声明实现的接口,即使这个对象实际实现了这个接口(呵呵,优点拗口)。
b) 当把一个对象变量赋给一个接口变量,在把这个接口变量赋还给对象变量时,这个对象变量的地址已经变了,也就是不再是原来的对象了,而是指向一个错误的地址。
例如:
I1 = interface
    function Do: Boolean;
end;
TC1 = Class
    ATT1: Integer;
end;
TC2 = Class(TC1, I1)
    ATT2: Integer;
    function Do: Boolean;
end;
Intf1: I1;
OBJ1: TC!;
OBJ2: TC2;
OBJ2 := TC2.Create;
OBJ1 := OBJ2.
I1(OBJ2).DO;正确。
I1(OBJ1).DO;编译失败。
因为OBJ1的类型TC1没有声明实现I1所以不能转换成I1,即使OBJ1确实实现了I1。
还有,如果把对象转为接口再转回来也会有问题。
OBJ2 := TC2.Create;
OBJ2.ATT1 := 0;
Intf1 := OBJ2;//正确。
OBJ2 := Intf1;
TC2(Intf1).ATT1 := 0; //运行期非法地址访问错误。
OBJ2.ATT1 := 0; //运行期非法地址访问错误。
也就是,从对象引用转换成指针引用后,地址改变了,但是由指针引用再转回对象引用时地址没有变回来(Delphi的bug?)。
陷阱二、接口的生存期管理
依据我的常识(此处是编程常识,不是Delphi使用常识)来讲,我认为接口是不需要生存期管理的,因为接口根本不可能生成真正的对象。但是Delphi却又一次打击了我的常识(咦,为什么要说“又”呢?),它的接口是有生存期的,而且必须实现以下三个方法:
    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
    function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
每次都要实现这三个方法是比较麻烦的,而且更重要的是,我不知道Delphi什么时候用以及怎么用这三个方法?所以我也不知道怎么实现这三个方法。
如果不想自己实现这三个方法,你可以使用TComponent。因为TComponent已经实现了这三个方法,所以可以从它继承,就不用实现这三个方法了。
这样就可以放心使用了吗?答案是否定的。因为Delphi在你把接口变量置为nil时偷偷的(因为很出乎我的意料)调用了_Release。
function _IntfClear(var Dest: IInterface): Pointer;
var
  P: Pointer;
begin
  Result := @Dest;
  if Dest <> nil then
  begin
    P := Pointer(Dest);
    Pointer(Dest) := nil;
    IInterface(P)._Release;
  end;
end;
而_Release时又做了什么呢?
function TComponent._Release: Integer;
begin
  if FVCLComObject = nil then
    Result := -1   // -1 indicates no reference counting is taking place
  else
    Result := IVCLComObject(FVCLComObject)._Release;
end;
不是Com对象的话,就什么也没作。我们作的不是Com对象,是不是就没有任何问题了呢?答案依然是否定的,考虑如下情况:
OBJ2 := TC2.Create;
try
Intf1 := OBJ2;
Intf1.DO;
Finally
 OBJ2.Free;
 Intf1 := nil;
End;
会怎么样呢?会出非法地址访问错误。为什么?上面说过把接口引用设为nil时,会调用_IntfClear,而_IntfClear又会调用对象的_Release,而这时这个对象已经释放了,自然就出非法地址访问错误啦。
有人说多此一举吗,接口引用只是个地址,没必要手动设为nil。
OBJ2 := TC2.Create;
try
Intf1 := OBJ2;
Intf1.DO;
Finally
 OBJ2.Free;
End;
结果可能还会出你的意料,还是非法地址访问错误。为什么?因为Delphi编译器耍了个小聪明,它认为你忘记把这个地址引用置为nil了,所以你会自动给你加上,看来Delphi编译器聪明过头了。
怎么解决呢?
方法1,先把接口引用置为nil,再释放对象。
 Intf1 := nil;
 OBJ2.Free;
方法2,把接口引用强制转成指针类型再置为nil。
 Pointer(Intf1) := nil;
此时相当于直接把地址清零,不会调用_IntfClear。
我倾向于使用第二种方法,这样你就不用考虑先释放谁的问题了。而且有些设计模式中你可能只持有接口引用,而且你也不知道引用的对象什么时候释放,此时就必须使用方法2。
例如考虑Composite模式。
TComposite = class(TComponent, I1)
Private
 interList: TXContainer;//一个容器类,存放“叶子”的接口引用。
Public
 Procedure Add (AIntf: I1);
 function DO: Boolean;
End;
它应该释放它的“叶子”吗?显然不是,那“叶子”是不是一定会晚于这个“合成对象”对象释放呢?我想也不一定吧。如果强制这样规定的话,就失去很多的灵活性。所以我们肯定想这些接口引用置nil时,不会和原对象发生什么关系,以免对象被释放后出非法地址访问错误。考虑使用什么容器呢?array?TList?TInterfaceList?
首先想到肯定是TInterfaceList了,因为我们是要容纳的就是接口。但是对他进行Free时,它会把它所有容纳的接口置为nil,这正是我们不想要的。或者我们可以在Free之前先把它存储的接口引用转为指针再置为nil。
  for I := 0 to interList.Count -1 do
Pointer(interList.Items[i]) := nil;
可惜的是,编译错误“[Error] XXXX.pas(XX): Left side cannot be assigned to”。
然后我们试一下array。
interList: Array of I1;
动态数组是不要释放的。好像很好用,但是编译器释放它时还是会对每个元素置为nil的,而且是作为接口,仍然有非法地址访问错误的可能。可以这样
  for i := Low(arr) to High(arr) do
Pointer(arr[i]) := nil;
但是这毕竟是违反编码习惯的,而且每次使用都要记得作,不记得作也可能不会马上出错,所以有可能成为隐患。
最后,就是使用TList。不过TList中是指针,所以Add时必须这样
procedure XXX.Add(AIntf: I1)
Begin
 InterList.Add(Pointer(AIntf));
End;
由于它本来就保存的是指针,所以释放时也不需要特殊处理。
好像比较完美,但是还是有一个陷阱,如果你写了这样的代码会怎么样呢?
interList.Add(TC2.Create);
或者
Obj2 := TC2.Create;
interList.Add(Obj2);
错!因为保存的是纯粹的指针,所以转化为接口时,对象引用到接口引用的地址转换没有进行(它也不知道如何进行),所以调用接口声明的方法时就又是一个非法地址访问错误。只能这么写:
interList.Add(Pointer(I1(TC2.Create)));
虽然有些麻烦,相比之下这已是最佳方案(我所知的)了。因为你如果你忘记转会在第一次调用时就出错,比较容易发现错误(相比于使用array)。
所以我建议:
1, 使用Tlist来管理接口引用。
2, 增加时要把对象转型成Interface再转型成Pointer,如果本来就是接口引用的话直接转为Pointer即可。
3, 对于没有使用Tlist来管理的接口引用。对于接口的引用要用下面的方法手动置为nil:Pointer(IntfRef):= nil;
另外,TInterfacedObject也实现了IInterface的三个方法。所以从它继承也可以省去自己实现这三个方法的麻烦。但是它的_Release是这样实现的:
function TInterfacedObject._Release: Integer;
begin
  Result := InterlockedDecrement(FRefCount);
  if Result = 0 then
    Destroy;
end;
我们前边说过,接口引用的置nil会调用接口的_Release,所以有可能会把对象给释放掉,我当时可是被它吓了一跳,多亏我没用过它。也就是这样实现下比从TComponent继承带来更大的麻烦。除非特殊用途,不建议使用。
上面对接口的所有讨论,只限于普通使用的语言级的接口,没有讨论Com+接口。Delphi的这些诡异的接口的实现,和它是从Com+接口发展而来是有很大关系的,也就是由于背负了过重的历史包袱而作出的妥协的结果。
 

标题基于SpringBoot+Vue的学生交流互助平台研究AI更换标题第1章引言介绍学生交流互助平台的研究背景、意义、现状、方法与创新点。1.1研究背景与意义分析学生交流互助平台在当前教育环境下的需求及其重要性。1.2国内外研究现状综述国内外在学生交流互助平台方面的研究进展与实践应用。1.3研究方法与创新点概述本研究采用的方法论、技术路线及预期的创新成果。第2章相关理论阐述SpringBoot与Vue框架的理论基础及在学生交流互助平台中的应用。2.1SpringBoot框架概述介绍SpringBoot框架的核心思想、特点及优势。2.2Vue框架概述阐述Vue框架的基本原理、组件化开发思想及与前端的交互机制。2.3SpringBoot与Vue的整合应用探讨SpringBoot与Vue在学生交流互助平台中的整合方式及优势。第3章平台需求分析深入分析学生交流互助平台的功能需求、非功能需求及用户体验要求。3.1功能需求分析详细阐述平台的各项功能需求,如用户管理、信息交流、互助学习等。3.2非功能需求分析对平台的性能、安全性、可扩展性等非功能需求进行分析。3.3用户体验要求从用户角度出发,提出平台在易用性、美观性等方面的要求。第4章平台设计与实现具体描述学生交流互助平台的架构设计、功能实现及前后端交互细节。4.1平台架构设计给出平台的整体架构设计,包括前后端分离、微服务架构等思想的应用。4.2功能模块实现详细阐述各个功能模块的实现过程,如用户登录注册、信息发布与查看、在线交流等。4.3前后端交互细节介绍前后端数据交互的方式、接口设计及数据传输过程中的安全问题。第5章平台测试与优化对平台进行全面的测试,发现并解决潜在问题,同时进行优化以提高性能。5.1测试环境与方案介绍测试环境的搭建及所采用的测试方案,包括单元测试、集成测试等。5.2测试结果分析对测试结果进行详细分析,找出问题的根源并
内容概要:本文详细介绍了一个基于灰狼优化算法(GWO)优化的卷积双向长短期记忆神经网络(CNN-BiLSTM)融合注意力机制的多变量多步时间序列预测项目。该项目旨在解决传统时序预测方法难以捕捉非线性、复杂时序依赖关系的问题,通过融合CNN的空间特征提取、BiLSTM的时序建模能力及注意力机制的动态权重调节能力,实现对多变量多步时间序列的精准预测。项目不仅涵盖了数据预处理、模型构建与训练、性能评估,还包括了GUI界面的设计与实现。此外,文章还讨论了模型的部署、应用领域及其未来改进方向。 适合人群:具备一定编程基础,特别是对深度学习、时间序列预测及优化算法有一定了解的研发人员和数据科学家。 使用场景及目标:①用于智能电网负荷预测、金融市场多资产价格预测、环境气象多参数预报、智能制造设备状态监测与预测维护、交通流量预测与智慧交通管理、医疗健康多指标预测等领域;②提升多变量多步时间序列预测精度,优化资源调度和风险管控;③实现自动化超参数优化,降低人工调参成本,提高模型训练效率;④增强模型对复杂时序数据特征的学习能力,促进智能决策支持应用。 阅读建议:此资源不仅提供了详细的代码实现和模型架构解析,还深入探讨了模型优化和实际应用中的挑战与解决方案。因此,在学习过程中,建议结合理论与实践,逐步理解各个模块的功能和实现细节,并尝试在自己的项目中应用这些技术和方法。同时,注意数据预处理的重要性,合理设置模型参数与网络结构,控制多步预测误差传播,防范过拟合,规划计算资源与训练时间,关注模型的可解释性和透明度,以及持续更新与迭代模型,以适应数据分布的变化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值