【问题标题】:Access Violation While Calling Form's Method调用表单方法时访问冲突
【发布时间】:2015-07-17 07:09:50
【问题描述】:

我有一个表单 TForm1,我在其中保存了 cxSpreadsheet 组件。我还创建了一个名为 TTest 的类。 TTest 类包含两种方法,一种用于加载数据,另一种用于保存数据。

当我在 TTest 类方法中调用 TForm1 类的方法时,即使创建了组件,当我调用 Test.LoadMyData 方法时也会出现访问冲突,它会导致对 ATableView1 变量的访问冲突。

我做错了什么?

type
  TForm1 = class(TForm)
    dxSpreadSheet1: TdxSpreadSheet;
    procedure FormShow(Sender: TObject);
  public
    ATableView1 : TdxSpreadSheetTableView;
    procedure Initilize;
    procedure LoadData;
  end;

  TTest = class
  public
    procedure SaveMyData(MyValue : String);
    procedure LoadMyData;
  end;

var
  Form1: TForm1;
  Test: TTest;

implementation

{$R *.dfm}

procedure TForm1.FormShow(Sender: TObject);
begin
  Form1 := TForm1.Create(Self);
  Initilize;
  Test := TTest.Create;
  Test.LoadMyData;
end;

procedure TForm1.Initilize;
begin
  ATableView1 := dxSpreadSheet1.Sheets[0] as TdxSpreadSheetTableView;
end;

procedure TForm1.LoadData;
begin
  ATableView1.Cells[10,1].SetText('Test Application');
end;

procedure TTest.LoadMyData;
begin
  Form1.LoadData;
end;

