VB.NET的‘With’语句 - 接受还是避免?

73

在工作中,我经常在项目中处理对象的多个属性,在它们的构造过程或生命周期早期进行设置。为了方便和可读性,我通常使用 With 语句来设置这些属性。我发现

With Me.Elements
    .PropertyA = True
    .PropertyB = "Inactive"
    ' And so on for several more lines
End With

比起之前看起来好多了

Me.Elements.PropertyA = True
Me.Elements.PropertyB = "Inactive"
' And so on for several more lines

对于仅简单设置属性的非常长的语句。

我注意到在调试时使用With存在一些问题;但是,我想知道是否有任何强烈的理由在实践中避免使用With?我始终认为编译器生成的上述两种情况的代码基本相同,这就是为什么我始终选择编写我认为更易读的代码。


6
在问题表单中,将问题标题+1。建议在问题正文中,如果可能的话,请提及使用“With”语句时遇到的一些调试问题。 - Geoffrey
10个回答

70

如果您有较长的变量名称并且最终会得到:

UserHandler.GetUser.First.User.FirstName="Stefan"
UserHandler.GetUser.First.User.LastName="Karlsson"
UserHandler.GetUser.First.User.Age="39"
UserHandler.GetUser.First.User.Sex="Male"
UserHandler.GetUser.First.User.Occupation="Programmer"
UserHandler.GetUser.First.User.UserID="0"
....and so on

那么我会使用 WITH 语句使其更易读:

With UserHandler.GetUser.First.User
    .FirstName="Stefan"
    .LastName="Karlsson"
    .Age="39"
    .Sex="Male"
    .Occupation="Programmer"
    .UserID="0"
end with

在后面的例子中,甚至比第一个例子还有性能优势,因为在第一个例子中,每次访问用户属性时都要获取用户,而在WITH情况下,我只获取一次用户。

我可以像这样获得性能增益,而不使用with:

dim myuser as user =UserHandler.GetUser.First.User
myuser.FirstName="Stefan"
myuser.LastName="Karlsson"
myuser.Age="39"
myuser.Sex="Male"
myuser.Occupation="Programmer"
myuser.UserID="0"

但我会选择使用WITH语句,它看起来更简洁。

我只是以这个为例子,不要因为有很多关键词的类而抱怨,另一个例子可能是:WITH RefundDialog.RefundDatagridView.SelectedRows(0)。


7
Embrace请将文本翻译成中文。仅返回翻译后的文本:+1 也解释性能差异。建议:在答案顶部以粗体形式放置“Embrace”。 - Geoffrey
2
编译器在大多数情况下不会优化掉这种性能提升吗? - Alxandr
1
如果VB.NET编译器没有执行这个简单的优化,我会非常惊讶。这两个代码片段很可能被编译成相同的MSIL。 - Saeb Amini
1
你的示例并没有展示出良好的编码实践。请参考下面 @ljorquera 的回答。 - David
然而,即使是使用非常长的变量“链”的极端示例,您也可以避免使用 With:Dim user = UserHandler.GetUser.First.User。现在继续使用“user”变量。 - Tim Schmelter
@TimSchmelter,在我的回答末尾已经讲过了这一点。在我看来,使用With语句仍然更加整洁。 - Stefan

24
在实践中,没有真正令人信服的反对意见。我不是粉丝,但这是个人喜好,没有经验证据表明With结构不好。在.NET中,它编译成与完全限定对象名称相同的代码,因此这种语法糖不会带来性能损失。我通过编译,然后反汇编以下VB .NET 2.0类来确定这一点。
Imports System.Text

Public Class Class1
    Public Sub Foo()
        Dim sb As New StringBuilder
        With sb
            .Append("foo")
            .Append("bar")
            .Append("zap")
        End With

        Dim sb2 As New StringBuilder
        sb2.Append("foo")
        sb2.Append("bar")
        sb2.Append("zap")
    End Sub
End Class

