人们为什么如此强烈地反对方法中的#region标签?


27

我听说过很多关于使方法简短的知识,而且我听过很多程序员说,在方法中使用#region标记是一个确定的信号,即它太长了,应该重构为多个方法。但是,在我看来,在许多情况下,在方法中使用#region标签分隔代码是重构为多个方法的绝佳解决方案。

假设我们有一种方法,其计算可以分为三个相当不同的阶段。此外,这些阶段中的每个阶段仅与方法的计算有关,因此将它们提取到新方法中不会获得代码重用。那么,将每个阶段提取到自己的方法中有什么好处?据我所知,我们获得的是每个阶段都有一定的可读性和单独的变量范围(这将有助于防止对特定阶段的修改意外破坏另一个阶段)。

但是,这两种方法都可以实现,而无需将每个阶段提取到自己的方法中。区域标签使我们可以将代码折叠成可读性强的形式(其附加好处是,如果我们决定扩展和检查代码,则不再需要在该文件中保留位置),并且只需将每个阶段包装在{}创建自己的工作范围。

这样做的好处是,我们不会使用实际上仅与第四个方法的内部工作相关的三个方法来污染类级别的范围。在我看来,立即将长方法重构为一系列短方法似乎是等同于过早优化的代码重用。您正在引入额外的复杂性以解决在许多情况下永远不会出现的问题。如果有出现代码重用的机会,您以后总是可以将阶段之一提取到其自己的方法中。

有什么想法吗?


4
我目前正在与一个相当大的班级一起工作。它遵循单一责任原则;该类中的所有内容都是必需的功能。它有很多私有方法,主要是由于重构。我使用#region将这些方法划分为功能类别,这对我们来说非常好。还有许多其他类在这个项目中这并没有要求这样的待遇。我说,正确的工具适合正确的工作。
罗伯特·哈维,

4
@RobertHarvey,在类中使用它们与在方法内部使用它们不同。
CaffGeek

18
@Chad:啊,没注意到。在一种方法中使用它们似乎有些极端。如果一个方法太长而需要#regions,我将其重构为单独的方法。
罗伯特·哈维

8
这并不是一个真正的答案,但我不仅讨厌所有#region标签,而且还完全关闭了Visual Studio中的代码折叠功能。我不喜欢试图向我隐藏的代码。
Daniel Pryden 2011年

2
“此外,这些阶段中的每一个仅与该方法的计算有关,因此将它们提取到新方法中不会获得代码重用。” OTOH,我发现如果将一个函数分解为3,则以后经常会发现各个部分<i>稍后会有用</ i>。如果一些输入数据的变化只有一个阶段,你可以测试在隔离等
杰克五,

Answers:


65

您只需要关心的是代码是可用的,而不是可重用的。如果根本没有任何转换,猴子可以将可用代码转换为可重用代码。

礼貌地说,“我只在这里需要这个”这一论点很差。您所描述的技术通常被称为头条新闻技术,并且通常被皱眉。

  1. 您不能测试区域,但是可以隔离地测试真实的方法。
  2. 区域是注释,而不是语法元素。在最坏的情况下,区域的嵌套和块的嵌套相互矛盾。您应该始终努力用所使用语言的语法元素来表示结构的语义。
  3. 重构为方法后,不再需要折叠即可阅读文本。可能正在使用任何不具有折叠功能的工具查看源代码,例如终端,邮件客户端,VCS的Web视图或差异查看器。
  4. 重构为方法后,所得方法至少与区域方法一样好,此外,移动各个零件也更为简单。
  5. 单一责任原则建议,任何单位都应该只有一项任务和一项任务。“ main”方法的任务是组合“ helper”方法以获得所需的结果。“帮助程序”方法隔离解决了一个离散的简单问题。包含所有这些方法的类也应该仅完成一项任务,并且仅包含与该任务相关的方法。因此,辅助方法要么属于类范围,要么代码不应首先位于类中,但至少应使用某些全局方法,或者更好的是应将其注入

同样相关:Jeff Atwoods关于代码折叠的思想


