16

有没有办法绕过 Delphi 中的循环单元引用?

也许是更新版本的德尔福或一些魔法黑客或什么的?

我的 delphi 项目有 100 000 多行代码,主要基于单例类。我需要重构它,但这意味着几个月的“循环引用”地狱:)

4

8 回答 8

44

在过去的 10 年里,我一直在维护近一百万行遗留代码,所以我理解你的痛苦!

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

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

我有些犹豫地发布了上述内容,因为我不是软件设计方面的专家,并且意识到这里还有很多其他人比我知识渊博。不过,希望我的经验对您有所帮助。

于 2010-04-15T13:48:02.890 回答
14
  1. 看来,您有相当严重的代码设计问题。除了这些问题的许多迹象外,一个是单元循环引用。但正如你所说 - 你不能重构所有的代码。
  2. 将所有可能的内容移至实施部分。他们被允许有循环引用。
  3. 为了简化任务 (2),您可以使用 3d 派对工具。我会推荐 - Peganza Pascal Analyzer ( http://www.peganza.com )。它将建议您可以将哪些内容移至实施部分。这将为您提供更多提高代码质量的提示。
于 2010-04-15T12:54:20.030 回答
10

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

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

如果您有特定情况认为无法避免循环引用,请编辑您的帖子并提供代码;我相信这里有人可以帮助您弄清楚如何修复它。

于 2010-04-15T12:49:52.900 回答
8

有很多方法可以避免循环引用。

  1. 代表。很多时候,一个对象会执行一些应该在事件中完成的代码,而不是由对象本身完成。无论是因为从事该项目的程序员的时间太短(我们不是总是这样吗?),没有足够的经验/知识还是只是懒惰,这样的一些代码最终会出现在应用程序中。真实世界的例子:直接更新应用程序 MainForm 上的一些可视化组件的 TCPSocket 组件,而不是让主窗体在组件上注册“OnTCPActivity”过程。

  2. 抽象类/接口。使用它们中的任何一个都可以消除许多单元之间的直接依赖关系。抽象类或接口可以在其自己的单元中单独声明,从而最大限度地限制依赖关系。示例:我们的应用程序有一个调试表单。它几乎用于整个应用程序,因为它显示来自应用程序各个区域的信息。更糟糕的是,每个允许显示调试表单的表单最终也将需要调试表单中的所有单元。更好的方法是有一个基本上为空的调试表单,但它具有注册“DebugFrames”的能力。

    TDebugFrm.RegisterDebugFrame(Frame : TDebugFrame);

    这样,TDebugFrm 没有自己的依赖项(除了 TDebugFrame 类)。任何需要显示调试表单的单元都可以这样做,而不会冒险添加太多的依赖项。

还有很多其他的例子……我敢打赌它可以写一本书。以高效的方式设计一个干净的类层次结构非常困难,而且需要经验。了解实现它的可用工具以及如何使用它们是实现它的第一步。但是要回答您的问题……您的问题没有千篇一律的答案,始终要根据具体情况进行处理。

于 2010-04-15T17:05:57.770 回答
4

类似问题:Delphi Enterprise:如何在没有循环引用的情况下应用访问者模式?

Uwe Raabe 提出的解决方案使用接口来解决循环依赖。

于 2010-04-15T16:03:22.010 回答
1

Modelmaker Code Explorer有一个非常棒的向导,用于列出所有用途,包括循环。

它要求您的项目编译。

我同意其他海报的观点,即这是一个设计问题。
您应该仔细查看您的设计,并删除未使用的单元。

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

——杰伦

于 2010-04-15T14:26:35.870 回答
1

我找到了一个不需要使用接口但可能无法解决循环引用的所有问题的解决方案。

我在两个单元中有两个课程:TMap 和 TTile。

TMap 包含一个地图并使用等距图块 (TTile) 显示它。

我想在 TTile 中有一个指针指向地图上。Map 是 TTile 的一个类属性。

类 Var FoMap:TObject;

通常,您需要在另一个单元中声明每个相应的单元......并获取循环引用。

在这里,我如何解决它。

在 TTile 中,我将 map 声明为 TObject,并在 Implementation 部分的 Uses 子句中移动 Map 单元。

这样我可以使用 map 但每次都需要将其转换为 TMap 以访问其属性。

我能做得更好吗?如果我可以使用 getter 函数进行类型转换。但我需要在界面部分移动使用地图......所以,回到第一格。

在实现部分,我确实声明了一个不属于我的类的 getter 函数。一个简单的功能。

执行

使用地图;

功能图:TMap;开始结果 := TMap(TTile.Map); 结尾;

酷,我想。现在,每次我需要调用 Map 的属性时,我只使用 Map.MyProperty。

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

所以,我将我的函数重命名为 aMap。但我的缪斯女神对我说话。不!将 Class 属性重命名为 aMap... 现在我可以按照我的意图使用 Map。

地图.尺寸;这调用了我的小函数,他将 aMap 类型转换为 TMap;

帕特里克森林

于 2013-08-05T23:09:02.547 回答
-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.

这里是单元 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。因此,我添加了与 Unit1 中 TForm1 的声明相匹配的存根类名称 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 未知的组件和方法。Has long TB不需要使用它们或者只需要使用TForm给出的通用性就可以了。如果您需要从 TB 调用 UNit1.TForm1 的特定内容,则需要将其类型转换为 Unit1.TForm1。

我尝试了它并使用 Delphi 2010 对其进行了测试,它编译并运行。

希望对您有所帮助,让您免于头疼。

于 2013-12-08T09:22:49.150 回答