【问题讨论】:

    标签: delphi delphi-2010


    【解决方案1】:

    在您的问题中,您遗漏了一个非常重要的难题。您在 cmets 中提到过,但我在这里重复一遍,因为它是您问题的直接触发因素。在 cmets 中,您说表单的创建方式如下:

    with TForm1.Create(Self) do
    begin
      try
        ShowModal;
      finally
        Free;
      end;
    end;
    

    您的问题是,在ShowModal 的调用链中,您希望Form1 被分配专门分配给您刚刚创建的实例。但显然你还没有做任何设置Form1

    您的“解决方案”是在 ShowModal 调用链中分配 Form1。虽然这解决了您当前的问题,但它远非正确,我稍后会解释原因。首先,我将展示一个可以完全避免该问题的简单解决方案(请注意,它不是一个完整的解决方案,因为您的代码存在的问题比您意识到的要多得多)。

    解决办法,去掉with

    //This first line is the most important.
    //It explicitly sets which variable must be assigned to the new form.
    Form1 := TForm1.Create(Self);
    try
      Form1.ShowModal;
    finally
      Form1.Free;
    end;
    

    维沙尔,试试看。如果你这样做,你会看到它有效。希望我现在得到你的全部关注。一开始你不相信,但也许你现在意识到我真的确实理解正确


    那么为什么我说你的“解决方案”是错误的呢?毕竟它似乎解决了问题....

    好吧,您自己说过:您希望在FormShow 的开头分配Form1。你是对的,应该是。通过在FormShow 中分配Form1 := Self;,您只是在修补之前的错误。您肯定同意修复最初的错误比稍后修补它更好吗?

    但是这里有一个更深层次的问题……我不确定你是否理解“对象实例”和“类”之间的区别。 (如果你这样做了,那么请继续阅读接下来的几段以进行修改。

    您似乎希望在创建TForm1 时自动将其分配给Form1 变量。就好像您预计在任何时间点内存中都只会有一个TForm1。但是TForm1是类类型;这意味着它通常定义了任意数量的相同类型的对象实例的行为。每次创建TForm1 时,它都是表单的一个新的单独实例。每个实例都可以有自己的变量分配给它。例如

    JohnsForm1 := TForm1.Create(Self);
    PaulsForm1 := TForm1.Create(Self);
    

    如果您需要 2 个表单变量,请考虑一下您不正确的“解决方案”会发生什么?你会在FormShow 方法中写什么? JohnsForm1 := Self; PaulsForm1 := Self;?

    当然,您仍然可以选择在内存中一次只保留一个TForm1 实例。但是 Delphi 无法自动知道这是您的意图。所以你仍然应该像上面那样明确地做任何想要的任务。


    我提到您的代码还有更严重的问题。和上面关于对象实例和类类型的讨论有关。

    你的TTest 类做了一堆不必要的假设:

    • 假设永远只有 1 个 TForm1 实例。
    • 它假定表单在需要时始终可用。
    • 它假定表单将分配给Form1 变量。

    同样,如果您需要 JohnsForm1PaulsForm1,您的代码将无法正常工作。

    TTest.LoadMyDataTForm1.FormShow 的微小改动可以解决这些问题。

    //Write LoadMyData so it can be told which form instance to load the data into
    procedure TTest.LoadMyData(ALoadForm: TForm1);
    begin
      ALoadForm.LoadData;
    end;
    
    //Change FormShow to tell Test which form to use in LoadMyData
    procedure TForm1.FormShow(Sender: TObject);
    begin
      Initilize;
      Test := TTest.Create;
      Test.LoadMyData(Self);
    end;
    

    作为记录,大卫已经在his answer 中向您提供了此信息。他的回答还展示了对新 TTest 实例的适当资源保护,而为了简单起见,我将其省略了。

    顺便说一句,这 2 个小改动也可以解决您的问题。
    基本上,您的代码中有 2 个错误。这两个错误的结合导致了您的问题。
    您可以修复其中任何一个以使问题消失。但是您应该同时修复这两个问题以使您的代码更好。

    【讨论】:

    • 我非常感谢您努力使事情顺利进行。我确实在 Delphi 有 10 多年的时间。我应该按照您所说的那样创建新的表单:Form1 := TForm1.Create(Self);。我只以这种方式创建表单。但正如我所说,代码不是我写的。我不能把所有的东西都放在这里,因为很多时候我都限制在这里谈论重点。这就是我在这里没有提到这么多事情的原因。现在请不要说这是我的问题。我提到的表单创建不是我做的,它在整个应用程序中都使用过,而且数量太高
    • 我知道这是完全错误的方式,但情况是我不能像我们通常那样去所有地方并更改代码。我完全同意你以及所有在这里发表评论的大师们的看法。但最可悲的是,尽管有人告诉我代码不是我写的,但人们仍然在不必要地胡思乱想。反正我也不像往常一样在意。我也是一个很好的学习者,这就是我想在这里学习一些东西的原因。这就是为什么我在这里发布问题,当您在整个大型项目中遇到这种情况时如何处理。
    • 这与谁对谁错无关。对我来说,这只是学习的一部分。再次,我非常感谢您为消除疑虑所做的宝贵努力。如果人们记得,我写了“你将如何处理这种情况?”。有句话说:“你不应该只看报纸,你也应该在你正在阅读的新闻背后阅读。”我期待这一点,但正如我所说,你不能在这里写所有东西。我同意我应该至少提到完整的观点,但有一种互动方式。我已经接受了你的努力作为答案。非常感谢克雷格。感谢您的努力。
    • @All:请尝试阅读句子之外的内容。 (我不是说你是上帝或随意想象,而是试着看看为什么有人这样写。确认他想说什么。然后把你有价值的解决方案放在上面进行健康讨论)。它将解决很多问题和对其他人的假设。再次,我想在这里提一下。我不反对任何人,只是希望有人应该尝试理解我想说的话。不要急于证明代码是错误的。 Ppl 在你之前就已经知道代码是错误的。他们想知道如何处理这样的代码。
    • 我要感谢所有“尝试”传达他们的信息的人,但有些人无法阅读句子之外的内容。有时需要了解意图是什么,而不是书面内容。从我这边没有难过的感觉。再次感谢大家的努力。
    【解决方案2】:

    你陷入了困境。 Delphi 方法对隐式Self 对象(即实例)进行操作。也称为方法调用的目标。如果我们用明确的Self 编写您的代码,那么它看起来像这样:

    procedure TForm1.FormShow(Sender: TObject);
    begin
      Form1 := TForm1.Create(Self);
      Self.Initilize;
      Test := TTest.Create;
      Test.LoadMyData;
    end;
    
    procedure TForm1.Initilize;
    begin
      Self.ATableView1 := Self.dxSpreadSheet1.Sheets[0] as TdxSpreadSheetTableView;
    end;
    
    procedure TForm1.LoadData;
    begin
      Self.ATableView1.Cells[10,1].SetText('Test Application');
    end;
    
    procedure TTest.LoadMyData;
    begin
      Form1.LoadData;
    end;
    

    正如您所看到的,您对SelfForm1 的引用非常糟糕。请注意,Form1 全局变量是在 .dpr 文件中使用以下代码行实例化的:

    Application.CreateForm(TForm1, Form1);
    

    这最终导致FormShowForm1 调用。然后覆盖Form1,现在有两个TForm1 实例。你在Self 上调用Initilize,然后在Form1 上调用LoadData

    整个事情都是一团糟。代码可能如下所示:

    // remove global variable Test
    
    procedure TForm1.FormShow(Sender: TObject);
    var
      Test: TTest;
    begin
      Initilize;
      Test := TTest.Create;
      try 
        Test.LoadMyData(Self);
      finally
        Test.Free;
      end;
    end;
    
    procedure TForm1.Initilize;
    begin
      ATableView1 := dxSpreadSheet1.Sheets[0] as TdxSpreadSheetTableView;
    end;
    
    procedure TForm1.LoadData;
    begin
      ATableView1.Cells[10,1].SetText('Test Application');
    end;
    
    procedure TTest.LoadMyData(Form: TForm1);
    begin
      Form.LoadData;
    end;
    

    我们不使用全局变量,而是将表单引用作为参数传递。

    一旦您掌握了这一点,您可能会考虑完全删除全局变量 Form1。 IDE 在假设每个表单都只有一个实例的情况下创建此变量。但没有理由必须如此。就个人而言,我认为从长远来看,删除这些全局变量会让您受益。

    【讨论】:

      【解决方案3】:

      正如大卫如此雄辩地指出的那样,这是你制造的一个相当混乱的局面。他说 IDE 在项目文件中插入一行,自动为您创建表单是正确的。因此,自己创建它是多余的。

      在这种情况下,OnShow 处理程序可能是将一些测试数据添加到网格的正确位置,但我不会打扰 TTest 类来执行此操作。也就是说,首先让逻辑工作,然后如果需要,将这些东西移到另一个类中。我建议这样做是因为您似乎没有很好地掌握如何进行 OOD。我们都在同一时间!先把事情简单化。

      我为 CodeRage 9 制作了一个您可能会觉得有趣的视频。它被称为“你最近接受了你的内部软件管道工吗?”在 YouTube 上搜索它。它会让你对这个一般性主题有一些了解(可能太多了)。

      【讨论】:

        【解决方案4】:
        type
          TForm1 = class(TForm)
            dxSpreadSheet1: TdxSpreadSheet;
            procedure FormShow(Sender: TObject);
          public
            ATableView1 : TdxSpreadSheetTableView;
            procedure Initilize;
            procedure LoadData;
          end;
        
          TTest = class
          public
            procedure SaveMyData(MyValue : String);
            procedure LoadMyData;
          end;
        
        var
          Form1: TForm1;
          Test: TTest;
        
        implementation
        
        {$R *.dfm}
        
        procedure TForm1.FormShow(Sender: TObject);
        begin
          //Form1 is nil, if I check before executing below statement.
          Form1 := Self; //This statement make my code work. mmmhhhhhaaaa Love This Statement.
        
          Initilize;
          Test := TTest.Create;
          Test.LoadMyData;
        end;
        

        【讨论】:

        • 这解决了问题,因为问题与我描述的完全一样。但是,这绝对是一个坏主意。通过全局变量传递参数被认为是非常糟糕的做法:c2.com/cgi/wiki?GlobalVariablesAreBad
        • Vishal,这是我见过的最丑陋的解决方案。请花点时间充分理解大卫试图告诉你的内容。
        • 这就像在说:我不知道如何用钥匙解锁我的车,但是你看,我用砖头砸窗户成功地打开了它。
        • @Craig:这没有任何意义。想到什么就不要说什么。如果你理解正确的话,我最近说过表单是直接创建的,而这个 Form1 是一个变量,在我在设计时创建这个表单的那一刻自动声明。所以表单的创建方式和在 FormShow 事件中至少 Form1 对象不应该是 Nil 。我再次为您重复,因为 Form1 为零,这就是我尝试创建它的原因。现在举例来说,如果我需要在其他按钮单击事件上调用 TTest 类方法中的 TForm1 类的任何方法,那么我该如何调用它?
        • @VishalTiwari 相信我,我理解你做的比你想象的要好得多。没有必要试图解释。我明白你为什么一开始就有这个问题。我了解您是如何提出“解决方案”的。我理解您的“解决方案”为什么错误。而且我知道如何正确解决您的问题。我将发布一个答案,向您解释这一切。在您的 cmets 中,您曾说过您想要富有成效且有用的答案。如果你有兴趣学习,请仔细阅读我的回答。
        猜你喜欢
        • 2023-03-14
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-09-07
        • 2014-02-21
        • 2016-06-22
        • 1970-01-01
        相关资源
        最近更新 更多