以下是拆卸过程--请注意,对于sbWith语句调用和sb2Append方法调用看起来完全相同:
.method public instance void  Foo() cil managed
{
  // Code size       91 (0x5b)
  .maxstack  2
  .locals init ([0] class [mscorlib]System.Text.StringBuilder sb,
           [1] class [mscorlib]System.Text.StringBuilder sb2,
           [2] class [mscorlib]System.Text.StringBuilder VB$t_ref$L0)
  IL_0000:  nop
  IL_0001:  newobj     instance void [mscorlib]System.Text.StringBuilder::.ctor()
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  stloc.2
  IL_0009:  ldloc.2
  IL_000a:  ldstr      "foo"
  IL_000f:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string)
  IL_0014:  pop
  IL_0015:  ldloc.2
  IL_0016:  ldstr      "bar"
  IL_001b:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string)
  IL_0020:  pop
  IL_0021:  ldloc.2
  IL_0022:  ldstr      "zap"
  IL_0027:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string)
  IL_002c:  pop
  IL_002d:  ldnull
  IL_002e:  stloc.2
  IL_002f:  newobj     instance void [mscorlib]System.Text.StringBuilder::.ctor()
  IL_0034:  stloc.1
  IL_0035:  ldloc.1
  IL_0036:  ldstr      "foo"
  IL_003b:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string)
  IL_0040:  pop
  IL_0041:  ldloc.1
  IL_0042:  ldstr      "bar"
  IL_0047:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string)
  IL_004c:  pop
  IL_004d:  ldloc.1
  IL_004e:  ldstr      "zap"
  IL_0053:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(string)
  IL_0058:  pop
  IL_0059:  nop
  IL_005a:  ret
} // end of method Class1::Foo

如果您喜欢它并且觉得更易读,请使用它;没有什么强制性的理由不这样做。
(顺便说一下,Tom,我很想知道调试器发生了什么事情 - 我不记得曾经看到过基于With语句的调试器出现任何异常行为,所以我很好奇您看到了什么行为。)

7
找到 With 语句的开头并设置断点。单步执行到下一行(这样你就可以隐藏在 if 块下面的第一行)。将其突出显示,然后添加监视器。你应该会看到这个提示:“With” 上下文和语句在调试窗口中无效。 - Tom
1
啊,有趣!我在调试中很少使用观察窗口,这就是为什么我从未遇到过它的原因。我得尝试一下 - 也许,为了好玩,我可以在2003年、2005年和2008年之间尝试一下,看看它们是否有不同的行为。感谢您的提示! - John Rudy
5
如果使用With与可变值类型(如Rectangle)的数组一起使用,生成的代码将不同于没有With时生成的任何代码。 - supercat
1
你无法“观察”它,因为它并不存在,它只是堆栈上的一个副本。当它超出作用域时,对它所做的更改也会消失!我猜它很好地实现了“不可变数据”的概念。 - user4624979
@不理解 - 不,那不对。WITH..建立了一个不变的本地指针指向原始对象,而不是对象的副本。更改将按预期应用于对象。当它超出范围时,被销毁的是本地指针。 - JohnRC

15

使用With语句和重复引用对象之间存在微妙的区别,应该牢记在心。

当使用With语句时,它会创建一个新的本地变量来引用对象。随后使用.xx的引用是对该本地引用属性的引用。如果在执行WITH语句时,原始变量引用发生更改,则由WITH引用的对象不会更改。考虑以下情况:

Dim AA As AAClass = GetNextAAObject()
With AA
    AA = GetNextAAObject()

    '// Setting property of original AA instance, not later instance
    .SomeProperty = SomeValue
End With
因此,WITH语句不仅是语法糖,而且实际上是一种不同的结构。尽管您不太可能编写像上面这样明确的代码,但在某些情况下,可能会无意中发生这种情况,因此您应该注意此问题。最有可能的情况是,您可能正在遍历一个结构,例如由设置属性隐含更改的对象网络。

