Delphi中解决循环引用问题

16

有没有方法可以解决Delphi中的循环单元引用问题?

也许是使用更新版本的Delphi或某种神奇的黑科技什么的?

我的Delphi项目有10万多行代码,大部分基于单例类。我需要重构它,但这意味着要经历几个月的“循环引用”地狱 :)

8个回答

46

我过去10年一直在维护接近一百万行的遗留代码,因此我理解您的痛苦!

在我维护的代码中,当我遇到循环使用时,我经常发现是由单元A中的常量或类型定义导致的,这些常量或类型定义被单元B需要。(有时也是单元A中的一小段代码(甚至全局变量)也被单元B需要。)

在这种情况下(如果我运气好的话!),我可以仔细地将那些代码部分提取到一个新的单元C中,其中包含常量、类型定义和共享代码。然后单元A和单元B使用单元C。

我有些犹豫地发布上面的内容,因为我不是软件设计专家,意识到这里有许多比我更有知识的人。希望我的经验能对您有所帮助。


6
在我看来,在这里没有必要犹豫。 - Uli Gerhardt
1
那是当你很幸运的时候。但是当你不幸的时候呢?这不是关于类型定义的问题吗? - Gabriel
嗨,RobertFrank。只是好奇(与原问题无关):您如何计算SLOC?您使用哪个工具? - Gabriel
我可以使用grep命令查找代码行数,或者在IDE中进行构建并查看编译的行数。 - RobertFrank
@RobertFrank-非常感谢您的回答。我之所以问这个问题,是因为我们正在讨论Delphi在计算SLOC方面的效率:http://stackoverflow.com/questions/23722031/how-is-sloc-counted-by-delphi-ide/23722872?noredirect=1#comment36497149_23722872 - Gabriel
显示剩余2条评论

14
  1. 看起来你的代码设计存在相当严重的问题。除了许多这种问题的迹象之外,一个是循环单元引用。但正如你所说:你无法重构所有代码。
  2. 尽可能将所有内容移动到implementation部分。它们被允许有循环引用。
    • 为了简化此任务,您可以使用第三方工具。我建议使用 Peganza Pascal Analyzer - 它会建议您可以移动到实现部分的内容,并提供许多其他提示以改进您的代码质量。

4
我不同意第一个观点。在将一个单元重构为多个单元时可能出现的循环引用并不总是证明初始代码设计不良。它们是编译器限制的影响,不代表好或坏的代码。例如,使用Delphi 2009 Enterprise创建GoF Visitor模式的代码时,所有代码都包含在一个单元中 - 如果我尝试将其拆分为模型和访问者类的单元,则会遇到循环引用。那么Visitor模式中是否存在代码设计问题?(参见https://dev59.com/rUzSa4cB1Zd3GeqPlEeq) - mjn
不需要,但访问者模式也不需要多个单位。 - Marco van de Voort
没有人责怪编程语言本身。我认为循环引用是 Pascal(Delphi)典型的灾难。我知道其他语言也有这个问题,但是 Pascal... 我还认为编译器可以解决一些问题。我的意思是,即使我们还没有完全编译该类,将指针设置为类有什么问题呢? - Gabriel
1
将该单元移动到实现部分解决了我的问题。 - user3820843

11

尽可能使用实现部分使用的内容,并将接口使用子句中的内容限制为必须在接口声明中可见的内容。

没有“魔法黑客”。循环引用会导致编译器陷入无限循环(单元A需要编译单元B,单元B需要编译单元A,单元A需要编译单元B,等等)。

如果您有特定情况认为无法避免循环引用,请编辑您的帖子并提供代码;我相信这里的某个人可以帮助您解决问题。


3
不会,例如C语言程序通常不会出现循环依赖的问题。编译器编译模块A时,发现模块B未编译,则会先分析B的源代码以编译A,再编译B。 - fuz
1
这个问题与"C程序"无关;它被特别标记为"Delphi",而"单元依赖性"与C的"模块依赖性"不同。但还是谢谢您的回答;如果您在评论之前熟悉编译器会更好。 :-) - Ken White
1
你说“对于编译器来说”。我只是想说我不同意这个语句适用于所有编译器。虽然可以编写可以处理循环依赖的编译器(即使是 Delphi),但默认编译器却做不到。 - fuz
1
这个问题是专门标记为Delphi的,这意味着正在讨论的是Delphi编译器。没有人说“任何编译器”。在一个特别标记为“Delphi”的问题中,“编译器”指的是特定的Delphi编译器。如果适用的话,这是一个很好的论点。 :-) - Ken White
1
首先,Delphi是一种语言(或者说是一种带有大量库的语言),有多个实现版本。例如,Lazarus实现了Delphi的大部分功能。当然,你可能想表达的是Borland、CodeGear或Embarcadero Delphi,但你并没有明确说明你指的是哪个实现版本。我甚至可以编写自己的编译器,与Delphi兼容,并移除这个限制。 - fuz
1
Lazarus是一个针对Free Pascal编译器的IDE,旨在实现Delphi兼容性。Delphi是Borland/CodeGear/Embarcadero实现其Delphi语言编译器的许可商标,任何未经限定的对Delphi的引用都特指该实现。再次强调,这个问题特指Delphi(但值得一提的是,Free Pascal也存在循环引用问题,并支持interface和implementation使用条款以便于解决它们)。 - Ken White

