调用函数时不知道函数的参数名称


13

这是我想听听您的想法的编程/语言问题。

我们已经制定了约定(大多数程序员应该遵循),这些约定不是语言语法的一部分,但是可以使代码更具可读性。当然,这些总是一个辩论的问题,但是至少有一些核心概念是大多数程序员认为可以接受的。适当地命名变量,一般来说命名,使行的长度不会过长,避免使用长函数,封装等那些东西。

但是,有一个问题我尚未找到任何人对此发表评论,而这可能是其中最大的问题。这是调用函数时参数匿名的问题。

函数源于数学,其中f(x)具有明确的含义,因为函数具有比编程中通常更为严格的定义。数学中的纯函数比编程中的函数执行的功能要少得多,并且它们是一种更精致的工具,它们通常只接受一个参数(通常是一个数字),并且总是返回一个值(通常是一个数字)。如果一个函数接受多个参数,则它们几乎总是只是该函数域的额外维度。换句话说,一个论据并不比其他论据更重要。当然,它们是明确排序的,但是除此之外,它们没有语义排序。

但是,在编程中,我们拥有更多的自由定义函数,在这种情况下,我认为这不是一件好事。常见的情况是,您定义了这样的函数

func DrawRectangleClipped (rectToDraw, fillColor, clippingRect) {}

查看定义,如果函数编写正确,则完全清楚是什么。在调用该函数时,您甚至可能在您的IDE /编辑器中出现了一些智能/代码完成魔术,这些魔术将告诉您下一个参数应该是什么。可是等等。如果我在实际编写呼叫时需要此功能,这里是否不缺少某些内容?读代码的人没有IDE的好处,除非他们跳转到定义,否则他们不知道作为参数传递的两个矩形中的哪个用于什么。

问题不仅仅如此。如果我们的参数来自某个局部变量,那么在某些情况下,我们甚至不知道第二个参数是什么,因为我们只能看到变量名称。以这行代码为例

DrawRectangleClipped(deserializedArray[0], deserializedArray[1], deserializedArray[2])

使用不同的语言可以在不同程度上缓解这种情况,但是即使使用严格类型的语言,即使您明智地命名变量,在将变量传递给函数时,您甚至都不会提到变量的类型。

与编程通常一样,此问题有很多潜在的解决方案。许多已经以流行语言实现。例如,C#中的命名参数。但是,我所知道的全部都有明显的缺点。命名每个函数调用中的每个参数都不可能导致可读代码。几乎感觉像是我们正在超越纯文本编程为我们提供的可能性。我们几乎在每个领域都从仅文本迁移而来,但是我们仍然编写相同的代码。需要在代码中显示更多信息吗?添加更多文本。无论如何,这有点切线,所以我在这里停止。

我对第二个代码段的回答是,您可能首先将数组解压缩为一些命名变量,然后使用它们,但是变量的名称可能意味着很多事情,并且其调用方式并不一定会告诉您应该使用的方式在调用函数的上下文中进行解释。在本地范围内,您可能有两个名为leftRectangle和rightRectangle的矩形,因为这是它们在语义上表示的,但是在扩展给函数时并不需要扩展到它们表示的内容。

实际上,如果变量是在被调用函数的上下文中命名的,则所引入的信息要少于该函数调用可能带来的信息,并且在某种程度上会导致代码变差。如果您有一个过程产生一个矩形,该过程存储在rectForClipping中,然后另一个过程提供了rectForDrawing,则对DrawRectangleClipped的实际调用只是一种仪式。这行没有什么新意,只是在那里,因此即使您已经用命名解释了它,计算机也知道您到底想要什么。这不是一件好事。

我真的很想听听对此的新见解。我确定我不是第一个将其视为问题的人,那么如何解决呢?


2
我对确切的问题感到困惑...这里似乎有一些想法,不确定哪一个是您的重点。
FrustratedWithFormsDesigner 2014年

1
函数的文档应告诉您参数的作用。您可能会反对阅读代码的人可能没有文档,但实际上却不知道代码的用途以及他们从阅读代码中提取的任何含义是有根据的猜测。在读者需要知道代码正确的任何情况下,他都需要文档。
2014年

3
@Darwin在函数式编程中,所有函数仍然只有1个参数。如果您需要传递“多个参数”,则该参数通常是一个元组(如果您希望对它们进行排序)或一个记录(如果您不希望对它们进行排序)。另外,随时形成函数的专用版本很简单,因此您可以减少所需的参数数量。由于几乎每种功能语言都提供了元组和记录的语法,因此将值捆绑在一起
就很轻松