1
更新本地副本与原始副本之间的区别并不是我所谓的“微妙”差别,更像是“代码损坏”与“代码正常”。在许多可能的情况下,带有块的代码会导致“代码损坏”。它就像一个指针反向,它取消指向实际数据。您的更改对您来说是未更改的。这样的编程结构有什么用途? - user4624979
1
对于您的评论长时间未作回应,我深表歉意。我认为说“代码已损坏”是不正确的。虽然WITH块的工作方式对于不谨慎的人来说确实是个陷阱,但是在我看来,实现的方式是最健壮的。WITH语句建立了一个本地、临时、不变的指向对象的指针(而不是对象的本地副本),在WITH语句的范围内,无论代码有什么副作用,它都将可靠地引用同一个对象。我认为这很清晰,也很明智。 - JohnRC
可能差异在于处理结构(记录)时,而不是对象和对它们的引用。至少如果我理解上面关于“制作副本”的文本。 - George Birbilis
哎呀,我查了一下,似乎结构体在那里有点被支持,但只能以只读方式使用。请看我的答案(只是添加它)来了解更多信息。 - George Birbilis

13

这完全是关于可读性的。像所有的语法糖一样,这可以被过度使用

如果您需要在几行中设置一个对象的多个成员,那么就采用它吧。

With myObject
  .Property1 = arg1
  .Property2 = arg2
...

避免在"With"块中进行其他操作

如果您编写了一个跨越50-100行并涉及许多其他变量的With块,则很难记住在块的顶部声明了什么。出于明显的原因,我不会提供这样混乱代码的示例。


6
我在这里寻找这样的答案。比起只有一个屏幕高度的块,with块更长会给读者带来不必要的负担。 - JKomusin

6

如果使用它可以使代码更易读,请使用它。如果使用它会使代码易读,请避免使用 - 特别是建议避免嵌套With语句。

C# 3.0仅具有初始化对象的此功能:

var x = new Whatever { PropertyA=true, PropertyB="Inactive" };

这不仅在LINQ中非常必要,而且从语法无法表明代码异味的角度来看也是有道理的。我通常发现,当我对一个对象执行许多不同的操作超出了其初始构造时,这些操作应该被封装为对象本身的单个操作。
关于你的示例,有一个注意事项 - 你真的需要“Me”吗?为什么不直接写:
PropertyA = True
PropertyB = "Inactive"

当然,在这种情况下,"我"是被暗示的...


我只是为了提供一个简洁的例子,而不是真正展示我在实际应用中如何使用。 - Tom
1
难道不是很典型吗,从虚无中提取现实的例子有多么困难...它们似乎总比真实世界的情况简单,并且总有人想出一种方法来简化这个例子,但并没有简化一般情况。 - bart
我不太明白为什么你建议避免嵌套 'With ... End With' 语句?当我在vba和通过COM处理Excel时,我经常这样做,因为随着我深入到属性级别,它确实让我的生活更轻松。有没有具体的理由我应该避免这样做? - yu_ominae
@yu_ominae:如果有共同的名称,那么会更加困难 - 而且通常很难知道哪个名称与哪个对象相关联。我建议明确指定每个源和目标。 - Jon Skeet
1
@JonSkeet 非常迅速!在我写完评论之后,我想到了您可能是指避免嵌套“With ... End With”块相互引用不同的对象。如果在同一对象结构内使用以缩写语法为目的,那么这样做可能是可以接受的...当然,这也取决于块的大小。我从未真正考虑过这个问题,所以感谢您让我思考这个问题。 - yu_ominae

5

如果代码中大量使用this关键字,我会怀疑它的质量。如果它被用来方便地设置许多实例变量或属性,那么这可能表明你的类太大了(Large Class smell)。如果你使用它来替换长串的调用,就像这样:

UserHandler.GetUser.First.User.FirstName="Stefan"
UserHandler.GetUser.First.User.LastName="Karlsson"
UserHandler.GetUser.First.User.Age="39"
UserHandler.GetUser.First.User.Sex="Male"
UserHandler.GetUser.First.User.Occupation="Programmer"
UserHandler.GetUser.First.User.UserID="0"

