为什么许多语言不支持命名参数?[关闭]


14

我只是在想,如果在调用函数时可以编写以下代码,那么读取代码会容易得多:

doFunction(param1=something, param2=somethingElse);

我想不出任何缺点,它会使代码更具可读性。我知道您可以将数组作为唯一的参数传递,并将数组键作为参数名称,但是这样的可读性仍然不高。

我缺少这种缺点吗?如果不是,为什么许多语言不允许这样做?


1
您能否提供一种应该(可以)具有命名参数但没有命名参数的语言?
布赖恩·陈

1
我认为在许多情况下它太冗长了。据我所知,一种语言是否使用它往往取决于它会对该语言产生积极或消极的影响。没有它,大多数C风格的语言都会表现出色。在他们的情况下,无论如何通常都很明显,而且它的缺失确实确实有助于减少混乱。
Panzercrisis

2
@SteveFallows:“命名参数成语”对于在构建器类上定义流利的API(由Martin Fowler命名)而言似乎是一个奇怪的名称。您可以在Joshua Bloch的Effective Java的第一批文章中找到相同模式的示例。
13年

3
这不一定是基于意见的问题
Catskul 2014年

2
同意 “为什么许多语言不支持命名参数?” 与其说是观点,不如说是语言历史的问题。如果有人问“是否更好地命名参数?”,这将是一种意见。
安迪·弗莱明

Answers:


14

指定参数名称并不总是使代码更具可读性:例如,您希望阅读min(first=0, second=1)还是min(0, 1)

如果您接受上一段的参数,那么很明显,指定参数名称不是必须的。为什么所有语言都不将指定参数名称作为有效选项?

我至少可以想到两个原因:

  • 通常,针对已经发现的需求(此处为传递参数)引入第二种语法,其好处必须与所引入的语言复杂性的固有成本(在语言实现和程序员的语言思维模型中)相平衡。 ;
  • 它使更改参数名称成为API的重大更改,这可能不符合开发人员的期望,即更改参数名称是微不足道的,非破坏性的更改。

请注意,我不了解任何实现命名参数而不实现可选参数(需要命名参数)的语言-因此您应该警惕高估它们的可读性,这可以通过Fluent Interfaces的定义更一致地获得。


6
我经常用几种不同的语言编写代码,几乎总是必须用任何给定的语言来查找简单子字符串的参数。它是substring(start,length)还是substring(start,end),并且end是否包含在字符串中?
James Anderson

1
@JamesAnderson-命名参数子字符串将如何解决您的问题?您仍然必须查找参数才能知道第二个参数的名称(除非两个函数都存在,并且语言的多态性功能允许使用不同名称的相同类型参数)。
mouviciel

6
@mouviciel:命名参数是不是在所有的多大用处的作家,但奇妙的读者。我们都知道变量名对于编写可读代码的重要性,而命名参数使接口的创建者可以提供良好的参数名和良好的函数名,从而使使用该接口的人们更容易编写可读代码。
Phoshi

@Phoshi-你是正确的。当我写之前的评论时,我只是忘记了阅读代码的重要性。
mouviciel 2013年

14

命名参数使代码更易于阅读,更难编写

当我阅读一段代码时,命名参数可以引入上下文,使代码更易于理解。例如,考虑以下构造函数:Color(1, 102, 205, 170)。这到底是什么意思?确实,Color(alpha: 1, red: 102, green: 205, blue: 170)这将更容易阅读。但是可惜,编译器说“不” –它想要Color(a: 1, r: 102, g: 205, b: 170)。使用命名参数编写代码时,您会花费不必要的时间查找确切名称-忘记某些参数的确切名称比忘记它们的顺序要容易得多。

使用DateTime具有几乎同一个接口的点和持续时间的两个同级类的API时,这曾经使我感到困惑。在DateTime->new(...)接受一个second => 30论点的同时,DateTime::Duration->new(...)通缉犯seconds => 30和其他单位也是如此。是的,这绝对有道理,但是这向我展示了命名参数≠易于使用。

坏名字甚至不容易阅读

R语言可能是命名参数可能不好的另一个示例。这段代码创建了一个数据图:

plot(plotdata$n, plotdata$mu, type="p", pch=17,  lty=1, bty="n", ann=FALSE, axes=FALSE)

您将看到xy数据行的两个位置参数,然后是命名参数的列表。有更多具有默认值的选项,并且仅列出那些我想要更改或明确指定其默认值的选项。一旦我们忽略了这段代码使用了幻数,并且可以从使用枚举中受益(如果R有的话!),问题就在于这些参数名中的许多都是难以理解的。

  • pch实际上是绘图字符,即将为每个数据点绘制的字形。17是一个空圆圈或类似的东西。
  • lty是线型。这1是一条实线。
  • bty是盒子类型。将其设置为"n"避免在绘图周围绘制一个框。
  • ann 控制轴注释的外观。

