我只是在想,如果在调用函数时可以编写以下代码,那么读取代码会容易得多:
doFunction(param1=something, param2=somethingElse);
我想不出任何缺点,它会使代码更具可读性。我知道您可以将数组作为唯一的参数传递,并将数组键作为参数名称,但是这样的可读性仍然不高。
我缺少这种缺点吗?如果不是,为什么许多语言不允许这样做?
我只是在想,如果在调用函数时可以编写以下代码,那么读取代码会容易得多:
doFunction(param1=something, param2=somethingElse);
我想不出任何缺点,它会使代码更具可读性。我知道您可以将数组作为唯一的参数传递,并将数组键作为参数名称,但是这样的可读性仍然不高。
我缺少这种缺点吗?如果不是,为什么许多语言不允许这样做?
Answers:
指定参数名称并不总是使代码更具可读性:例如,您希望阅读min(first=0, second=1)
还是min(0, 1)
?
如果您接受上一段的参数,那么很明显,指定参数名称不是必须的。为什么所有语言都不将指定参数名称作为有效选项?
我至少可以想到两个原因:
请注意,我不了解任何实现命名参数而不实现可选参数(需要命名参数)的语言-因此您应该警惕高估它们的可读性,这可以通过Fluent Interfaces的定义更一致地获得。
当我阅读一段代码时,命名参数可以引入上下文,使代码更易于理解。例如,考虑以下构造函数: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)
您将看到x和y数据行的两个位置参数,然后是命名参数的列表。有更多具有默认值的选项,并且仅列出那些我想要更改或明确指定其默认值的选项。一旦我们忽略了这段代码使用了幻数,并且可以从使用枚举中受益(如果R有的话!),问题就在于这些参数名中的许多都是难以理解的。
pch
实际上是绘图字符,即将为每个数据点绘制的字形。17
是一个空圆圈或类似的东西。lty
是线型。这1
是一条实线。bty
是盒子类型。将其设置为"n"
避免在绘图周围绘制一个框。ann
控制轴注释的外观。对于不知道每个缩写含义的人来说,这些选项相当令人困惑。这也揭示了R为什么使用这些标签的原因:不是作为自文档代码,而是(作为一种动态类型的语言)作为将值映射到其正确变量的键。
函数签名可能具有以下属性:
不同的语言落在该系统的不同坐标上。在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; }});
it is easier to forget the exact names of some parameters than it is to forget their order
” ...这是一个有趣的断言。
出于同样的原因,匈牙利符号不再被广泛使用;Intellisense(或其在非Microsoft IDE中的道德标准)。多数现代IDE只需将鼠标悬停在参数引用上,即可告诉您所有需要了解的参数。
也就是说,许多语言都支持命名参数,包括C#和Delphi。在C#中,它是可选的,因此,如果您不想使用它,则不必使用它,并且还有其他方法来专门声明成员,例如对象初始化。
当您只想指定可选参数的子集而不是全部时,命名参数最有用。在C#中,这非常有用,因为您不再需要一堆重载方法来为程序员提供这种灵活性。
sin(radians=2)
,则不需要“ Intellisense”。
Bash确实支持这种编程风格,Perl和Ruby都支持传递名称/值参数列表(与任何支持本机hash / map的语言一样)。没有什么可以阻止您选择定义将名称附加到参数的结构/记录或哈希/映射。
对于本地包含哈希/映射/键值存储的语言,只需采用惯用语将键/值散列传递给函数/方法,就可以获取所需的功能。一旦在项目中进行了尝试,您将可以更好地了解是否获得了任何收益,无论是通过易用性提高生产率,还是通过减少缺陷来提高质量。
请注意,经验丰富的程序员已经习惯于按顺序/插槽传递参数。您可能还会发现,这种成语仅在您有多个论点(例如> 5/6)时才有价值。而且,由于大量参数通常表示(过于)复杂的方法,因此您可能会发现该惯用法仅对最复杂的方法有用。
命名参数是对重构源代码中的问题的一种解决方案,并不旨在使源代码更具可读性。命名参数用于帮助编译器/解析器解析函数调用的默认值。如果要使代码更具可读性,请添加有意义的注释。
难以重构的语言通常支持命名参数,因为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);
现在,不需要重构,并且该函数可以在gender
null为空时以旧版模式执行。
具体来说,gender
现在将值硬编码到的调用中age
。作为此示例:
foo(10,10,0,"female");
程序员查看了函数定义,发现其age
默认值为,0
并使用了该值。
现在,age
函数定义中的默认值完全没有用。
命名参数使您可以避免此问题。
foo(weight: 10, height: 10, gender: "female");
可以将新参数添加到其中,foo
而不必担心它们的添加顺序,并且可以更改默认值,因为您知道所设置的默认值确实是真实的默认值。