8
有很多方法可以避免循环引用。
1. 委托。对象经常会执行一些应该在事件中完成而不是由对象本身完成的代码。无论是因为项目上的程序员时间太短(我们总是这样吗?),经验/知识不足还是懒惰,此类代码最终都会出现在应用程序中。实际例子:TCPSocket组件直接更新应用程序主窗体上的某个可视化组件,而不是让主窗体在组件上注册“OnTCPActivity”过程。
2. 抽象类/接口。使用它们之一可以消除许多单元之间的直接依赖关系。抽象类或接口可以在其自己的单元中单独声明,将依赖关系限制到最大。例如:我们的应用程序有一个调试窗体。它在整个应用程序中都有用处,因为它显示来自应用程序各个区域的信息。更糟糕的是,允许显示调试窗体的每个窗体也将最终需要所有调试窗体的单元。更好的方法是拥有一个基本为空的调试窗体,但具有注册“DebugFrames”的能力。
TDebugFrm.RegisterDebugFrame(Frame: TDebugFrame);
这样,TDebugFrm就没有自己的依赖关系(除了对TDebugFrame类的依赖关系)。任何需要显示调试窗体的单元都可以这样做,而不会冒着添加过多依赖关系的风险。
还有许多其他例子……我敢打赌它可以填满一本书。在时间紧迫的情况下设计一个清晰的类层次结构非常困难,这需要经验。知道可用的工具以及如何使用它们是实现这一目标的第一步。但是要回答您的问题……没有通用的答案,必须根据具体情况进行考虑。

4

1

Modelmaker Code Explorer有一个非常好的向导,可以列出所有使用情况,包括循环。

它要求您的项目编译通过。

我同意其他帖子中提到的这是一个设计问题。
您应该仔细查看您的设计,并删除未使用的单元。

在DelphiLive'09上,我做了一个名为Smarter code with Databases and data aware controls的会议,其中包含了一些关于良好设计的技巧(不仅限于DB应用程序)。

--jeroen


1
我找到了一个解决方案,不需要使用接口,但可能无法解决循环引用的所有问题。
我有两个类在两个单位:TMap和TTile。
TMap包含一个地图,并使用等距线瓦片(TTile)显示它。
我希望在TTile中有一个指针,指向地图。地图是TTile的类属性。
一般来说,您需要在另一个单位中声明每个相应的单位...并获得循环引用。
在这里,我是如何解决它的。
在TTile中,我声明地图为TObject,并将Map单元移动到实现部分的Uses子句中。
这样,我就可以使用地图,但每次需要将其强制转换为TMap才能访问其属性。
我能做得更好吗?如果我可以使用getter函数进行类型转换。但我需要将Uses Map移到接口部分....所以回到原点。
在实现部分,我声明了一个不属于我的类的getter函数。一个简单的函数。

实现

使用 Map;

函数 Map: TMap; 开始 结果 := TMap(TTile.Map); 结束;

太好了,我想。现在,每次我需要调用我的地图属性时,我只需使用 Map.MyProperty。

哎呀!编译失败了! :) 没有按预期工作。编译器使用 TTile 的 Map 属性而不是我的函数。

所以,我将我的函数重命名为 aMap。但我的缪斯向我倾诉。不要!重命名类属性为 aMap... 现在我可以按照自己的意愿使用 Map。

Map.Size; 这会调用我的小函数,它将 aMap 强制转换为 TMap;

Patrick Forest


在StackOverflow上,您只需发布一个答案。您应该合并您的答案,并保持它们简短和客观。 - Gabriel

-1

