Delphi中的"with"关键字是一种不好的实践吗?

35

我已经阅读了一些关于 Delphi 中 with 关键字的不好的评价,但是我认为,只要不滥用它,它能使你的代码看起来更简单。

我经常把所有的 TClientDataSet 和 TField 放在 TDataModule 中。因此,在我的表格中,我会有这样的代码:

procedure TMyForm.AddButtonClick(Sender: TObject);
begin  
  with LongNameDataModule do
  begin
     LongNameTable1.Insert;
     LongNameTable1_Field1.Value := "some value";
     LongNameTable1_Field2.Value := LongNameTable2_LongNameField1.Value;
     LongNameTable1_Field3.Value := LongNameTable3_LongNameField1.Value;
     LongNameTable1_Field4.Value := LongNameTable4_LongNameField1.Value;
     LongNameTable1.Post;
  end
end;

没有使用with关键字,我必须像这样编写代码

    procedure TMyForm.AddButtonClick(Sender: TObject);
    begin            
      LongNameDataModule.LongNameTable1.Insert;
      LongNameDataModule.LongNameTable1_LongNameField1.Value := "some value";

      LongNameDataModule.LongNameTable1_LongNameField2.Value :=
               LongNameDataModule.LongNameTable2_LongNameField1.Value;

      LongNameDataModule.LongNameTable1_LongNameField3.Value :=
               LongNameDataModule.LongNameTable3_LongNameField1.Value;

      LongNameDataModule.LongNameTable1_LongNameField4.Value :=
               LongNameDataModule.LongNameTable4_LongNameField1.Value;

      LongNameDataModule.LongNameTable1.Post;
    end;

我认为使用with关键字更容易阅读。

我应该避免使用with关键字吗?


虽然我同意你在这种情况下使用with的做法,但是如果没有这种数据模块的使用方式,它并不是必要的,而这种方式违反了Demeter法则。Delphi需要表单/数据模块中的所有组件都是公共的,但最好不要过多地使用它(们)。 - mghie
1
重复:https://dev59.com/d3VD5IYBdhLWcg3wJIEK - Lars Truijens
它可以用于优化目的,例如避免额外的解引用操作。 - arthurprs
2
@arthurprs 声明一个本地变量用作别名,就像你所认为的“优化”一样有效 - 或许更有效,因为现在你完全控制了该本地变量。 - Disillusioned
14个回答

64

使用with的最大危险,在于除了像“with A,B,C,D”这样的病态条件外,你的代码可能会在没有任何提示的情况下默默地改变含义。考虑以下示例:

with TFoo.Create
try
  Bar := Baz;
  DoSomething();
finally
  Free;
end;

你编写这段代码时知道Bar是TFoo的属性,而Baz是包含此代码的方法所属类型的属性。

现在,两年后,一些好心的开发人员添加了一个Baz属性到TFoo中。你的代码默默地改变了含义。编译器不会报错,但代码现在已经失效了。


15
嗯嗯...那真的很可怕。 - Fabricio Araujo

32

with关键字是一项使代码更易读的好功能,但也存在一些陷阱。

调试:

当使用如下代码时:

with TMyClass.Create do
try
  Add('foo');
finally
  Free;
end;

无法检查此类的属性,因此始终声明变量并在其中使用with关键字。

接口:

with子句中创建接口时,它将存在于方法的结尾:

procedure MemoryHog;
begin
  with GetInterfaceThatTakes50MBOfMemory do
    Whatever;
  ShowMessage('I''m still using 50MB of memory!');
end;

清晰度

在使用一个类进入with语句时,如果类中的属性或方法名已经存在于当前作用域中,那么这很容易让你产生错觉。

with TMyForm.Create do
  Width := Width + 2; //which width in this with is width?

当然,当存在重复的名称时,您使用在 with 语句中声明的类(TMyForm)的属性和方法。


1
“with” 结构在 Delphi 7 的调试器中让人困惑不已。我没有使用过更高版本的 Delphi,也不知道这种糟糕的行为是否得到了改善。 - Johan Buret
1
至少在BDS2006之前,它没有得到改进。D2007及以后的版本,我不清楚。 - Fabricio Araujo

