为什么有那么多语言按价值传递?


36

即使是像C这样的具有显式指针操作的语言,也总是按值传递(您可以按引用传递它们,但这不是默认行为)。

这有什么好处,为什么这么多的语言通过值传递,为什么其他的语言通过引用传递?(我不确定Haskell是通过引用传递的,尽管我不确定)。


5
void acceptEntireProgrammingLanguageByValue(C++);
Thomas Eding 2012年

4
还可能会更糟糕的。某些旧语言也允许按名称致电
hugomg

25
实际上,在C语言中您不能通过引用传递。您可以按值传递指针,这与按引用传递非常相似,但不一样。但是,在C ++中,您可以通过引用传递。
梅森·惠勒2012年

@Mason Wheeler您可以详细说明还是添加一些链接,因为您不是我的C / C ++专家,您的陈述对我来说还不清楚,谢谢
Betlista 2012年

1
@Betlista:通过引用传递,你可以写一个交换程序,看起来像这样:temp := a; a := b; b := temp;当它返回的值a,并b会被交换。用C语言无法做到这一点。你必须通过指针abswap例程有作用于它们指向的值。
梅森惠勒2012年

Answers:


58

值传递通常比引用传递更安全,因为您不会意外地修改方法/函数的参数。这使语言更易于使用,因为您不必担心为函数提供的变量。您知道它们不会被更改,而这通常是您期望的

但是,如果修改参数,则需要进行一些显式操作以使其清楚(传递指针)。这将迫使您的所有调用者进行稍有不同的调用(&variable在C中为),这明确表明可以更改变量参数。

因此,现在您可以假定一个函数不会更改您的变量参数,除非它被明确标记为这样做(通过要求您传递一个指针)。这是比替代方案更安全,更清洁的解决方案:假定所有内容都可以更改您的参数,除非他们明确声明不能更改。


4
衰减到指针的+1数组存在明显的异常,但总体解释是好的。
dasblinkenlight 2012年