我之前回答了一个问题,但是经过一些思考和琢磨,我找到了解决循环引用问题的更好方法。这是我的第一个单元,需要在单元B中定义一个指向对象TB的指针。

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, b, StdCtrls;

type

  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }

  public
    { Public declarations }
    FoB: TB;
  end;

var
  Form1: TForm1;



implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
begin
  FoB := TB.Create(Self);
  showmessage(FoB.owner.name);
end;

end.

这里是 Unit B 的代码,其中 TB 拥有指向 TForm1 的指针。
unit B;

interface

  Uses
    dialogs, Forms;

  type
    TForm1 = class(TForm);

    TB = class
     private
       FaOwner: TForm1;
     public
       constructor Create(aOwner: TForm);
       property owner: TForm1 read FaOwner;
    end;

implementation
  uses unit1;

  Constructor TB.create(aOwner: TForm);
  Begin
    FaOwner := TForm1(aOwner);

    FaOwner.Left := 500;
  End;//Constructor
end.

这里是它为什么能编译的原因。首先,Unit B在实现部分声明了对Unit1的使用。立即解决了Unit1和Unit B之间的循环引用问题。

但是为了让Delphi编译通过,我需要给它提供一些关于FaOwner:TForm1的声明信息。所以,我添加了一个名为TForm1的存根类,它与Unit1中的TForm1声明相匹配。 接下来,在调用构造函数时,TForm1能够将自己作为参数传递。在构造函数代码中,我需要将aOwner参数强制转换为Unit1.TForm1。然后,FaOwner就指向了我的表单。

现在,如果TB类需要在内部使用FaOwner,我不需要每次都将其强制转换为Unit1.TForm1,因为两个声明是相同的。请注意,您可以将构造函数的声明设置为

Constructor TB.create(aOwner: TForm1);

但是当TForm1调用构造函数并将自身作为参数传递时,您需要将其强制转换为b.TForm1。否则,Delphi会抛出错误,告诉您两个TForm1不兼容。因此,每次调用TB.constructor时,您都需要将其强制转换为适当的TForm1。第一种解决方案,使用共同的祖先,更好。写一次类型转换,然后忘记它。

发布后,我意识到我犯了一个错误,告诉大家两个TForm1是相同的。他们不是。Unit1.TForm1具有组件和方法,这些组件和方法对于B.TForm1来说是未知的。只要TB不需要使用它们或只需要使用TForm提供的共性,您就可以使用它。如果您需要从TB调用特定于UNit1.TForm1的内容,则需要将其强制转换为Unit1.TForm1。

我在Delphi 2010中尝试并测试了它,它编译并正常工作。

希望它能帮助您,减轻一些头痛。


这就像拿着引爆的手榴弹,试图自我诱发癫痫发作。问题在于你依赖于反直觉的作用域技巧。更重要的是,它真的能正确地工作吗?如果你有第三个单元,并询问一个 TB 实例的所有者,它会得到正确的所有者类吗?(不会)所有多态覆盖是否都表现正确?(我不确定)目前的代码没有任何阻止我使用 TSomeEntirelyDifferentForm 的实例创建 TB,这可能会在长期运行中导致极端灾难。(至少这个问题可以解决) - Disillusioned
我同意它并不能解决所有问题。大多数情况下,您只想要一个指向拥有者对象的指针。在这种情况下,它是有效的。完美的解决方案将来自Delphi。如果Delphi能够读取所有接口部分,然后查看是否有什么问题来警告我们。不,现在继续实现部分。 - Patrick Forest
@Craig 如果您有第三个单元并要求TB实例的所有者,它会得到正确的所有者类吗?(不会)是的,简单地声明一个指向对象的指针即可FaOwner: TForm1;。一旦指针被初始化,它将始终指向同一个对象。这个事实的证明是我可以更改我的代码,忽略B单位中TForm1的声明,并用指针替换它。代码仍然可以运行。但我每次使用它时都需要进行类型转换。但是对于我的存根TForm1,如果我只想使用Unit1.TForm1中声明的东西,则只需要进行类型转换即可。 - Patrick Forest
它将始终指向相同的对象。相信我,我比你想象的更理解这一点。但是每次使用它都需要进行类型转换,这正是为什么它是错误的原因!引用类型在层次结构的不同分支中。您只能通过未经检查的类型转换来进行强制转换。您正在编写导致灾难的配方,以欺骗编译器进行循环引用,而不是避免它。如果您真的需要类之间的循环引用,则它们无论如何都是紧密耦合的 - 最好将它们放在同一个单元中。 - Disillusioned

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