如果您这样做的话,很可能违反了 Demeter法则

这只是为了举个例子。它可以是长变量名与一个或两个关键词的组合。 - Stefan
1
例如:WITH RefundDialog.RefundDatagridView.SelectedRows(0) - Stefan
1
我认为将包含在RefundDialog中的整个GridView暴露给外部代码也违反了Demeter法则。根据我的经验,几乎每次使用"With"语句时,都会发生一些Demeter法则的违规情况。长变量名可能是With语句的一个例子,但我发现开发人员必须小心,不要让With块变得太大,否则很难跟踪哪个对象构成了"With context"。 - Jeremy Wiebe
同意。只要遵循良好的面向对象原则,就不需要像这样多次"跳转"到对象中。如果你只是处理立即对象引用上的属性或方法,则 With 语句变得不那么有用了。 - David

3

'with' 基本上是 Smalltalk 中的 'cascade'。这是肯特·贝克(Kent Beck)在他的《Smalltalk 最佳实践模式》一书中介绍的一种模式。

该模式的概述:当对对象发送的消息可以分组时,请使用它。如果只是向同一对象发送一些消息,则不要使用它。


什么时候会有意义,什么时候又没有意义呢? - Geoffrey

3

我不使用VB.NET(我曾经使用过普通的VB)但是...

前导点是否必需?如果是这样,那么我就看不出有什么问题了。在Javascript中,使用with的结果是一个对象的属性看起来就像一个普通的变量,而这非常危险,因为你无法确定你正在访问一个属性还是一个变量,因此,with是需要避免的。

不仅使用起来更加方便,而且对于重复访问对象的属性,它很可能更快,因为对象只通过方法链获取一次,而不是每个属性都获取一次。

我同意其他回答的意见,你应该避免嵌套使用with,原因与在Javascript中避免with完全相同:因为你不再能看到你的属性属于哪个对象。


2
尽量避免使用With Block(即使可读性会变差)。有两个原因:
  1. Microsoft的文档指出,在某些情况下,它会在堆栈上创建数据副本,所以您所做的任何更改都将被丢弃。
  2. 如果您将其用于LINQ查询,则lambda结果不会链接,因此每个中间子句的结果都会被丢弃。

为了描述这一点,我们有一个(错误的)例子来自我同事问作者的教科书(确实是不正确的,名称已更改以保护...什么):

With dbcontext.Blahs
.OrderBy(Function(currentBlah) currentBlah.LastName)
.ThenBy(Function(currentBlah) currentBlah.FirstName)
.Load()
End With

OrderBy和ThenBy完全没有效果。如果您仅删除With和End With,并在前三行末尾添加行继续字符...它可以工作(如同一教科书15页后所示)。

我们不需要更多理由来消灭With Blocks。它们只在解释型框架中有意义。


1
使用 With 结构时需要注意,如果你想设置结构体中的字段值,是不行的。因为在 With 块中,你只是在使用“with”表达式的本地副本,而不是在使用(副本的)对象引用。对象表达式的数据类型可以是任何类或结构类型,甚至是 Visual Basic 的基本类型,如 Integer。如果 objectExpression 的结果不是对象,则只能读取其成员的值或调用方法。如果你尝试为在 With...End With 语句中使用的结构体的成员分配值,就会出现错误。这与你调用返回结构体并立即访问和分配函数结果的成员,例如 GetAPoint().x = 1 时出现的错误相同。在两种情况下都存在问题,因为结构体仅存在于调用堆栈上,在这些情况下修改结构体成员的方式无法写入位置,以便程序中的任何其他代码都可以观察到更改。对象表达式在进入块时被计算一次。你不能在 With 块内重新分配对象表达式。

https://learn.microsoft.com/en-us/dotnet/visual-basic/language-reference/statements/with-end-with-statement

猜测编译器如果您将结构名称传递给with语句而不是返回结构的表达式,可能会更聪明,但似乎并没有。

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