6
@dasblinkenlight:数组在C中是一个痛点:(
Matthieu M.

55

按值调用和按引用调用是很久以前就误认为参数传递模式的实现技术。

最初,有FORTRAN。FORTRAN仅具有按引用调用的功能,因为子例程必须能够修改其参数,并且计算周期过于昂贵,以至于无法使用多个参数传递模式,而且首次定义FORTRAN时对编程的了解还不够。

ALGOL提出了按名称致电和按值致电。按值调用用于不应该更改的事物(输入参数)。名称呼叫用于输出参数。事实证明按姓名呼叫是一个主要的障碍,ALGOL 68放弃了它。

PASCAL提供了按值调用和按引用调用。它没有提供任何方式让程序员告诉编译器他正在通过引用传递一个大对象(通常是数组),以避免破坏参数堆栈,但是不应更改该对象。

PASCAL添加了指向语言设计词典的指针。

C通过定义kludge运算符以返回指向内存中任意对象的指针,提供了按值调用和模拟的按引用调用。

后来的语言复制了C语言,主要是因为设计人员从未见过其他东西。这可能就是按值致电如此受欢迎的原因。

C ++在C代码的顶部添加了一个代码,以提供按引用调用。

现在,作为按值调用,按引用调用与按指针调用的混合的直接结果,C和C ++(程序员)对const指针和指向const的指针(只读)感到头痛不已对象。

艾达设法避免了整个噩梦。

Ada没有显式的按值调用与按引用调用。相反,Ada具有in参数(可以读取但不能写入),out参数(必须在可以读取之前写入)和in out参数,可以按任意顺序读取和写入。编译器决定是通过值还是通过引用传递特定参数:它对程序员是透明的。


1
+1。这是我到目前为止在这里看到的唯一答案,它实际上回答了这个问题并且是有意义的,并且不会将其用作支持FP人士的透明借口。
梅森惠勒2012年

11
+1为“后来的语言复制了C,主要是因为设计人员从未见过其他东西”。每当我看到带有0前缀的八进制常量的新语言时,我都会在里面消亡。
librik 2012年

4
这可能是《编程圣经》的第一句话: In the beginning, there was FORTRAN

1
关于ADA,如果将全局变量传递给in / out参数,该语言是否会阻止嵌套调用检查或修改它?如果没有,我认为in / out和ref参数之间会有明显的区别。就个人而言,我希望看到语言明确支持“输入/输出/输入+输出”和“按值/按引用/无关”的所有组合,因此程序员可以针对其意图提供最大的清晰度,但可以使用优化程序(只要它符合程序员的意图)将具有最大的实现灵活性。
超级猫2014年

2
@Neikos还有一个事实是,该0前缀与其他所有非十进制的前缀不一致。如果八进制是引入时八进制是唯一的替代基础,那么在C语言(和大多数与源兼容的语言,例如C ++,Objective C)中,这可能是可以原谅的,但是当更现代的语言同时使用0x并且0从一开始就使用它们时,这只会使它们看起来很差深思熟虑。
8bittree '16

13

通过引用传递会产生非常细微的意外副作用,当它们开始引起意外行为时,很难将其几乎追踪到。

按值传递,尤其是finalstatic或者const输入参数使得这整个类的错误消失。

不可变的语言更具确定性,并且更容易推理和理解函数的内容和预期结果。


7
这是您尝试调试“古老的” VB代码后发现的事情之一,默认情况下所有内容都通过引用得到。
jfrankcarr 2012年

也许只是我的背景不同,但我不知道您在说的是哪种“非常微妙的意外副作用,很难甚至无法追踪”。我习惯了Pascal,其中按值是默认值,但可以通过显式标记参数(基本上与C#相同的模型)来使用按引用,而我从来没有遇到过这样的问题。我可以看到在“古代VB”中默认使用by-ref是有问题的,但是当选择加入by-ref时,它会让您在编写它时考虑它。
梅森惠勒2012年

4
@MasonWheeler出现在您身后的新手呢,只是复制您所做的而没有理解它,然后开始间接地在整个地方操纵该状态,现实世界一直在发生。减少间接访问是一件好事。不变是最好的。

1
@MasonWheeler简单的别名示例(例如Swap,不使用临时变量的hack)虽然本身不​​太可能成为问题,但它们显示了调试更复杂的示例的难度。
马克·赫德2012年

1
@MasonWheeler:FORTRAN中的经典示例导致说“变量不会;常量不是”,这将像3.0这样的浮点常量传递给修改传入参数的函数。对于传递给函数的任何浮点常量,系统都会创建一个“隐藏”变量,并使用适当的值对其进行初始化,然后可以将其传递给函数。如果函数在其参数上添加1.0,则程序中3.0的任意子集可能突然变为4.0。
超级猫

8

为什么有那么多语言按价值传递?

将大型程序分解为较小的子例程的目的是,您可以独立地推理子例程。引用传递会破坏此属性。(共享的可变状态也是如此。)

即使是像C这样的具有显式指针操作的语言,也总是按值传递(您可以按引用传递它们,但这不是默认行为)。

实际上,C 始终是按值传递,而不是按引用传递。您可以接受某物的地址并传递该地址,但是该地址仍将按值传递。

这有什么好处,为什么这么多的语言通过值传递,为什么其他的语言通过引用传递

使用通过引用的主要原因有两个:

  1. 模拟多个返回值
  2. 效率

我个人认为#1是虚假的:它几乎始终是不良API和/或语言设计的借口:

  1. 如果您需要多个返回值,请不要模拟它们,只需使用支持它们的语言即可。
  2. 您也可以通过将多个返回值打包到某些轻量级数据结构(例如元组)中来模拟它们。如果该语言支持模式匹配或解构绑定,则效果特别好。例如Ruby:

    def foo
      # This is actually just a single return value, an array: [1, 2, 3]
      return 1, 2, 3
    end
    
    # Ruby supports destructuring bind for arrays: a, b, c = [1, 2, 3]
    one, two, three = foo
    
  3. 通常,您甚至不需要多个返回值。例如,一种流行的模式是子例程返回错误代码,并且实际结果通过引用写回。相反,如果错误是意外错误,则仅应引发异常;如果预期错误,则应返回Either<Exception, T>。另一种模式是返回一个布尔值,该布尔值指示操作是否成功,并通过引用返回实际结果。同样,如果失败是意外的,则应该引发异常,如果期望失败,例如,在字典中查找值时,则应返回a Maybe<T>

引用传递可能比值传递更有效,因为您不必复制值。

(我不确定Haskell是通过引用传递的,尽管我不确定)。

不,Haskell不是传递引用。它也不是按值传递的。引用传递和值传递都是严格的评估策略,但是Haskell是非严格的。

实际上,Haskell规范并未指定任何特定的评估策略。大多数Hakell实现都使用按名称调用和按需调用(带备注的按名称调用的变体)的混合体,但是该标准并未强制要求这样做。

请注意,即使对于严格的语言,也无法区分功能语言的按引用传递或按值传递,因为仅当您对引用进行了更改时,才能观察到它们之间的差异。因此,实现者可以在两者之间自由选择,而不会破坏语言的语义。


9
“如果需要多个返回值,请不要模拟它们,只需使用支持它们的语言即可。” 这说起来有点奇怪。它基本上是说:“如果你的语言可以做你需要的一切,不过有一个功能,你可能需要在你的代码的不到1% -但你还是需要它为1% -不能在做以一种特别干净的方式,那么您的语言对您的项目来说还不够好,您应该用另一种语言重写整个内容。” 抱歉,但这简直太荒谬了。
梅森惠勒2012年

+1我完全同意这一点。将大型程序分解为较小的子例程的要点是,您可以独立地推理这些子例程。引用传递会破坏此属性。(共享的可变状态也是如此。)
Rémi2014年

3

根据语言的调用模型,参数的类型和语言的存储模型,会有不同的行为。

对于简单的本机类型,按值传递允许您通过寄存器传递值。这可能非常快,因为不需要从内存中加载值,也无需保存回去。一旦被调用方完成了参数的重用,也可以简单地通过重用自变量使用的内存来进行类似的优化,而不必担心会弄乱对象的调用方副本。如果参数是一个临时对象,那么您可能会保存一个完整副本(C ++ 11使用新的右引用及其移动语义使这种优化更加明显)。

在许多OO语言中(在这种情况下C ++例外),您不能按值传递对象。您被迫通过引用传递它。这使代码默认情况下是多态的,并且更适合面向对象的实例的概念。另外,如果您要按价值传递,则必须自己制作副本,并确认产生此类操作的性能成本。在这种情况下,该语言会为您选择最有可能为您提供最佳性能的方法。

对于功能语言,我想按值或引用传递只是一个优化问题。由于这种语言的函数是纯函数,因此没有副作用,因此除了速度之外,实际上没有任何理由复制该值。我什至可以肯定,这种语言经常共享具有相同值的对象的相同副本,只有当您使用传递(常量)引用语义时,这种可能性才可用。Python还将此技巧用于整数和通用字符串(如方法和类名),解释了为什么整数和字符串是Python中的常量对象。通过允许使用指针比较而不是内容比较,并且对某些内部数据进行延迟评估,这也有助于再次优化代码。


2

您可以按值传递表达式,这很自然。通过(临时)引用传递表达式是...很奇怪。


1
同样,通过临时引用传递表达式可能会导致错误的错误(使用有状态语言),当您愉快地更改它时(由于它只是临时的),但是当您实际传递变量时,它适得其反,因此您必须设计丑陋的解决方法,例如传递foo +0而不是foo。
Herby 2012年

2

如果您通过引用通过,那么您实际上总是在使用全局值以及全局变量的所有问题(即范围和意外副作用)。

像全局变量一样,引用有时是有益的,但它们不应成为您的首选。


3
我想你的意思是在第一句话中引用传递?
jk。
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.