【问题标题】:Delphi: declare variable avoiding circular referenceDelphi:声明变量避免循环引用
【发布时间】:2016-07-04 18:53:39
【问题描述】:

我有一个Delphi单元,它需要保存应用程序的各种形式的指针,以便以后对它们进行操作。

为了执行这些操作,我需要将指针转换为表单类型,例如。

var    
  ptrFrmMain: Pointer;
  CurrentFrmMain: TfrmMain;
begin
    CurrentFrmMain := ptrFrmMain;
    CurrentFrmMain.Close();
end;

问题是这个单元包含在应用程序的所有其他 Delphi 单元的使用中。因此,虽然我可以在接口部分声明一个简单的Pointer 类型,但我不能声明在其他单元中声明的类型(例如单元frmMain.pasTfrmMain)。

我可以通过在实现部分放置一个使用来解决这个问题,例如:

interface
type TMyThread = class(TThread)
  Public
    ptrFrmMain:Pointer
...

implementation
    uses frmMain

    var
      CurrentFrmMain: TfrmMain;

但仍然存在一个问题:我需要该变量特定于我的类实例,用于多线程目的,而不是通用全局变量。 但是我不能把它放在我的TmyThread 类中,因为TfrmMain 没有在那里声明,我不能把它放在接口部分的用途中。

一个解决方案是将CurrentFrmMain作为一个局部变量放在所有使用它的程序中,然后每次都进行CurrentFrmMain := ptrFrmMain转换,但是您知道更好的解决方案吗?

非常感谢您。

【问题讨论】:

  • 您可以就地类型转换您的指针,也可以声明为通用基础 TForm...
  • @FreeConsulting 我不能声明为通用基本 Tform,因为我需要调用特定于我的表单的函数,例如。 'CurrentFrmMain.MyFunction()'
  • 将变量声明为 TForm,然后在实现部分使用as 将其转换为特定的表单类型。这不会“重新初始化”任何东西,它只是使用正确类型的引用来引用现有表单。
  • 不,不会。 As 检查表单是否真的是您指定的表单类型,如果不是则抛出异常。这是一个类型安全的演员表。您可以使用is 检查之前的类型。另请注意,asis 仅适用于对象,不适用于无类型指针。
  • 一般模式是:if MyForm is TFormABCD then MyFormABCD := MyForm as TFormABCD; // now you can use MyFormABCD

标签: delphi circular-reference


【解决方案1】:

我根本不会在线程中放置表单指针。我会让线程持有回调函数,甚至是一个接口:

type
  TCloseProc: procedure of object;

  TMyThread = class(TThread)
  public
    CloseProc: TCloseProc;
    ...
  end;

...

begin
  if Assigned(CloseProc) then CloseProc();
end;

type
  IMyIntf = interface(IInterface)
  ['{9CC7DB9E-D47F-4B7D-BBF9-6E9B80823086}']
    procedure DoClose;
  end;

  TMyThread = class(TThread)
  public
    Intf: IMyIntf;
    ...
  end;

...

begin
  if Assigned(Intf) then Intf.DoClose();
end;

...

type
  TfrmMain = class(TForm, IMyIntf)
  public
    procedure doClose;
  end;

procedure TfrmMain.doClose;
begin
  Close;
end;

创建线程后,将 Form 方法分配给那些回调,或者将 Form 的接口实现传递给线程:

Thread := TMyThread.Create(True);
Thread.CloseProc := frmMain.Close;
Thread.Resume;

Thread := TMyThread.Create(True);
Thread.Intf := frmMain as IMyIntf;
Thread.Resume;

无论哪种方式,线程都不需要知道实际的表单,同时仍然满足特定于表单的功能。

【讨论】:

  • 界面很整洁,但他们不是很危险被 ARC 和意想不到的释放形式击中吗?
  • 猜猜如果表单的给定对象甚至实现了所需的接口,就会添加一个保护检查......
  • @Arioch'The: ARC 至少在 VCL 中不是问题,因为TComponent 禁用组件对象实现的接口的引用计数,而TFormTComponent 的后代。而as 运算符为接口实现提供了必要的检查。
【解决方案2】:

取决于您所说的“保留各种形式的应用程序的指针,以便以后对它们进行操作”是什么意思。 - 那是什么(或几种)工作?这是一个关于通用软件设计、分解的问题,而不仅仅是circular reference 或任何其他特定于语言的问题。

