VB.NET“带有”语句-拥抱还是避免?


71

在工作中,我经常从事一些项目,在这些项目中,必须在构造过程中或生命周期的早期设置某些对象的众多属性。为了方便和易读,我经常使用该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在实践中使用?我一直认为在以上两种情况下通过编译器生成的代码基本上是相同的,这就是为什么我总是选择编写自己认为更具可读性的代码。


4
+1以问题形式填写问题标题。建议:在问题正文中,With如果可能的话,请提及您遇到的一些调试问题。
杰弗里

Answers:


68

如果您有很长的变量名,并且最终以:

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

在后面的示例中,与第一个示例相比,它甚至具有性能上的优势,因为在第一个示例中,每次访问用户属性时,Im都会提取用户,而在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)


6
+1也用于解释性能差异。建议:将“拥抱”以粗体显示在答案的顶部。
杰弗里

2
在大多数情况下,编译器是否无法优化这种性能增益?
Alxandr

1
如果VB.NET编译器没有执行此简单优化,我会感到非常惊讶。这两个代码段很可能被编译为相同的MSIL。
Saeb Amini

1
您的示例没有证明良好的编码习惯。请参阅下面的@ljorquera的答案。
戴维(David)

然而,即使有很长的可变极端的例子“链”你能避免附:Dim user = UserHandler.GetUser.First.User。现在继续使用user变量。
蒂姆·施密特

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

反汇编如下-请注意,对sb2Append方法的调用与对的With语句的调用相同sb

.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

因此,如果您喜欢它,并且发现它更具可读性,请继续尝试;没有令人信服的理由不这样做。

(顺便说一句,汤姆,我有兴趣知道调试器发生了什么-我不记得曾经在调试器中基于With语句看到过任何异常行为,所以我很好奇知道您确实看到了什么行为)


6
@John Rudy-查找With语句的开头并设置一个断点。转到下一行(因此,您将第一行隐藏在if块下)。突出显示它,然后单击“添加手表”。您应该看到:“带有”上下文和语句在调试窗口中无效。
汤姆”,

1
啊,有趣!我在调试中不经常使用Watches,这就是为什么我从未遇到过它。我将不得不尝试-也许是咧嘴笑,在2003年,2005年和2008年尝试一下,看看它们是否有不同的表现。谢谢你的提示!
John Rudy

5
如果With与可变值类型(例如Rectangle)的数组一起使用,则生成的代码将与不使用会产生的任何代码不同With
2012年

1
您不能“观看”它,因为它实际上不存在,它是堆栈上的副本。当它超出范围时,也要对其进行更改!我猜它很好地实现了“不变数据”的概念。

@no comprende-不,那是不对的。WITH ..建立指向原始对象的不变局部指针,而不是对象的副本。如您所愿,对对象进行了更改。当它超出范围时,将破坏本地指针。
JohnRC '16

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
更新本地副本与原始副本并不是我所说的“微妙”区别。更像是:“代码损坏”与“代码作品”。在许多可能的情况下,WITH Block =“代码已损坏”。就像反向指针一样,它未指向实际数据。您所做的更改没有更改。这种编程结构的用例是什么?

1
很抱歉延迟回复您的评论。我认为说“代码损坏”是不对的。当然,这是一个对粗心大意的陷阱,但是实施时WITH块的工作方式是最强大的方法。WITH语句建立指向该对象(不是该对象的本地副本)的局部,临时,不变的指针,并且在WITH语句的范围内将始终可靠地引用同一对象,而不管代码可能产生什么副作用。有。我认为这很清楚也很明智。
JohnRC '16

处理结构(记录)而不是对象和对其的引用时,差异可能会开始。至少如果我收到上面有关“复制”的文字
George Birbilis

糟糕,看起来有点像,似乎在那里支持结构,但只能以只读方式进行。看到我的回答(只需添加一下)
George Birbilis

12

这全都与可读性有关。像所有语法糖一样,它也可能被过度使用

如果 要在几行中设置对象的多个成员,请拥抱它

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

避免对“ With”进行其他操作

如果编写一个横跨50-100行并包含许多其他变量的With块,则可能很难记住该块顶部声明的内容。出于明显的原因,我不会提供此类凌乱代码的示例


6
我在这里寻找这样的答案。With超过一个屏幕高度的方块会给阅读器增加不必要的负担。
JKomusin 2012年

6

在使代码真正更具可读性的地方,请继续努力。当它使更少的可读性,避免它-尤其是,我建议你避免嵌套with语句。

C#3.0仅将此功能用于对象初始化:

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

这不仅是LINQ所必需的,而且在语法不表示代码气味的地方也很有意义。我通常会发现,当我在对象的初始构造之外执行许多不同的操作时,这些操作应封装为一个单独的对象。

关于您的示例的注释-您真的需要“我”吗?为什么不写:

PropertyA = True
PropertyB = "Inactive"

?当然,在这种情况下暗指“我” ...


我只是为了一个简洁的例子而做,并不是为了真正展示我在实际实践中的用法。
汤姆”,