1
@Bergi人们倾向于在纯FP中推广更多的东西,因此我认为这些函数本身通常更小且数量更多。我可能离我远了。我在与Haskell和帮派合作进行实际项目方面经验不足。
达尔文

4
我认为答案是“不要将变量命名为'deserializedArray'”吗?
whatsisname 2014年

Answers:


10

我同意经常使用函数的方式可能会成为编写代码(尤其是阅读代码)时令人困惑的部分。

这个问题的答案部分取决于语言。如您所述,C#已命名参数。Objective-C解决此问题的方法涉及更多描述性的方法名称。例如,stringByReplacingOccurrencesOfString:withString:是一种具有清晰参数的方法。

在Groovy中,某些函数采用映射,从而允许使用如下语法:

restClient.post(path: 'path/to/somewhere',
            body: requestBody,
            requestContentType: 'application/json')

通常,可以通过限制传递给函数的参数数量来解决此问题。我认为2-3是一个很好的极限。如果某个函数似乎需要更多参数,则会使我重新考虑设计。但是,这通常很难回答。有时您试图在函数中做太多事情。有时考虑使用一个类来存储参数是有意义的。另外,在实践中,我经常发现带有大量参数的函数通常将其中许多作为可选函数。

即使使用像Objective-C这样的语言,限制参数的数量也是有意义的。原因之一是许多参数是可选的。有关示例,请参见rangeOfString:及其在NSString中的变体。

我在Java中经常使用的一种模式是使用流利的样式类作为参数。例如:

something.draw(new Box().withHeight(5).withWidth(20))

它使用一个类作为参数,并使用流畅的类,从而使代码易于阅读。

上面的Java代码段也可以在参数排序不太明显的地方提供帮助。我们通常假设坐标X早于Y。根据惯例,我通常认为高度早于宽度,但这仍然不是很清楚(something.draw(5, 20))。

我也看到过一些类似的函数,drawWithHeightAndWidth(5, 20)但是即使这些函数也不能接受太多参数,否则您将开始失去可读性。


2
如果继续使用Java示例,该命令确实非常棘手。例如,比较awt:Dimension(int width, int height)和中的以下构造函数GridLayout(int rows, int cols)(行数是高度,意思GridLayout是高度先Dimension于宽度)。
皮埃尔·阿劳德

1
这种不一致也已经非常批评与PHP(eev.ee/blog/2012/04/09/php-a-fractal-of-bad-design),例如:array_filter($input, $callback)array_map($callback, $input)strpos($haystack, $needle)array_search($needle, $haystack)
皮埃尔Arlaud的

12

大多数情况下,可以通过对函数,参数和参数的良好命名来解决。您已经对此进行了探索,但是发现它有缺陷。通过在调用上下文和被调用上下文中使函数保持较小且具有少量参数,可以缓解大多数此类缺陷。您的特定示例有问题,因为您正在调用的函数试图同时执行几项操作:指定基本矩形,指定剪切区域,绘制并用特定颜色填充。

这有点像试图只用形容词写一个句子。在其中放置更多的动词(函数调用),为句子创建一个主语(宾语),更容易阅读:

rect.clip(clipRect).fill(color)

即使clipRect并且color名称很糟糕(并且不应),您仍然可以从上下文中识别它们的类型。

反序列化的示例是有问题的,因为调用上下文试图立即做太多事情:反序列化并绘制某些内容。您需要分配有意义的名称,并明确区分这两个职责。至少是这样的:

(rect, clipRect, color) = deserializeClippedRect()
rect.clip(clipRect).fill(color)

过于简洁会导致很多可读性问题,从而跳过了人类识别上下文和语义所需要的中间阶段。


1
我喜欢将多个函数调用串起来以澄清含义的想法,但这不只是围绕这个问题跳舞吗?基本上是“我想写一个句子,但是我使用的语言不允许我使用,所以我只能使用最接近的对等语言”
达尔文

@Darwin恕我直言,通过使编程语言更像自然语言可以改善这一点。自然语言非常模棱两可,我们只能在上下文中理解它们,实际上永远无法确定。字符串函数调用会更好,因为每个术语(理想情况下)都有文档和可用的资料,并且括号和点使结构清晰易懂。
maaartinus