如果您只想对任何表单进行相同的工作 - 那么您应该从相同的 BASE-FORM-CLASS 派生表单并保留对该基类的引用,而不是对特定表单类的引用。例如,如果您只需要 .Release 它们,则可以将它们全部保留为 TForm 类型引用,它们都来自它们。这只是提取通用抽象接口的一个典型案例。

TMyFormWithActions = class ( TForm ) .... end;    
TMyForm1234 = class ( TMyFormWithActions ) .... end;
TMyFormABCD = class ( TMyFormWithActions ) .... end;

您也可以不将通用功能提取到中间类中,而是提取到 MS COM interface 中,就像 Remy 在他的回答中显示的那样。然而,这与 MS COM 所基于的完全不同的内存模型(ARC one)接壤。虽然我不希望TForm 具有自动销毁引用计数,但我也不能完全确定它不会发生,尤其是在继承和复杂的应用程序中。因此,虽然我确实喜欢这种方法,但我省略了它,因为有时在实践中它可能会导致对象意外和过早死亡。如果您可以确保不会发生这种情况,尽管它可能是最干净的解决方案。


如果您需要执行不同的操作,那么您确实不仅可以存储对表单本身的引用,还可以存储对操作、软件 sn-ps 的引用。然后,您的线程声明类将构建一个通用框架来保存表单和过程数据单元。然后你会有额外的单元来执行这些特定的操作。

(线程和动作接口单元)== uses ==>(TMyFormABCD 单元的动作)uses ==(TMyFormABCD 表单声明单元)

作为一个简化选项,您可以使用与表单本身相同的单位来声明这些操作。然后,您将让所有表单单元都依赖于线程单元,但线程单元(重新制作为通用和特定表单不可知)将不再依赖于任何表单单元。可能它可能被称为“控制反转”。

看这个系列:http://www.uweraabe.de/Blog/2010/08/16/the-visitor-pattern-part-1/


另外一种设计方案,可以看作是实现这两种方法 - 将使用 Windows 消息。 您的“通用界面”,您的“操作”将由您制作的自定义 WM_xxx 消息(整数常量)表示。然后您的线程将使用PostMessage API 向表单发送这些操作信号。而那些形式——通过实现处理这些消息的方法(或通过不实现=忽略这些消息)将提供那些动作实现。

见:http://www.cryer.co.uk/brian/delphi/howto_send_custom_window_message.htm

PostMessage 可以从外部线程使用,但不能(轻松)返回值。 SendMessage 只能在 Delphi 主线程中使用。此外,您必须在发布消息之前检查是否MyTargetForm.HandleAllocated()

【讨论】:

  • 我不能使用通用的基本 Tform 类,因为我需要调用特定于我的表单的函数,例如。 'CurrentFrmMain.MyFunction()'。所以我希望能够轻松调用这些函数,而不必重写大部分代码(如果可能的话,将 CurrentFrmMain 变量保留为类成员,而不必将其用作局部变量,这需要从每个使用它的函数中的指针重新初始化)。
  • “特定于我的形式”是什么意思 - 特定意图(= 声明 = 接口 = 合同)或特定实现(又名“虚拟函数”和“抽象函数”)?
  • 如果您想对不同的表单进行完全不同的操作 - 请阅读访问者模式序列。您将不得不离开并查看如何分解程序的更大图景。如何设计它的块。在哪里放置动作,在哪里放置表单,它。有几种可能的方案,它们都有各自的取舍,只有了解自己的程序工作的您才能尝试猜测哪个问题较少。
  • 基本上在这种情况下,您确实有相同的基类接口,并且该接口是procedure/function HandleSocketMessage(const DATA: socket-message-data)。所有特定于表单的知识和功能都应在该统一功能中处理!并且该函数将在表单本身或特殊代理(桥)类中创建,将其目标表单类与套接字管理器链接
  • @Flavio:请注意,HandleSocketMessage(或任何你称之为的)应该是所有表单派生的基类的一部分。如果 HandleSocketMessage 是虚拟的,您可以在每个特定的表单类中重写它以执行该表单需要执行的操作。 ISTM,您可能还希望将您的逻辑(您的模型)与表单(您的演示文稿)分开。不要将所有代码都放在事件处理程序中。只有直接负责表单工作的代码应该/可以去那里,而不是整个程序逻辑。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-09-05
  • 1970-01-01
  • 1970-01-01
  • 2014-06-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多