9
1.大多数程序员不会测试所有私有方法。2.方法名称不能传达关于该方法的所有知识。可能会抛出什么异常?null是有效输入吗?有时可以使用非语法结构使您的代码更易于理解。在许多语言中,文件是非语法元素的一个示例,该非语法元素被公认为组织代码的一种手段。3.出于多种原因,最好在IDE中尝试进入方法的内部工作。区域可能在VCS或邮件客户端中咬住您的情况很少见。
jjoelson 2011年

3
4.但是您已经将严重的复杂性导出到了课程的其余部分。值得吗?同样,区域可以像方法调用一样容易地移动。5.现在您正在谈论用更多的类污染您的名称空间,这些类只会在一个地方使用。为什么类和方法是您将接受的唯一封装单位?您始终可以在以后(或如果有必要)将此新类分开时创建该新类。
jjoelson 2011年

7
@jjoelson:1. 那又如何?2.确切地说,我的观点是:仅在(且仅)语法还不够的情况下,才可以使用非语法结构。3.“稀有?” -我想你有数字可以证明这一点。我可以肯定地告诉您,与TortoiseSVN捆绑在一起的diff(或怪怪或其他问题)工具没有折叠。4.这与我提出的观点有什么关系。5.这就是为什么大多数现代语言都具有可嵌套名称空间的原因。您要诉诸于ASCII艺术,而不是使用广泛的可用语言元素来构造代码。
back2dos

4
@jjoelson:1.这是您的意见,超出了此问题的范围。2.通过扩展我的论点,嵌套函数比区域更好,是的。3.因为有时我需要浏览一些修订以跟踪问题。无论哪种方式,源代码都是为人类而不是IDE编写的。4.首先,论点与我的观点无关,其次,这又只是您的观点,其次,这超出了这个问题的范围。5. C#除了嵌套功能外,还具有其他结构化代码的方法。您应该使用那些,或者使用允许嵌套功能的语言。
back2dos

13
如果您仍然想辩论,你们应该带着这个聊天。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。什么都不会改变他的想法,但更多的经验会给国际海事组织带来好处。
maple_shaft

15

问题不是方法中的区域,而是需要(甚至使用)将区域放入方法中的情况。

不得不调试大量200-300行方法之后,我可以告诉您,我对任何人都不希望这样做。如果您正在编写和维护自己的代码,那很好-做任何您想做的事情。但是一旦其他人查看了它,就应该立即弄清楚一种方法在做什么。

“首先,它得到了东西,然后将它们晃动,然后如果他们需要嗡嗡声,它就会这样做,否则它会检查它们是否是蓝色并反转哥白尼值。然后,如果第二件事大于第一件事,我们需要拿果汁盒并将其插入...”

不,那还不够。将其分解成您想要的所有区域,但是我还在想着这件事摇头。