3

实际上,可以通过更好的设计来解决。编写良好的函数要接受两个以上的输入是非常罕见的,而且当输入发生时,很多输入不能被聚合到某个内聚的捆绑中是罕见的。这使分解函数或聚集参数变得非常容易,因此您不会使函数做太多事情。一个输入有两个输入,它很容易命名,并且更清楚地知道哪个输入。

我的玩具语言具有用词组来解决此问题的概念,而其他以自然语言为重点的编程语言也有其他方法可以解决此问题,但是它们都具有其他缺点。另外,即使短语也只是使函数具有更好的名称的一种不错的语法。当需要大量输入时,总是很难建立一个好的函数名。


短语确实看起来像是向前迈出了一步。我知道有些语言具有类似的功能,但从广泛的角度来看它是FAR。更不用说,由于所有的宏恨都来自C(++)的纯粹主义者,他们从未正确使用过宏,所以我们可能永远不会在流行语言中拥有像这样的功能。
达尔文

欢迎来到特定领域语言的一般主题,我真的希望更多的人能理解...(+1)
Izkata 2014年

2

例如,在Javascript(或ECMAScript)中,许多程序员已经习惯于

在单个匿名对象中将参数作为一组命名对象属性传递。

作为编程实践,它从程序员到他们的库,再到其他程序员,他们逐渐喜欢它并使用它并编写更多的库等。

而不是打电话

function drawRectangleClipped (rectToDraw, fillColor, clippingRect)

像这样:

drawRectangleClipped(deserializedArray[0], deserializedArray[1], deserializedArray[2])

,这是一种有效且正确的样式,您称

function drawRectangleClipped (params)

像这样:

drawRectangleClipped({
    rectToDraw: deserializedArray[0], 
    fillColor: deserializedArray[1], 
    clippingRect: deserializedArray[2]
})

,对于您的问题,这是有效的,正确的很好的。

当然,这必须有合适的条件-在Javascript中,它比在C语言中更可行。在javascript中,这甚至催生了现在被广泛使用的结构符号,并逐渐成为XML的轻量级符号。它称为JSON(您可能已经听说过)。


我对该语言了解不多,无法验证语法,但是总体而言,我喜欢这篇文章。看起来很优雅。+1
IT亚历克斯

通常,它与常规参数结合在一起,即,有1-3个参数,后跟params(通常包含可选参数,而其本身通常是可选的),例如此函数。这使得带有许多参数的函数非常容易掌握(在我的示例中,有2个强制参数和6个选项参数)。
maaartinus

0

然后,您应该使用Objective-C,这是一个函数定义:

- (id)performSelector:(SEL)aSelector withObject:(id)anObject withObject:(id)anotherObject

在这里使用:

[someObject performSelector:someSelector withObject:someObject2 withObject:someObject3];

我认为ruby具有类似的构造,您可以使用键值列表以其他语言模拟它们。

对于Java中的复杂函数,我喜欢在函数用语中定义伪变量。对于您的左右示例:

Rectangle referenceRectangle = leftRectangle;
Rectangle targetRectangle = rightRectangle;
doSomeWeirdStuffWithRectangles(referenceRectangle, targetRectangle);

看起来更多的编码,但是例如您可以使用leftRectangle,然后在以后用“提取局部变量”重构代码,如果您认为将来对代码的维护者可能无法理解,则可能是您。


关于该Java示例,我在问题中写道为什么我认为这不是一个好的解决方案。您对此有何看法?
达尔文

0

我的方法是创建临时局部变量-而不只是调用它们LeftRectangeRightRectangle。相反,我使用更长的名称来传达更多的含义。我经常尝试尽可能区分名称,例如something_rectangle,如果它们的角色不是很对称,则不要同时使用它们。

范例(C ++):

auto& connector_source = deserializedArray[0]; 
auto& connector_target = deserializedArray[1]; 
auto& bounding_box = deserializedArray[2]; 
DoWeirdThing(connector_source, connector_target, bounding_box)

而且我什至可以编写一个单行包装函数或模板:

template <typename T1, typename T2, typename T3>
draw_bounded_connector(
    T1& connector_source, T2& connector_target,const T3& bounding_box) 
{
    DoWeirdThing(connector_source, connector_target, bounding_box)
}

(如果您不了解C ++,请忽略与号)。

如果该函数做了一些奇怪的事情而没有很好的描述-那么可能需要重构它!

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.