【问题标题】:Why should I not use "with" in Delphi?为什么我不应该在 Delphi 中使用“with”?
【发布时间】:2010-09-18 03:27:26
【问题描述】:

我听说很多程序员,尤其是 Delphi 程序员鄙视使用“with”。

我认为它使程序运行得更快(只有一个对父对象的引用),并且如果使用得当(少于十几行代码且没有嵌套)更容易阅读代码。

这是一个例子:

procedure TBitmap32.FillRectS(const ARect: TRect; Value: TColor32);
begin
  with ARect do FillRectS(Left, Top, Right, Bottom, Value);
end;

我喜欢使用with。我怎么了?

【问题讨论】:

    标签: delphi with-statement


    【解决方案1】:

    我不喜欢它,因为它使调试变得很麻烦。您无法通过将鼠标悬停在变量等上来读取变量的值。

    【讨论】:

    • 这听起来像是调试器的问题,而不是语言功能的问题。
    • @GregHewgill 你是对的,这是调试器的问题。但这仍然是避免使用它的完全合理的理由。此外,这并不能原谅 with 引入的所有语言功能问题。最大的是 with 实际上 降低了 可读性。注意:更少的代码(使用 with)不会自动提高可读性。它减少了它,因为它不再明确 with 块中标识符的范围所指的内容。声明和初始化局部变量别名可以实现类似的代码减少,但保持显式取消引用。
    【解决方案2】:

    这种争论也经常发生在 Javascript 中。

    基本上,使用 With 语法很难一眼看出您调用的是哪个 Left/Top/etc 属性/方法。您可以有一个名为 Left 的局部变量和一个属性(已经有一段时间了我已经完成了delphi,如果名称错误,对不起)称为Left,甚至可能是一个名为Left的函数。任何阅读代码但对 ARect 结构不太熟悉的人都可能会非常迷茫。

    【讨论】:

      【解决方案3】:

      只要你保持简单并避免歧义,它没有任何问题。

      据我所知,它并没有加快任何速度 - 它纯粹是语法糖。

      【讨论】:

      • 如果涉及多个间接级别,它可以加快速度。考虑以下内容:使用 aControl.parent.parent.parent 执行 ... 后跟多个操作。 with 语句只计算一次三重间接并多次使用该引用。
      • 你怎么知道编译器在没有 with 的情况下不会这样做?前段时间我做了一些基本的基准测试,根本找不到任何区别。
      • @MasonWheeler - 使用GreatGrandParent := aControl.parent.parent.parent; 并引用GreatGrandParent 的成员可以达到相同的结果。只有它比with 更具可读性。
      【解决方案4】:

      在这种情况下我更喜欢 VB 语法,因为在这里,您需要在 with 块内的成员前面加上 . 以避免歧义:

      With obj
          .Left = 10
          .Submit()
      End With
      

      但实际上,with 总的来说并没有什么问题。

      【讨论】:

      • 同意。这基本上是我在 VB 中比 Delphi 更喜欢的唯一东西。
      • 这不适用于嵌套的“with”。 (不管你喜不喜欢。)
      • @Ulrich:VB 语法确实适用于嵌套的withWith fooWith .bar…。
      • @Konrad:那么内部with 中的.Left 是什么意思-foo.Leftfoo.bar.Left
      • @Ulrich:foo.bar.Left,当然。否则,嵌套 with 就没有意义了,不是吗?
      【解决方案5】:

      使用 with 的一个烦恼是调试器无法处理它。因此它使调试更加困难。

      更大的问题是代码不太容易阅读。特别是如果 with 语句有点长。

      procedure TMyForm.ButtonClick(...)
      begin
        with OtherForm do begin
          Left := 10;
          Top := 20;
          CallThisFunction;
        end;
      end;
      

      将调用哪个表单的 CallThisFunction?自我(TMyForm)或其他形式?如果不检查 OtherForm 是否具有 CallThisFunction 方法,您将无法知道。

      最大的问题是你可以在不知情的情况下轻松解决错误。如果 TMyForm 和 OtherForm 都有 CallThisFunction,但它是私有的怎么办。您可能期望/希望调用 OtherForm.CallThisFunction,但实际上并非如此。如果您不使用 with,编译器会警告您,但现在不会了。

      在 with 中使用多个对象会使问题成倍增加。见http://blog.marcocantu.com/blog/with_harmful.html

      【讨论】:

        【解决方案6】:

        它允许不称职或邪恶的程序员编写难以阅读的代码。因此,请仅在您既不无能也不邪恶的情况下使用此功能。

        【讨论】:

        • 如果你能保证你的代码永远不会被比你能力差的程序员阅读或编辑。
        • 如果您有足够的能力始终完美地使用 with:您只需声明一个局部变量别名,因为这样代码将是 explicit 并且在未来更具可读性,但现在要多输入几秒钟。
        【解决方案7】:

        你在打字时节省的东西,你会失去可读性。 许多调试器也不知道您指的是什么,因此调试更加困难。 它不会使程序运行得更快。

        考虑将 with 语句中的代码作为您所引用对象的方法。

        【讨论】:

          【解决方案8】:

          在工作中,我们为从现有 Win 32 代码库中删除 Withs 打分,因为维护使用它们的代码需要额外的努力。我在之前的工作中发现了几个错误,其中一个名为 BusinessComponent 的局部变量被一个带有相同类型的已发布属性 BusinessComponent 的对象的 With begin 块中所掩盖。编译器选择使用已发布的属性,本应使用局部变量的代码崩溃了。

          我见过类似的代码

          用 a,b,c,d 做{除了它们的名字要长得多,这里只是缩短了) 开始 我 := xyz;
          结束;

          试图找到 xyz 的来源可能会很痛苦。如果是 c,我会更早写成

          i := c.xyz;

          您认为理解这一点很简单,但在一个 800 行长的函数中却没有,而且一开始就使用了 with!

          【讨论】:

            【解决方案9】:

            “with”不太可能使代码运行得更快,编译器更有可能将其编译为相同的可执行代码。

            人们不喜欢“with”的主要原因是它可能会混淆命名空间范围和优先级。

            在某些情况下这是一个真正的问题,而在某些情况下这是一个非问题(非问题情况将如问题中所述“合理使用”)。

            由于可能造成混淆,一些开发人员选择完全避免使用“with”,即使在可能不存在这种混淆的情况下也是如此。这可能看起来很教条,但是可以争辩说,随着代码的变化和增长,即使在代码被修改到会使“with”混淆的程度之后,“with”的使用可能仍然存在,因此最好不要首先介绍它的用途。

            【讨论】:

            • 我曾经在优化一些经常调用的代码时对使用 with 进行了一些非常有限的测试。它没有改善运行时间。如果这是一个问题,比如数组查找,那么临时变量就是解决方案。编译器非常聪明,通常最好不要管它。
            【解决方案10】:

            我们最近在 Delphi 编码标准中禁止了它。

            利大于弊。

            这是由于误用而引入的错误。这些并不能证明节省编写或执行代码的时间是合理的。

            是的,使用 with 可以(稍微)加快代码执行速度。

            在下面,foo 只被评估一次:

            with foo do
            begin
              bar := 1;
              bin := x;
              box := 'abc';
            end
            

            但是,这里它被评估了三遍:

            foo.bar := 1;
            foo.bin := x;
            foo.box := 'abc';
            

            【讨论】:

            • 确定对此有信心吗?如果编译器不够聪明,无法将第二种情况优化为第一种情况,我会感到非常惊讶。
            • 非常确定。 Delphi 是主要的环境。在我的日常工作中,我非常熟悉它。由于普遍的静态类型,Pascal 的性质使优化变得容易,但编译器执行的优化现在在技术上落后了几代。这个编译器没有我们聪明。
            • 如果您将 foo 存储在局部变量中,那么您将获得与“with”相当的性能水平。
            • 完全正确。访问函数类型属性(函数调用,非内联预Delphi10)和实例变量(vtable查找)时,性能会受到影响。如果您使用本地并注意将访问一个接一个地放置,它同样快作为 with 子句。
            • 如果 Foo 是一个函数,或者由 GetFoo 方法调用的属性,编译器必须每次都执行该函数,因为它可能有其他副作用(通常是糟糕的设计,但确实会发生)。
            【解决方案11】:
            ...跑得更快...

            不一定 - 您的编译器/解释器通常比您更擅长优化代码。

            我觉得这让我说“糟糕!”因为它很懒——当我阅读代码(尤其是其他人的)时,我喜欢看明确的代码。所以我什至会在 Java 中写“this.field”而不是“field”。

            【讨论】:

              【解决方案12】:

              这主要是一个维护问题。

              从语言的角度来看,WITH 的想法是合理的,而且当合理使用时,它可以保持代码更小更清晰的论点具有一定的有效性。然而问题是,大多数商业代码将在其生命周期内由几个不同的人维护,并且最初是一个小的、易于解析的构造,在编写时很容易随着时间的推移变成笨拙的大型结构,其中 WITH 的范围不是维护者很容易解析。这自然会产生错误,并且很难找到错误。

              例如,假设我们有一个小函数 foo,其中包含三或四行代码,这些代码已包装在 WITH 块中,那么确实没有问题。然而几年后,这个函数可能已经在几个程序员的带领下扩展为 40 或 50 行代码,仍然包裹在 WITH 中。现在这很脆弱,并且引入错误的时机已经成熟,特别是如果维护者星标引入额外的嵌入式 WITH 块。

              WITH 没有其他好处 - 代码应该以完全相同的方式解析并以相同的速度运行(我在 D6 中用于 3D 渲染的紧密循环中对此进行了一些实验,我找不到任何区别)。调试器无法处理它也是一个问题 - 但是应该在一段时间前修复它,如果有任何好处,则值得忽略。可惜没有。

              【讨论】:

                【解决方案13】:

                如果with 语句可以通过以下方式扩展,那就太好了:

                with x := ARect do
                begin
                  x.Left := 0;
                  x.Rigth := 0;
                  ...
                end;
                

                您不需要声明变量“x”。它将由编译器创建。写起来很快,不用混淆,用的是哪个函数。

                【讨论】:

                • 在 XE2 和更新版本中只需调用:x.Create(left,top,right,bottom);。不再需要使用with。许多标准类型都是用advanced record 类型实现的。
                【解决方案14】:

                你可以结合 with 语句,所以你最终得到 ​​p>

                with Object1, Object2, Object3 do
                begin
                  //... Confusing statements here
                end
                

                如果你认为调试器被 1 混淆了,我看不出有人能确定 with 块中发生了什么

                【讨论】:

                  【解决方案15】:

                  对于 Delphi 2005,with-do 语句中存在硬错误 - 评估指针丢失并用指针向上替换。必须使用局部变量,而不是直接使用对象类型。

                  【讨论】:

                  • 你能改写你的答案吗?很难理解你想说什么。
                  【解决方案16】:

                  事实上:

                  procedure TBitmap32.FillRectS(const ARect: TRect; Value: TColor32);
                  begin
                    with ARect do FillRectS(Left, Top, Right, Bottom, Value);
                  end;
                  

                  procedure TBitmap32.FillRectS(const ARect: TRect; Value: TColor32);
                  begin
                    FillRectS(ARect.Left, ARect.Top, ARect.Right, ARect.Bottom, Value);
                  end;
                  

                  将生成完全相同的汇编代码。

                  如果with 子句的值是函数或方法,则可能存在性能损失。在这种情况下,如果您想要良好的维护和良好的速度,只需执行编译器在后台执行的操作,即创建一个临时变量

                  事实上:

                  with MyRect do
                  begin
                    Left := 0;
                    Right := 0;
                  end;
                  

                  由编译器以伪代码编码:

                  var aRect: ^TRect;
                  
                  aRect := @MyRect;
                  aRect^.Left := 0;
                  aRect^.Right := 0;
                  

                  那么aRect可以只是一个CPU寄存器,但也可以是一个真正的堆栈临时变量。当然,我在这里使用指针,因为TRectrecord。对于对象来说更直接,因为它们已经是指针了。

                  就个人而言,我有时会在我的代码中使用 with,但我几乎每次生成 asm 时都会检查它以确保它完成了它应该做的事情。不是每个人都能或有时间这样做,所以恕我直言,局部变量是一个很好的替代方案。

                  我真的不喜欢这样的代码:

                  for i := 0 to ObjList.Count-1 do
                    for j := 0 to ObjList[i].NestedList.Count-1 do
                    begin
                      ObjList[i].NestedList[j].Member := 'Toto';
                      ObjList[i].NestedList[j].Count := 10;
                    end;
                  

                  它仍然很容易阅读:

                  for i := 0 to ObjList.Count-1 do
                    for j := 0 to ObjList[i].NestedList.Count-1 do
                    with ObjList[i].NestedList[j] do
                    begin
                      Member := 'Toto';
                      Count := 10;
                    end;
                  

                  甚至

                  for i := 0 to ObjList.Count-1 do
                    with ObjList[i] do
                    for j := 0 to NestedList.Count-1 do
                    with NestedList[j] do
                    begin
                      Member := 'Toto';
                      Count := 10;
                    end;
                  

                  但如果内部循环很大,则局部变量确实有意义:

                  for i := 0 to ObjList.Count-1 do
                  begin
                    Obj := ObjList[i];
                    for j := 0 to Obj.NestedList.Count-1 do
                    begin
                      Nested := Obj.NestedList[j];
                      Nested.Member := 'Toto';
                      Nested.Count := 10;
                    end;
                  end;
                  

                  这段代码不会比with慢:编译器实际上在幕后完成!

                  顺便说一句,它可以让调试更容易:你可以下一个断点,然后将鼠标直接指向ObjNested 以获取内部值。

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 1970-01-01
                    • 2010-12-22
                    • 2010-10-07
                    • 1970-01-01
                    • 1970-01-01
                    • 2010-11-18
                    • 2016-07-23
                    相关资源
                    最近更新 更多