1
凭空想出现实的例子难道不是很典型吗?它们似乎总是比真实情况更简单,总有人想出一种方法来简化例子,而不是简单的例子。
巴特

我真的不明白为什么您建议避免嵌套'With ... End With'语句?在vba和通过COM使用Excel时,我经常这样做,随着我深入到属性级别,这确实使我的生活更轻松。有什么具体原因为什么我应该避免这样做?
yu_ominae 2014年

@yu_ominae:如果存在通用名称,则将变得更加困难-并且通常更难于知道哪个名称与哪个对象相关联。我只是显式地指定每个源和目标。
乔恩·斯基特

1
@JonSkeet太快了!在写完我的评论后,我想到您可能要避免嵌套引用彼此不同对象的“ With ... End With”块。如果用于缩写语法而又停留在同一对象的结构中,那可能很好。当然,这取决于块的大小。我从未真正考虑过,所以感谢您让我考虑这个问题。
yu_ominae 2014年

5

我对使用大量此关键字的代码感到怀疑:如果用于简化设置大量实例变量或属性的代码,我认为这可能表明您的类太大(Large Class气味)。如果使用它来替换长呼叫链,例如:

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"

那你可能违反了得墨meter耳法则


只是为了举例。它可以是带有一个或两个关键字的长变量名
Stefan

1
例如,像这样:WITH RefundDialog.RefundDatagridView.SelectedRows(0)
Stefan

1
我认为将RefundDialog中包含的整个GridView暴露给外部代码也违反了Demeter法则。以我的经验,几乎每次使用“ With”语句时,都会发生一些违反Demeter的法则。较长的变量名可能是With语句的一种情况,但是我发现开发人员必须注意不要让With块变大,否则将很难跟踪组成“ With context”的对象。
杰里米·维比

同意 只要遵循良好的面向对象原则,就不需要在这样的对象中进行多次“跳跃”。而且,如果您仅在直接对象引用上处理属性或方法,则With语句的帮助会大大减少。
戴维(David)

3

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

前导点是强制性的吗?如果是这样,那么我认为没有问题。在Javascript中,使用的结果with是,一个物体的外观的属性完全一样作为一个普通的变量,是非常危险的,因为你没有看到,如果你访问属性或变量,因此,with是要避免的事情。

它的使用不仅在眼睛上更容易,而且对于重复访问对象的属性而言,它可能会更快,因为通过方法链仅一次(而不是为每个属性一次)获取对象。

我确实同意其他答复,您应该避免嵌套使用with,这与为什么with完全避免在Javascript中使用的原因相同:因为您不再看到属性属于哪个对象。


3

“ with”基本上是Smalltalk的“ cascade”。这是Kent Beck的Smalltalk最佳实践模式书中的一种模式。

模式的摘要:在有意义的情况下使用它来对发送到对象的消息进行分组。如果只是碰巧一些消息发送到同一对象,请不要使用它。


什么时候有意义,什么时候没有?
杰弗里

2

不惜一切代价避免使用WITH块(甚至是可读性)。两个原因:

  1. 关于使用微软的文档......最终随着说,在某些情况下,它会创建堆栈上的数据的副本,所以任何的变化,你都会得到扔掉。
  2. 如果将其用于LINQ查询,则lambda结果“请勿链接”,因此将丢弃每个中间子句的结果。

为了描述这一点,我们有一个教科书中的一个(残破的)示例,我的同事不得不向作者询问(这确实是不正确的,名称已更改以保护...等等):

使用dbcontext.Blahs
.OrderBy(Function(currentBlah)currentBlah.LastName)
.ThenBy(Function(currentBlah)currentBlah.FirstName)
.Load()
结尾为

在排序依据和ThenBy有没有影响的。如果仅通过删除With和End With并在前三行的末尾添加行连续字符来重新格式化代码,则它可以工作(如同一本教科书的15页所示)。

我们不需要任何其他原因来搜索和销毁WITH块。它们仅在解释框架中具有含义。


1

将其与结构一起使用时有一个陷阱,即无法设置其字段,因为您正在处理“ with”表达式的本地副本(在使用block输入时制作),而不是使用(副本)在这种情况下的对象引用):

objectExpression的数据类型可以是任何类或结构类型,甚至可以是Visual Basic基本类型,例如Integer。如果objectExpression导致对象以外的其他任何东西,则您只能读取其成员的值或调用方法,如果尝试为With ... End With语句中使用的结构的成员分配值,则会出现错误。如果调用一个返回结构的方法并立即访问该方法并将结果分配给函数结果的成员的方法(例如GetAPoint()。x = 1),则将得到相同的错误。两种情况下的问题是结构仅存在于调用堆栈中,在这种情况下,修改后的结构成员无法写入位置,以便程序中的任何其他代码都可以观察到更改。

进入该块后,对objectExpression进行一次评估。您不能从With块内重新分配objectExpression。

https://docs.microsoft.com/zh-cn/dotnet/visual-basic/language-reference/statements/with-end-with-statement

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.