如果您采用多种方法,则会获得很多好处:

  • 即使仅通过方法名称,也可以更清楚地了解在哪里发生了什么。而你是错的,一个方法不能完全记录-添加文档块(HIT ///),并输入描述,参数,返回类型,而且exceptionexampleremarks节点详细的方案。那就是它的去向,这就是它的作用!
  • 没有副作用或作用域问题。您可能会说您是在子作用域中声明了所有变量{},但是我是否必须检查每种方法以确保您没有“作弊”?这是dodgyObject我用这里只是因为在这个区域中使用,还是它来自别的地方?如果使用我自己的方法,则可以很容易地看到它是传递给我的,还是我自己创建的(或者更确切地说,是您创建的)。
  • 我的事件日志告诉我发生异常的方法。是的,我可以找到带有行号的有问题的代码,但是知道方法名称(特别是如果我正确命名了它们)可以很好地指示发生了什么,出了什么问题。“哦,该TurnThingsUpsideDown方法失败了-在变坏的时候可能会失败”比“哦,该DoEverything方法失败了-可能有50种不同的东西,我将不得不花一个小时才能找到它” 。

一切都在考虑可重用性或即兴性之前。正确分离的方法显然会促进重用,并且还使您可以轻松替换未执行的方法(太慢,错误太多,依赖关系发生更改等)。您是否尝试过重构这些庞大的方法?没有办法知道您所做的更改不会影响其他任何事情。

请正确地将您的逻辑封装到合理大小的方法中。我知道他们很容易失控,并且认为此时不值得重新设计是另一个问题。但是,您必须接受这样的事实,即正确封装,编写简洁且设计简单的方法没有任何危害,并且至少具有潜在的好处。


Visual Studio的XML注释解析以及#region标记属于Visual Studio功能的类别。我的全部意思是,如果项目中的每个人都在使用Visual Studio,则依靠这些功能并没有错。至于副作用和作用域问题,您仍然可以通过全局字段解决问题。无论哪种情况,都必须依靠程序员不要做有副作用的愚蠢的事情。
jjoelson 2011年

5
@jjoelson您仍然可以在没有Visual Studio的情况下阅读XML注释,但是Region标记在VS之外几乎完全没有用。参数的逻辑扩展是一种可以运行整个应用程序的巨大方法,只要您将其分成多个区域即可!
Kirk Broadhurst,

2
@jjoelson,请不要让我们开始了解全球领域有多糟糕。说些什么是没有用的,因为无论如何您都有“解决方法”将其弄糟,这只是愚蠢的。只是让您的方法简短,并限制变量的范围。
OliverS 2011年

@ OliverS,Kirk是提出共享状态的人,这是我的区域方法中的一个问题。基本上,我是在准确地说出的意思:程序员可以通过在区域之间共享太多变量来滥用状态,但这并不是对整个方案的起诉,就像通过全局变量搞砸方法之间共享状态的能力一样。对多方法方案的起诉。
jjoelson 2011年

7

如果您的方法足够复杂以至于可以分为三个不同的阶段,那么这些方法不仅应该是三个单独的方法……而且您的方法实际上应该是一个单独的类,专门用于该计算。

“但是那时我的班级将只有一种公共方法!”

您是对的,这没有错。单一责任原则的思想是,您的班级要做一件事

您的类可以依次调用这三种方法,并返回结果。一件事。

另外,现在您的方法可以测试了,因为它不再是私有方法了。

但是不要介意上堂课。实际上,您应该有四个:方法的每个部分一个,再加上一个将这三个部分中的每一个作为依赖项,并以正确的顺序调用它们,并返回结果。

这使您的课程更具可测试性。它还使您轻松更改这三个部分之一。只需编写一个继承自旧类的新类,然后覆盖更改的行为即可。或为三个部分的行为创建一个界面;然后替换它,只需编写一个实现该接口的新类,并在依赖项注入容器配置中将其替换为旧的类。

什么?您没有使用依赖注入吗?好吧,您可能还没有看到需求,因为您没有遵循单一责任原则。一旦开始,您将发现赋予每个类其依赖性的最简单方法是使用DI容器。

编辑

好,让我们搁置重构问题。 的目的#region是隐藏代码,这主要是指在任何正常情况下都无需编辑的代码。 生成的代码是最好的例子。它应该隐藏在区域中,因为通常不需要编辑它。如果确实需要,那么扩大区域的行为会给您一些“龙在这里”式的警告,以确保您知道自己在做什么。

因此,我想说#region应该不会在方法中使用。如果我打开一个方法,我想查看代码。使用#region给了我我不需要的另一种隐藏级别,这变得很烦人:我展开了该方法……仍然看不到任何代码。

如果您担心别人会看到该方法的结构(并且拒绝重构),请添加如下横幅广告:

//
// ---------- COMPUTE SOMETHING OR OTHER ----------
//

这些将帮助浏览代码的人看到您方法的各个部分,而不必处理扩展和折叠#region标签。


2
所有这些对于一种在一个地方被调用的方法而言?甚至大多数顽固的Lispers都不会抽象出更高阶的函数,除非他们确定了至少两个可以用来更轻松地定义函数的地方。
jjoelson 2011年

1
@jjoelson上新课很难吗?这是什么,一条线,一根开括号和一根闭括号。哦,你得按一下ctrl+n
柯克·布罗德赫斯特

2
这并不是说这是很难做出一个新的类,但有一些开销,一个额外的类。这是间接的一层,这使代码维护者更难找到她想要的东西。这并不是交易的大,但它也不会买任何东西在这段代码是不可能从其他地方调用情况。而且,正如您提到的,创建新类并将代码放到那里很容易,如果事实证明您确实需要在多个地方调用这一段代码。
jjoelson 2011年

2
@jjoelson,这是一个非常普遍的反应。是我的,当我第一次了解SRP和DI之类的东西时,这是我的同事对我们开始打破依赖关系的反应。如果您正在看一个个案,那似乎不值得。好处是合计的。一旦开始对所有类进行SRP ,您会发现更改代码片段变得容易得多,而所做的更改不会遍历一半的应用程序。
Kyralessa

@Kyralessa,这些方法仅在单个位置使用。更改这种方法不会影响整个应用程序的一半。如果将此类代码实现为使用该代码的方法中的区域,那么您将拥有完美的封装;否则,您将获得完美的封装。仅包含方法正在使用该代码,因此您可以随意更改所有内容,而不必担心它的效果(除了该方法之外)。
jjoelson 2011年

4

我认为您在参数中给出的示例与YAGNI(您将不再需要)无关,而与编写易于维护的适当质量代码有关。

在最早的编程课程中,我们了解到,如果一种方法的长度为700 LOC,则该方法可能太大,并且尝试调试可能会非常痛苦。

其次,如果我编写仅在方法D中使用的方法A,B和C,那么我可以更轻松地独立于D进行AB和C的单元测试,从而允许在所有4种方法中进行更可靠的单元测试。


单元测试当然是一个公平的点,但是我不确定在每种情况下它是否都可以证明污染了类范围。现实地讲,是否有人实际对他们编写的每个小私有方法进行单元测试?我想说人们倾向于对经常使用的私有方法进行单元测试,也就是说,私有方法无论如何都是代码重用的候选者。
jjoelson 2011年

@jjoelson如果该方法具有有意义的逻辑并且没有包装其他功能,则我总是针对该方法编写单元测试。单元测试方法与代码重用无关。您应该对测试方法进行单元验证,以验证对于给定的输入,您是否以一致且可重复的方式获得了预期的输出。如果您注意到类范围在被调用时受到污染,则可能是您的类违反了“单一职责原则”。
maple_shaft

2
我遇到的大多数程序员都不同意对每种私有方法进行单元测试的想法。只是不值得花时间在所有私有方法上实现和维护测试。
jjoelson 2011年

3

假设我们有一种方法,其计算可以分为三个相当不同的阶段。此外,这些阶段的每一个仅与该方法的计算有关,因此将它们提取到新方法中不会获得代码重用。那么,将每个阶段提取到自己的方法中有什么好处?据我所知,我们获得的是每个阶段都有一定的可读性和单独的变量范围(这将有助于防止特定阶段的修改意外破坏另一个阶段)。

这是两个好处。无论如何,对我而言,重要的是可调试性。如果您必须遍历该代​​码,则能够轻松遍历三个部分中的两个,而仅跟踪到您关心的那个部分。重构为较小的方法可以做到这一点;区域标签没有。


1
ctrl-f10-游标
Steven Jeuris,

2

我个人的规则是,如果一个方法比一个屏幕长,例如40行,那么它就太长了。如果一类的长度超过500行,则该类太大。我有时会违反这些规则,有时甚至会违反很多规则,但我通常会在一年内后悔。

在大多数情况下,即使是20到30行的方法也会变得有点长。因此,我要说的是,在一种方法中使用区域通常不是一个好主意,因为将20到30行的区域折叠成多个子区域几乎是没有意义的。

打破长期以来定义的功能至少有两个好处:

  1. 您必须在这些子功能的作用上起个名字,这可以阐明您目前的想法,并简化以后的维护人员的工作。

  2. 分解为较小的函数会强制您仅传递那些较小的函数所需的参数,因此它们所需和修改的数据范围非常清楚。假设您没有在一百个不同的叶子函数中访问和修改对象状态,我也不鼓励这样做。

以我的经验,这些规则产生的代码易于编写而不会犯常见错误,并且以后可以理解和修改这些代码而不会破坏微妙的行为。无论是要理解/修改代码的人是别人,还是我睡着并忘记一切之后的本人,都没有关系。

因此,我认为方法中的区域是“代码异味”,表明正在应用不可持续的实践。与往常一样,可能会有例外,但是它们很少发生,因此几乎不值得讨论。


2

我们再来一次 …… 关于程序员的许多其他主题都在同一讨论中结束。

您指出了一些与短函数有关的非常有趣的论点。这不是一个流行的说法,但是我对此表示支持。在经过多次讨论之后,我的印象是讨论通常归结为您主要是专注于正确的封装还是最大限度地进行测试驱动的开发。这是似乎又引发一场神圣战争的两个主要竞争者,恐怕我们处于力量不足的一面。

但是...

我认为解决方案绝对不是将“阶段”包装在区域中。代码折叠用于清除地毯下的代码。将代码保持原样,它可能会提醒您以后可能要重构它!将其与您的问题中的一个参数联系起来:如果您看不到任何代码,您将如何看到代码重用的机会?较长的代码段应该就是这样,这使您想重构它。

作为对区域的适当替代,我建议使用代码段,因为Alex Papadimoulis 在注释中为其命名。您实际上可能已经在使用类似的方法。它只是带有注释头的代码块,解释了整个块的作用。您具有函数的可读性,但是能够使用通常间隔的英语句子,并且不会丢失任何封装。


好吧,我实质上是使用区域来分隔代码段(区域可以具有即使折叠代码也可见的注释)。我喜欢为此使用区域,因为它使维护者可以选择查看代码(如果她想检查实现),或者选择折叠代码(如果她只想查看“全局”)。
jjoelson 2011年

1

尽管我同意通常重构代码比使用代码更好#region,但这并不总是可能的。

为了支持这一说法,让我举一个例子。

class Foo : IComparable
{
  private String name;
  private int foo;
  private Bar bar;

  public int CompareTo(Foo other)
  {
#region Ascending on name
     if (this.name < other.name) return -1;
     if (this.name > other.name) return 1;
#endregion
#region Usual order on bar
     int compBar = bar.compareTo(other.bar);
     if (compBar != 0) return compBar;
#endregion
#region Descending on foo
     if (this.foo > other.foo) return -1;
     if (this.foo < other.foo) return 1;
#endregion
     return 0; // equal
  }

重新调整区域以更改顺序或在将附加字段添加到类中时很容易扩展。无论如何都需要注释,以快速查看订购的工作方式。最重要的是:在这种情况下,我看不到重构如何帮助提高可读性。

但是,这些例外很少见。像许多其他答案一样,通常可以重构。


0

对于某些人或团队决定不使用#region的原因,我认为没有唯一的答案,但是如果我不得不列出一些原因,这将是我所看到的首要原因。

  • 区域的名称和顺序使代码的顺序和组织更加明确,代码的顺序和组织实施了一种特定的样式,这种样式可能会引起争用和自行车脱落。
  • 大多数概念并不完全适合少数地区。通常,您从访问修饰符publicprivate,然后按成员类型(例如,方法,属性,字段)开始,然后是跨越这些修饰符的构造函数,所有这些的静态变体,受保护成员,内部成员,受保护内部成员。成员,隐式接口成员,事件等。最后,您将拥有设计最完善的类,其中由于粒度分类而在每个区域中只有一个或一个方法。
  • 如果您能够保持务实,并且仅将事物归类为三种或四种恒定类型的区域,那可能会起作用,但是它真正增加了什么价值?如果您可以很好地设计类,则实际上不需要筛选代码页面。
  • 正如上面所暗示的,区域可以隐藏难看的代码,这可能使它看起来不那么麻烦。保持眼痛可能有助于解决它。
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.