对于不知道每个缩写含义的人来说,这些选项相当令人困惑。这也揭示了R为什么使用这些标签的原因:不是作为自文档代码,而是(作为一种动态类型的语言)作为将值映射到其正确变量的键。

参数和签名的属性

函数签名可能具有以下属性:

  • 参数可以是有序的或无序的,
  • 命名或未命名,
  • 必需或可选。
  • 签名也可以按大小或类型进行重载,
  • 并且可以使用varargs来指定大小。

不同的语言落在该系统的不同坐标上。在C语言中,参数是有序的,未命名的,始终是必需的,并且可以是varargs。在Java中,情况类似,除了签名可以重载。在目标C中,签名是有序的,命名的,必需的,并且不能重载,因为它只是C周围的语法糖。

具有varargs的动态类型语言(命令行界面,Perl等)可以模拟可选的命名参数。具有签名大小重载的语言具有类似位置的可选参数。

如何不实现命名参数

在考虑命名参数时,我们通常假设命名,可选,无序参数。实施这些是困难的。

可选参数可以具有默认值。这些必须由被调用函数指定,并且不应编译为调用代码。否则,如果不重新编译所有相关代码,则无法更新默认值。

现在,一个重要的问题是参数如何实际传递给函数。使用有序参数,可以将args传递到寄存器中,或以其固有顺序传递到堆栈中。当我们暂时排除寄存器时,问题在于如何将无序的可选参数放入堆栈。

为此,我们需要对可选参数进行一些排序。如果声明代码更改了怎么办?由于顺序无关紧要,因此函数声明中的重新排序不应更改值在堆栈上的位置。我们还应该考虑是否可以添加新的可选参数。从用户的角度来看,这似乎是这样,因为以前未使用该参数的代码仍应与新参数一起使用。因此,这排除了诸如在声明中使用顺序或使用字母顺序之类的顺序。

还要根据子类型和Liskov替换原理考虑这一点-在编译的输出中,相同的指令应该能够对可能具有新命名参数的子类型和超类型调用该方法。

可能的实现

如果我们不能确定的顺序,那么我们需要一些无序的数据结构。

  • 最简单的实现是简单地将参数名称和值一起传递。这是在Perl或命令行工具中模拟命名参数的方式。这解决了上面提到的所有扩展问题,但可能会浪费大量空间,而不是性能关键代码中的选择。而且,处理这些命名的参数现在比简单地将值弹出堆栈要复杂得多。

    实际上,可以通过使用字符串池来减少空间需求,这可以将以后的字符串比较减少为指针比较(除非不能保证实际存储了静态字符串,否则必须将两个字符串进行比较)。详情)。

  • 相反,我们还可以传递一个巧妙的数据结构,该结构用作命名实参的字典。在呼叫方这是便宜的,因为密钥集在呼叫位置是静态已知的。这将允许创建完美的哈希函数或预先计算特里。被调用方仍将必须测试所有可能的参数名称的存在,这有点昂贵。Python使用了类似的东西。

所以在大多数情况下太贵了

如果具有命名参数的函数要适当扩展,则不能采用确定的顺序。因此,只有两种解决方案:

  • 将命名参数的顺序作为签名的一部分,并禁止以后的更改。这对于自记录代码很有用,但对可选参数没有帮助。
  • 将键值数据结构传递给被调用方,然后该被调用方必须提取有用的信息。这在比较中非常昂贵,并且通常仅在脚本语言中看到而不强调性能。

其他陷阱

函数声明中的变量名称通常具有一些内部含义,并且不是接口的一部分,即使许多文档工具仍会显示它们。在许多情况下,您希望内部变量和相应的命名参数使用不同的名称。如果未在调用上下文中使用变量名称,则不允许选择命名参数的外部可见名称的语言不会获得太多好处。

模拟命名参数的一个问题是调用方缺少静态检查。当传递参数字典(看着您,Python)时,这尤其容易忘记。这很重要,因为传递字典是常见的解决方法,例如在JavaScript:中foo({bar: "baz", qux: 42})。在此,不能静态检查值的类型或某些名称的存在与否。

模拟命名参数(使用静态类型的语言)

在存在静态类型检查器的情况下,简单地将字符串用作键,将任何对象用作值并不是很有用。但是,可以使用结构或对象文字来模拟命名参数:

// Java

static abstract class Arguments {
  public String bar = "default";
  public int    qux = 0;
}

void foo(Arguments args) {
  ...
}

/* using an initializer block */
foo(new Arguments(){{ bar = "baz"; qux = 42; }});