22
with语句有其适用的场景,但我同意过度使用会导致代码含义不明确。一个好的经验法则是,在添加with语句后,确保代码变得“更加”可读和可维护。如果你在添加语句后觉得需要添加注释来解释代码,那么这可能是个坏主意。如果像你的例子一样,代码变得更可读了,那就使用它吧。
顺便说一下:在Delphi中,这始终是我最喜欢的显示模态窗口的模式之一。
with TForm.Create(nil) do
try
  ShowModal;
finally
  Free;
end

1
同意。使用“with”既是好习惯也是坏习惯。过一段时间你会学会如何把握分寸。 - user34411
10
模态表单的另一个解决方案是添加一个新的静态方法 "CreateShowModalAndFree" ;) ... (注:原文中的“;)”为表情符号,表示开玩笑或者调皮的意思) - mjn
1
使用关键字 with 的另一个好处是,你经常可以写 finally free。这是一种解放感。 - nurettin

15

我倾向于完全禁用with语句。正如之前所述,它可能会让事情变得复杂,我的经验是确实如此。由于with语句,调试器很多时候无法评估值,并且我经常发现嵌套的with语句导致代码难以阅读。

Brian的代码看起来简洁易读,但如果您直接对发送器进行类型转换并删除所有关于该组件的疑虑,代码将更短:

TAction(Sender).Enabled := Something;

如果您担心打太多字,我建议将长命名对象做一个临时引用:

var
  t: TTable;
begin
  t := theLongNamedDataModule.WithItsLongNamedTable;
  t.FieldByName(' ');
end;

我不明白为什么打字会让你烦恼。我们首先是打字员,其次才是程序员。代码补全、复制粘贴和键盘录制可以帮助你成为更有效的打字员。 点击此处阅读更多。

更新: 刚刚偶然发现了一篇长文章,其中有一个小节介绍了with语句:它是语言中最丑陋、最危险、最容易导致错误的功能。 :-)


我担心(过于严谨)应该将其类型转换为(Sender as TAction).Enabled := Something,因为这样会执行运行时检查,以确保Sender确实是从TAction派生的,如果不是,则会引发一个错误,而直接类型转换会导致崩溃。 - Toby Allen
但是为什么发送者不应该是TAction以外的任何东西呢?如果我为不同类型的组件使用相同的事件处理程序,我将需要检查生成事件的发送者的类型,但是当我将事件连接到一个特定的组件时,为什么我应该期望发送者是其他东西呢? - Vegar
3
不是关于打字,而是关于代码清晰度。感谢反馈。 - Marioh
1
@Marioh 很好的观点。with 块内的代码不清晰,因为每个标识符都需要评估以确定它是“with member”还是外部作用域成员。 - Disillusioned
@Vegar 我会用不同的方式提问。为什么你要进行不安全的转换,而不是使用安全的转换呢?难道没有其他选择吗?...让我们希望编译器也知道这一点,并消除检查。 - maaartinus
不确定现在的情况如何,但回想起我使用 Delphi 时,安全类型转换并没有被优化掉。但是可以肯定的是:类型检查可能不是您主要的性能问题 :-) - Vegar

8
当我第一次开始使用Pascal编程(使用TurboPascal!)并边学边用时,WITH语句似乎很棒。就像你所说的,它可以减少繁琐的输入,非常适合处理长记录。自从Delphi出现后,我一直在删除它,并鼓励其他人也不要使用它——正如Verity在the register中所总结的那样。 除了降低可读性之外,我避免使用它的两个主要原因是:
  1. 如果您使用类,则根本不需要它——只有记录“似乎”从中受益。
  2. 使用调试器使用Ctrl-Enter跟踪代码到声明的位置无法正常工作。
话虽如此,为了提高可读性,我仍然使用以下语法:
procedure ActionOnUpdate( Sender : TObject )
begin
  With Sender as TAction do
    Enabled := Something
end;

我从未见过一个更好的结构。

首先,TAction(Sender).Enabled := Something; 是我偏好的写法,因为它更易读和清晰。 - Kromster
同时 (Sender as TAction).Enabled := Something 也能解决问题。 - adlabac

6

