【问题标题】:getting around circular references in Delphi [duplicate]在Delphi中绕过循环引用[重复]
【发布时间】:2010-04-15 12:00:21
【问题描述】:

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

也许是更新版本的 delphi 或一些魔术技巧或其他什么?

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

【问题讨论】:

    标签: delphi circular-reference


    【解决方案1】:

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

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

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

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

    【讨论】:

    • +1。恕我直言,这里无需犹豫。
    • +1 我和格哈特先生在一起。无需犹豫。
    • +1 “AppTypes.pas”单元在 Delphi 应用程序中很常见。
    • 那是你走运的时候。但是运气不好的时候呢?不是关于类型定义吗?
    • 嗨罗伯特弗兰克。只是好奇(与原始问题无关):您如何计算 SLOC?你用什么工具?
    【解决方案2】:
    1. 看来,您有相当严重的代码设计问题。除了这些问题的许多迹象外,一个是单元循环引用。但正如你所说 - 你不能重构所有代码。
    2. 将所有可能的内容移至实施部分。它们可以有循环引用。
    3. 为了简化任务 (2),您可以使用 3d 派对工具。我会推荐 - Peganza Pascal Analyzer (http://www.peganza.com)。它将建议您可以将哪些内容移至实施部分。这将为您提供更多提高代码质量的提示。

    【讨论】:

    • 我不同意第一点。在将一个单元重构为多个单元时可能出现的循环引用并不总是初始代码设计不良的证明。它们是编译器限制的结果,而不是“好”或“坏”代码。示例:使用 Delphi 2009 Enterprise 为 GoF 访问者模式创建代码,所有代码都包含在一个单元中 - 如果我尝试将其拆分为模型和访问者类的单元,我会遇到循环引用。那么访问者模式中是否存在代码设计问题? (见stackoverflow.com/questions/2356318
    • 否,但访客模式也不需要多个单元
    • 没有人指责语言(本身)。我认为循环引用是 Pascal (Delphi) 的典型瘟疫。我知道其他语言也有它们,但是 Pascal ......我也认为编译器可以解决一些问题。我的意思是即使我们还没有完全编译那个类,设置一个指向一个类的指针有什么问题?
    • 将单元移至实施部分更正了我的问题。
    【解决方案3】:

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

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

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

    【讨论】:

    • 不,他们不会。例如,C 程序通常完全没有循环依赖的问题。编译器编译模块A,发现模块B未编译,只分析B的源码编译A再编译B。
    • 本题与“c程序”无关;它被专门标记为“Delphi”,并且“单元依赖”与 c 的“模块依赖”不同。不过,感谢您的意见;如果您在评论之前熟悉编译器可能会很好。 :-)
    • 你说“为了编译器”。我刚刚写道,我不同意这适用于任何编译器的说法。可以编写可以处理循环依赖的编译器(即使对于 Delphi),但默认编译器根​​本不具备这种能力。
    • 这个问题被专门标记为 Delphi,这意味着正在讨论的是 Delphi 编译器。没有人说“任何编译器”。专门标记为“Delphi”的问题中的“编译器”是指Delphi 编译器。很好的论点,如果它适用的话。 :-)
    • Lazarus 是一个用于 Free Pascal 编译器的 IDE,它致力于实现 Delphi 兼容性Delphi 是其 Delphi 语言 编译器的 Borland/CodeGear/Embarcadero 实现的许可商标,任何对 Delphi 的不合格引用都特别指执行。再一次,这个问题专门提到 Delphi (但 FWIW Free Pascal 也有循环引用问题,并且支持 interfaceimplementation 使用子句以便解决它们)_ .
    【解决方案4】:

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

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

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

      TDebugFrm.RegisterDebugFrame(Frame : TDebugFrame);

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

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

    【讨论】:

      【解决方案5】:

      类似问题:Delphi Enterprise: how can I apply the Visitor Pattern without circular references?

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

      【讨论】:

        【解决方案6】:

        Modelmaker Code Explorer 有一个非常好的向导,可以列出所有用途,包括周期。

        它要求你的项目编译。

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

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

        --杰罗恩

        【讨论】:

          【解决方案7】:

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

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

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

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

          类 Var FoMap: TObject;

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

          在这里,我如何解决它。

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

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

          我可以做得更好吗?如果我可以使用 getter 函数进行类型转换。但我需要将 Uses Map 移到 Interface 部分......所以,回到第一格。

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

          实施

          使用地图;

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

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

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

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

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

          帕特里克·森林

          【讨论】:

          • 在 StackOverflow 上,您只需发布一个答案。你应该合并你的答案。并保持简短和客观。
          【解决方案8】:

          我之前给出了一个答案,但经过一番思考和摸索后,我找到了解决循环引用问题的更好方法。这里我的第一个单元需要在单元 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 对其进行了测试,它编译并运行。

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

          【讨论】:

          • 这就像拿着一个引爆的手榴弹并试图自我诱发癫痫发作。问题是您依赖于违反直觉的范围界定技巧。更重要的是它真的工作正确吗?如果您有第三个单元并向一个 TB 实例询问它的所有者,它会得到正确的所有者类吗? ()是否所有的多态覆盖行为都正确? (我不确定)就目前的代码而言,没有什么能阻止我使用TSomeEntirelyDifferentForm 的实例创建TB,从长远来看,这可能会导致极端灾难。 (至少这个可以修复)
          • 我同意它并不能解决所有问题。大多数时候,您只想拥有一个指向所有者对象的指针。在这种情况下,它可以工作。完美的解决方案来自德尔福。如果 Delphi 可以读取所有的接口部分,那么看看是否有什么问题来警告我们。不,现在继续执行部分。
          • @Craig 回答你的问题如果你有第三个单元并询问一个 TB 实例的所有者,它会得到正确的所有者类吗? (否) 是的,FaOwner: TForm1; 只是声明一个指向对象的指针。一旦指针被初始化,它将始终指向同一个对象。证明这一点我可以更改我的代码,忽略 B 单元中 TForm1 的声明并用指针替换它。和代码立场。但是我每次使用它时都需要对它进行类型转换。但是对于我的存根 TForm1,如果我想使用仅在 Unit1.TForm1 中声明的东西,我只需要进行类型转换。
          • “它总是指向同一个对象” 相信我,我明白这比你想象的要好得多。 “但是我每次使用它时都需要对它进行类型转换” 这就是正是为什么它是错误的!引用的类型位于层次结构的不同分支中。您只能通过 unchecked 类型转换来转换它。您正在为 disaster 炮制配方,以欺骗编译器进行循环引用而不是避免它。如果您真的需要类之间的循环引用,那么无论如何它们都是紧密耦合的——不妨将它们放在同一个单元中。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2013-07-20
          • 2016-09-04
          • 2019-03-21
          • 1970-01-01
          • 1970-01-01
          • 2018-04-12
          相关资源
          最近更新 更多