3
it is easier to forget the exact names of some parameters than it is to forget their order” ...这是一个有趣的断言。
Catskul 2014年

1
第一个反例不是命名参数的问题,而是英文复数如何映射到接口中字段名称的缺陷。第二个反例同样是开发人员做出错误命名选择的问题。(参数传递接口的选择本身不会成为可读性的充分条件-问题是在某些情况下它是否是必要条件或对它有帮助)。
mtraceur

1
+1表示有关实现成本和进行命名参数的权衡的总体要点。我只是认为开始会失去人们。
mtraceur

@mtraceur您的意思是正确的。现在,五年后,我可能会写出完全不同的答案。如果您愿意,可以编辑我的答案以删除不太有用的内容。(通常,这样的编辑会被拒绝,因为它们与作者的意图相抵触,但在这里我可以接受)。例如,我现在相信许多命名参数问题只是动态语言的限制,它们应该只具有类型系统。例如,JavaScript具有Flow和TypeScript,Python具有MyPy。通过适当的IDE集成,可以消除大多数可用性问题。我仍在等待Perl中的某些东西。
阿蒙(Amon)

7

出于同样的原因,匈牙利符号不再被广泛使用;Intellisense(或其在非Microsoft IDE中的道德标准)。多数现代IDE只需将鼠标悬停在参数引用上,即可告诉您所有需要了解的参数。

也就是说,许多语言都支持命名参数,包括C#和Delphi。在C#中,它是可选的,因此,如果您不想使用它,则不必使用它,并且还有其他方法来专门声明成员,例如对象初始化。

当您只想指定可选参数的子集而不是全部时,命名参数最有用。在C#中,这非常有用,因为您不再需要一堆重载方法来为程序员提供这种灵活性。


4
Python使用命名参数,这是该语言的绝佳功能。我希望更多的语言使用它。它使默认参数变得更容易,因为您不再像在C ++中那样被迫在默认值之前“显式”指定任何参数。(就像您在上一段中所讲的那样,这是python避免不重载的方法...默认参数和名称参数使您可以执行所有相同的操作。)命名参数也使代码更具可读性。如果您有类似的内容sin(radians=2),则不需要“ Intellisense”。
使机器人变形

2

Bash确实支持这种编程风格,Perl和Ruby都支持传递名称/值参数列表(与任何支持本机hash / map的语言一样)。没有什么可以阻止您选择定义将名称附加到参数的结构/记录或哈希/映射。

对于本地包含哈希/映射/键值存储的语言,只需采用惯用语将键/值散列传递给函数/方法,就可以获取所需的功能。一旦在项目中进行了尝试,您将可以更好地了解是否获得了任何收益,无论是通过易用性提高生产率,还是通过减少缺陷来提高质量。

请注意,经验丰富的程序员已经习惯于按顺序/插槽传递参数。您可能还会发现,这种成语仅在您有多个论点(例如> 5/6)时才有价值。而且,由于大量参数通常表示(过于)复杂的方法,因此您可能会发现该惯用法仅对最复杂的方法有用。


2

我认为这是C#比VB.net更受欢迎的原因。虽然VB.NET更具“可读性”,例如,您键入“ end if”而不是右括号,但最终它会填满更多内容,从而使代码更加难以理解。

事实证明,使代码更易于理解的是简洁。越少越好。无论如何,函数参数名称通常都很明显,并且不会真正帮助您理解代码。


1
我坚决不同意“越少越好”。顺理成章地讲,代码高尔夫赢家将是“最佳”计划。
莱利少校

2

命名参数是对重构源代码中的问题的一种解决方案,并不旨在使源代码更具可读性。命名参数用于帮助编译器/解析器解析函数调用的默认值。如果要使代码更具可读性,请添加有意义的注释。

难以重构的语言通常支持命名参数,因为foo(1)foo()更改签名时会中断名称,但是foo(name:1)中断的可能性较小,程序员需要花费较少的精力进行更改foo

当您必须在以下函数中引入新参数并且有数百行或数千行代码调用该函数时,该怎么办?

foo(int weight, int height, int age = 0);

大多数程序员将执行以下操作。

foo(int weight, int height, int age = 0, string gender = null);

现在,不需要重构,并且该函数可以在gendernull为空时以旧版模式执行。

具体来说,gender现在将值硬编码到的调用中age。作为此示例:

 foo(10,10,0,"female");

程序员查看了函数定义,发现其age默认值为,0并使用了该值。

现在,age函数定义中的默认值完全没有用。

命名参数使您可以避免此问题。

foo(weight: 10, height: 10, gender: "female");

可以将新参数添加到其中,foo而不必担心它们的添加顺序,并且可以更改默认值,因为您知道所设置的默认值确实是真实的默认值。

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.