在我看来,你提供的一个在按钮点击中访问数据模块的示例是一个不太恰当的示例。如果将这段代码移动到应该所在的数据模块中,那么对WITH的需求就消失了。然后OnClick只需调用LongNameDataModule.InsertStuff,不需要使用with。

使用with并不是一个好的做法,你应该检查你的代码,看看为什么需要它。你可能做错了什么,或者可以通过其他更好的方式实现。


6
正如Vegar所提到的,使用临时引用不仅整洁,而且更易读,更易于调试,也不容易出现潜在问题。
到目前为止,我从未发现需要使用with的情况。我曾对此持中立态度,直到接手一个经常使用双重with的项目。怀疑原始开发人员是想引用第一个with中的项目还是第二个with中的项目,如果这种歧义引用是with-slip或笨拙的代码,那么尝试调试它的折磨以及扩展或修改使用这些可恶代码的类所带来的影响,都不值得花费任何人的时间。
显式代码更易读。这样你就可以既拥有蛋糕,又能享受它的美味。
procedure TMyForm.AddButtonClick(Sender: TObject);
var
  dm: TLongNameDataModuleType
begin  
  dm:=LongNameDataModule;

  dm.LongNameTable1.Insert;
  dm.LongNameTable1_Field1.Value := "some value";
  dm.LongNameTable1_Field2.Value := LongNameTable2_LongNameField1.Value;
  dm.LongNameTable1_Field3.Value := LongNameTable3_LongNameField1.Value;
  dm.LongNameTable1_Field4.Value := LongNameTable4_LongNameField1.Value;
  dm.LongNameTable1.Post;
end;

5
我坚信在Delphi中删除WITH支持。您使用带有命名字段的数据模块的示例用法是我能想到的唯一一种情况,它可能有效。否则,Craig Stuntz提出的最佳反对意见我也同意。
我只是想指出随着时间的推移,您最终(应该)会删除所有OnClick事件中的编码,您的代码也将最终从数据模块的命名字段迁移到使用包装此数据的类,并且使用WITH的原因将消失。

5
您的问题是“锤子并不总是解决问题”的绝佳例子。
在这种情况下,“with”不是您的解决方案:您应该将此业务逻辑从表单中移出,放到数据模块中。如果不这样做,就会违反迪米特法则,正如mghie(Michael Hieke)所评论的那样。
也许您的示例只是为了说明,但如果您实际在项目中使用这样的代码,那么您应该采取以下措施:
procedure TLongNameDataModule.AddToLongNameTable1(const NewField1Value: string);
begin  
  LongNameTable1.Insert;
  LongNameTable1_Field1.Value := NewField1Value;
  LongNameTable1_Field2.Value := LongNameTable2_LongNameField1.Value;
  LongNameTable1_Field3.Value := LongNameTable3_LongNameField1.Value;
  LongNameTable1_Field4.Value := LongNameTable4_LongNameField1.Value;
  LongNameTable1.Post;
end;

然后像这样从表单中调用它:

procedure TMyForm.AddButtonClick(Sender: TObject);
begin  
  LongNameDataModule.AddToLongNameTable1('some value');
end;

这样做可以有效地消除您的with语句,并同时使您的代码更易于维护。
当然,用单引号括起来Delphi字符串也有助于编译。

2
我正要写同样的东西,但是现在我读了这个答案后就不需要再写了。这个例子很不好,它明显违反了Demeter法则。但是关于“with”的争论超出了这个范围,还有其他的论点。 - Rafael Piccolo

3
就我而言,使用with在你所给出的情况下是相当可接受的。 它确实提高了代码的清晰度。
真正的问题是当您同时打开多个with时。
此外,我的观点是,您在使用with的对象会产生很大的差异。 如果它是一个完全不同的对象,那么with可能是一个坏主意。 然而,即使在这种情况下,我也不喜欢在一个级别上有很多变量--通常是包含整个非常复杂数据项的数据对象--通常是程序设计要处理的整个任务。 (我认为这种情况不会出现在没有这样一个项目的应用程序中。)为了使世界更清晰,我经常使用记录来组合相关项目。 我发现我使用的几乎所有的with都是用于访问这些子组。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接