块中的附加行与清理代码中的附加参数


33

语境

在第35页的“ 清理代码”中,它说

这意味着if语句,else语句,while语句等中的块应为一行。该行可能是函数调用。这不仅使封闭函数较小,而且还增加了文档价值,因为在块中调用的函数可以具有很好的描述性名称。

我完全同意,这很有意义。

稍后,在第40页上,它介绍了有关函数参数的信息

函数的理想参数个数为零(尼拉度)。接下来是一个(单声道),紧接着是两个(双声道)。在可能的情况下,应避免使用三个参数(三重性)。超过三个(多义词)需要非常特殊的理由-因此无论如何都不应使用。争论很难。他们具有很大的概念力。

我完全同意,这很有意义。

问题

但是,通常我会发现自己从另一个列表创建了一个列表,而我将不得不忍受两个弊端之一。

在代码块中使用了两行,一行用于创建事物,一行用于将其添加到结果中:

    public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
    {
        List<Flurp> flurps = new List<Flurp>();
        foreach (BadaBoom badaBoom in badaBooms)
        {
            Flurp flurp = CreateFlurp(badaBoom);
            flurps.Add(flurp);
        }
        return flurps;
    }

或者,我在要向其中添加事物的列表的函数中添加了一个参数,从而使其“变差了一个参数”。

    public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
    {
        List<Flurp> flurps = new List<Flurp>();
        foreach (BadaBoom badaBoom in badaBooms)
        {
            CreateFlurpInList(badaBoom, flurps);
        }
        return flurps;
    }

我没有看到哪些(缺点)优势,这些优势通常使它们之一更受欢迎?还是在某些情况下有这样的优势?在这种情况下,做出决定时我应该寻找什么?


58
这有什么错flurps.Add(CreateFlurp(badaBoom));
cmaster

47
不,这只是一个声明。它只是一个琐碎的嵌套表达式(单个嵌套级别)。而且,如果简单f(g(x))不符合您的风格指南,那么我就无法修复您的风格指南。我的意思是,您也不会分成sqrt(x*x + y*y)四行,对吗?这就是两个(!)内部嵌套级别(gasp!)上的三个(!)嵌套子表达式。您的目标应该是可读性,而不是单个运算符。如果您想要以后的版本,那么,我为您提供了一种完美的语言:汇编程序。
cmaster

6
@cmaster即使x86程序集也严格没有单操作符语句。内存寻址模式包括许多复杂的操作,并且可以用于算术运算-实际上,您可以仅使用x86 mov指令并jmp toStart在最后使用单个指令来制作图灵完整的计算机。有人实际上制作了一个可以做到这一点的编译器:D
Luaan

5
@Luaan更不用说rlwimi在PPC上的臭名昭著的指示了。(这代表“旋转左字立即掩码插入”。)此命令使用不少于五个操作数(两个寄存器和三个立即值),并且执行以下操作:将一个寄存器内容立即移位一次,掩码为使用一个由其他两个立即数操作数控制的1位单次运行创建的,将与另一个寄存器操作数中该掩码中的1位相对应的位替换为旋转寄存器的相应位。非常酷的指示:-)
cmaster18年

7
@ R.Schmitz“我正在做通用编程” –实际上,不是,您不是在为特定目的进行编程(我不知道是什么目的,但我假设做;-)。实际上有数千种编程目的,并且针对它们的最佳编码样式也有所不同-因此适合您的内容可能不适合其他人,反之亦然:这里的建议通常是绝对的(“ 总是做X; Y 不好 “等),而忽略了在某些领域坚持这样做是完全不切实际的。这就是为什么像“清洁代码”这样的书中的建议应该总是用少量的(实用的)盐
服用的方法

Answers:


104

这些准则只是指南针,而不是地图。他们为您指明了明智的方向。但是他们不能真正绝对地告诉您哪种解决方案是“最佳”的。在某个时候,由于您已经到达目的地,因此您需要停止走入指南针指向的方向。

干净的代码鼓励您将代码分成非常小的明显的块。这通常是一个好的方向。但是,如果达到极限(正如所引用建议的字面解释所暗示的那样),那么您将把代码细分为无用的小片段。实际上什么也没做,只是委托。这本质上是另一种代码混淆。

在“越小越好”与“过小无用”之间取得平衡是您的工作。问问自己哪种解决方案更简单。对我来说,这显然是第一个解决方案,因为它显然可以汇编一个列表。这是一个很好理解的成语。无需看另一个功能就可以理解该代码。

如果有可能做得更好,那就是要注意“将所有元素从一个列表转换到另一个列表”是一种常见的模式,通常可以通过使用功能map()操作来抽象出来。在C#中,我认为它叫做Select。像这样:

public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
{
    return badaBooms.Select(BadaBoom => CreateFlurp(badaBoom)).ToList();
}

7
代码仍然是错误的,并且毫无意义地重新发明了轮子。为什么CreateFlurps(someList)在BCL已经提供时调用someList.ConvertAll(CreateFlurp)
Ben Voigt

44
@BenVoigt这是一个设计级的问题。我不关心确切的语法,特别是因为白板没有编译器(我上次在09年编写C#)。我的意思不是“我已经展示了最好的代码”,而是“顺便说一句,这是一种已经解决的通用模式”。LINQ是一个办法做到这一点,你提到的ConvertAll 另一个。感谢您提出替代方案。
阿蒙

1
您的回答是明智的,但是LINQ将逻辑抽象化,并将语句减少到一行的事实似乎与您的建议相矛盾。附带说明,这BadaBoom => CreateFlurp(badaBoom)是多余的;您可以CreateFlurp直接通过(Select(CreateFlurp))作为函数。(据我所知,情况一直如此。)
jpmc26

2
请注意,这完全消除了对该方法的需要。这个名字CreateFlurps实际上比仅仅看到它更容易误导和理解badaBooms.Select(CreateFlurp)。后者是完全声明性的-分解没有问题,因此根本不需要方法。
卡尔·莱斯

1
@ R.Schmitz这不是很难理解,但它不太容易,而不是理解badaBooms.Select(CreateFlurp)。您创建一个方法,以便其名称(高级)代表其实现(低级)。在这种情况下,它们处于同一级别,因此要确切了解正在发生的事情,我只需要查看该方法(而不是内联查看)。 CreateFlurps(badaBooms)可以带来惊喜,但badaBooms.Select(CreateFlurp)不能。这也是一种误导,因为它错误地要求使用List而不是IEnumerable
卡尔·莱斯

61

函数的理想参数个数为零(尼拉度)

没有!一个函数的理想参数个数为1。如果为零,则表示您保证该功能必须访问外部信息才能执行操作。“叔叔”鲍勃误解了这一说法。

关于您的代码,您的第一个示例在块中只有两行,因为您要在第一行上创建一个局部变量。删除该分配,您将遵守以下干净代码准则:

public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
{
    List<Flurp> flurps = new List<Flurp>();
    foreach (BadaBoom badaBoom in badaBooms)
    {
        flurps.Add(CreateFlurp(badaBoom));
    }
    return flurps;
}

但这是冗长的(C#)代码。只需执行以下操作即可:

IEnumerable<Flurp> CreateFlurps(IEnumerable<BadaBoom> badaBooms) =>
    from badaBoom in babaBooms select CreateFlurp(badaBoom);

14
参数为零的函数意味着隐含对象封装了所需的数据,而不是事物以对象外部的全局状态存在。
Ryathal

19
@Ryathal,有两点:(1)如果您正在谈论方法,那么对于大多数(所有?)OO语言,该对象被推断为(或明确声明为Python)第一个参数。在Java,C#等中,所有方法都是具有至少一个参数的函数。编译器只是向您隐藏了该细节。(2)我从未提及“全球”。例如,对象状态在方法外部。
大卫·阿尔诺

17
我很确定,当鲍勃叔叔写“零”时,他的意思是“零(不算这个)”。
布朗

26
@DocBrown,可能是因为他非常喜欢在对象中混合状态和功能,因此他通过“功能”可能专门指代方法。而且我仍然不同意他。最好只提供所需的方法,而不是让它遍历对象以获得所需的内容(即,操作中经典的“告诉,不要问”)要好得多。
大卫·阿诺

8
@AlessandroTeruzzi,理想是一个参数。零太少。例如,这就是为什么出于功能性目的,功能语言采用一个作为参数数量的原因(实际上,在某些功能语言中,所有功能都只有一个参数:不多;不小于)。用零参数进行咖喱是没有意义的。说“理想越少越好,因此,最佳选择是零”,这是一种荒谬还原
David Arno

19

“清洁代码”建议是完全错误的。

在循环中使用两行或更多行。当它们是一些需要描述的随机数学时,将相同的两行隐藏在一个函数中是有意义的,但是当这些行已经具有描述性时,它什么都不做。“创建”和“添加”

您提到的第二种方法实际上没有任何意义,因为您不必为了避免这两行而添加第二个参数。

public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
    {
        List<Flurp> flurps = new List<Flurp>();
        foreach (BadaBoom badaBoom in badaBooms)
        {
            flurps.Add(badaBoom .CreateFlurp());
            //or
            badaBoom.AddToListAsFlurp(flurps);
            //or
            flurps.Add(new Flurp(badaBoom));
            //or
            //make flurps a member of the class
            //use linq.Select()
            //etc
        }
        return flurps;
    }

要么

foreach(var flurp in ConvertToFlurps(badaBooms))...

正如其他人所指出的那样,关于最佳功能是不带参数的建议,最好是偏向于OOP,而最坏的情况就是普通的不良建议。


也许您想编辑此答案以使其更清晰?我的问题是,根据“清洁法规”,一件事是否胜过另一件事。您说整个事情都是错误的,然后继续描述我提供的选项之一。目前,此答案似乎是您遵循的是“反清洁代码”议程,而不是实际尝试回答问题。
R. Schmitz

抱歉,我将您的问题解释为暗示第一种是“正常”方式,但是您被推入第二种。我一般不是反清洁代码,但是这个引用显然是错误的
Ewan

19
@ R.Schmitz我本人已经阅读了“清洁代码”,并且遵循了该书的大部分内容。但是,关于完美函数的大小几乎只有一条语句,这是完全错误的。唯一的效果是,它将意大利面条代码转换为米饭代码。读者会迷失在众多琐碎的功能中,这些功能只有一起看时才会产生有意义的意义。人类的工作内存容量有限,您可以使用语句或函数来重载代码。如果您想使其可读性,则必须在两者之间取得平衡。避免极端!
cmaster18年

@cmaster答案仅是我写评论时的前两段。到目前为止,这是一种更好的答案。
R. Schmitz

7
坦白说,我希望我的回答简短些。这些答案大多数都涉及太多外交话题。所引用的建议是完全错误的,无需'测“它的真正含义”或四处寻找尝试的好解释。
伊万

15

其次肯定是更糟糕的,因为CreateFlurpInList接受列表并修改该列表,使函数不纯净且难以推理。方法名称中没有任何内容表明该方法仅添加到列表中。

我提供了第三种最佳选择:

public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
{
    return badaBooms.Select(CreateFlurp).ToList();
}

而且,如果仅在一个地方使用它,则可以立即内联该方法,因为单行代码本身就很清楚,因此不需要用方法封装它即可赋予其含义。


我不会抱怨这种方法“不是很纯正,也很难推理”(尽管是真的),但是对于处理特殊情况而言,它是完全不必要的方法。如果我想创建一个独立的Flurp,将Flurp添加到数组,添加到字典,然后在字典中查找并删除匹配的Flurp等,该怎么办?使用相同的参数,Flurp代码也将需要所有这些方法。
gnasher729 '18

10

一个参数版本更好,但不是主要因为参数数量多。

更好的最重要原因是它具有较低的偶联,这使其更有用,更易于推论,更易于测试,并且不太可能变成复制粘贴的克隆。

如果你向我提供CreateFlurp(BadaBoom),我可以使用任何类型的收集容器:简单Flurp[]List<Flurp>LinkedList<Flurp>Dictionary<Key, Flurp>,等。但是,有了CreateFlurpInList(BadaBoom, List<Flurp>),我明天再问您,CreateFlurpInBindingList(BadaBoom, BindingList<Flurp>)以便我的viewmodel可以收到列表已更改的通知。!

另外一个好处是,更简单的签名更可能与现有的API相适应。您说您经常遇到问题

通常我发现自己是从另一个列表创建一个列表

只是使用可用工具的问题。最短,最有效和最佳的版本是:

var Flurps = badaBooms.ConvertAll(CreateFlurp);

这不仅让您编写和测试的代码更少,而且速度也更快,因为List<T>.ConvertAll()它足够聪明,可以知道结果将具有与输入相同的项目数,并将结果列表预先分配给正确的大小。虽然您的代码(两个版本)都需要增加列表。


不要使用List.ConvertAll。在C#中,将可枚举的对象映射到不同对象的惯用方式称为SelectConvertAll甚至在这里可用的唯一原因是因为OP错误地List在方法中要求一个-它应该是一个IEnumerable
卡尔·莱斯

6

牢记总体目标:使代码易于阅读和维护。

通常,可以将多行分组为一个有意义的功能。在这种情况下,请这样做。有时,您需要重新考虑您的一般方法。

例如,在您的情况下,将整个实现替换为var

flups = badaBooms.Select(bb => new Flurp(bb));

可能是有可能的。或者你可能会做类似的事情

flups.Add(new Flurp(badaBoom))

有时,最干净,最易读的解决方案根本无法与任何一行兼容。因此,您将有两行。不要为了满足一些任意规则而使代码更难理解。

(我认为)第二个例子比第一个例子难理解。不仅仅是拥有第二个参数,还在于该参数已由函数修改。查找“清洁代码”对此有何评论。(现在没有手头的书,但是我很确定这基本上是“如果可以避免就不要这样做”)。

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.