百科问答小站 logo
百科问答小站 font logo



为什么 Java 只有值传递,但 C# 既有值传递,又有引用传递,这种语言设计有哪些好处? 第1页

  

user avatar   Ivony 网友的相关建议: 
      

好吧,大家都在忙着解释这个语法的功用和名词的含义,却没有怎么去回答提问者真正的问题么:

这种语言设计有哪些好处?

糖?当然是糖,但事实上任何一个程序设计语言都是一坨糖,否则大家都图灵完备的,你用汇编不也能写出来?仅仅一个糖并不足以说明这种设计的优势。

除了大家说的简化语法之外,这种语法设计还有两个重要的优势:

1、性能。

C#/CLR其实给予了程序员完整的操作栈对象的能力,ref参数本质上就是栈对象引用,栈对象(值类型对象)相较于引用类型对象是有极大的性能优势的,虽然这种性能的优势大多数时候并不能完整的体现出来,但是当这个方法被调用成千上万次时,这种优势就会变得非常可观。

2、互操作性。

在语言和运行时层面支持这种语法,对于互操作性是有很大的帮助的,这样一来,带有引用传递参数的C++的函数便可以轻松地映射为C#的函数,实现两个语言之间的无缝调用。


user avatar   guqiangqiang 网友的相关建议: 
      

先强调这个问题前半句是真命题。说问题逻辑有问题,说一切都是值传递,都是没理解什么叫引用传递和值传递。


虽然这个问题根本就没有在问“Java是不是值传递”,但是看完其它答案发现,如果不先解释清楚到底什么是值传递,什么是引用传递,后面的好处也无从谈起。只关心好处的请拉到最后。


第一种误解是:Java是引用传递。(这么理解的人,大体会解释说Java的形参是对象的引用所以才叫引用传递。这个解释的错误在于:引用传递这个词不是这个意思,这个词是形容调用方式,而不是参数本质的类型的。所以,即使有人因为明白引用本身也是个值,然后觉得Java其实是值传递了,这种理解也是错的。你这种理解,叫“传递的是值”,而非“值传递”。后面展开。)

第二种误解是:值类型是值传递,引用类型用的是引用传递。

第三种误解是:认为所有的都是值传递,因为引用本质上也是个值,本质就是个指针嘛。

第四种误解是:常出现在C++程序员中,声明的参数是引用类型的,就是引用传递;声明的参数是一般类型或指针的就是值传递。(也有人把指针归为引用传递,其实它比较特殊,无论你归哪边都是错的。)


值传递与引用传递,在计算机领域是专有名词,如果你没有专门了解过,一般很难自行悟出其含义。而且在理解下面的解释时,请不要把任何概念往你所熟悉的语言功能上套。很容易产生误解。比如Reference,请当个全新的概念,它和C#引用类型中的引用,和C++的&,一点儿关系都没有。


值传递和引用传递,属于函数调用时参数的求值策略(Evaluation Strategy),这是对调用函数时,求值和传值的方式的描述,而非传递的内容的类型(内容指:是值类型还是引用类型,是值还是指针)。值类型/引用类型,是用于区分两种内存分配方式,值类型在调用栈上分配,引用类型在堆上分配。(不要问我引用类型里定义个值类型成员或反之会发生什么,这不在这个本文的讨论范畴内,而且你看完之后,你应该可以自己想明白)。一个描述内存分配方式,一个描述参数求值策略,两者之间无任何依赖或约束关系。


在函数调用过程中,调用方提供实参,这些实参可以是常量:

Call(1);

也可以是变量:

Call(x);

也可以是他们的组合:

Call(2 * x + 1);

也可以是对其它函数的调用:

Call(GetNumber());

但是所有这些实参的形式,都统称为表达式(Expression)。求值(Evaluation)即是指对这些表达式的简化并求解其值的过程。

求值策略(值传递和引用传递)的关注的点在于,这些表达式在调用函数的过程中,求值的时机、值的形式的选取等问题。求值的时机,可以是在函数调用前,也可以是在函数调用后,由被调用者自己求值。这里所谓调用后求值,可以理解为Lazy Load或On Demand的一种求值方式。


而且,除了值传递和引用传递,还有一些其它的求值策略。这些求值策略的划分依据是:求值的时机(调用前还是调用中)和值本身的传递方式。详见下表:



看到这里的名传递,可能就有人联想到C++里的别名(alias),其实也是两码事儿。语言层直接支持名传递的语言很不主流,但是在C#中,名传递的行为可以用Func<T>来模拟,说到这儿应该能大概猜出名传递的大致行为了。不过这不是重点,重点是值传递和引用传递。上面给出的传值方式的表述有些单薄,下表列出了一些二者在行为表象上的区别。


这里的改变不是指mutate, 而是change,指把一个变量指向另一个对象,而不是指仅仅改变属性或是成员什么的(如Java,所以说Java是Pass by value,原因是它调用时Copy,实参不能指向另一个对象,而不是因为被传递的东西本质上是个Value,这么讲计算机上什么不是Value?)。


这些行为,与参数类型是值类型还是引用类型无关。对于值传递,无论是值类型还是引用类型,都会在调用栈上创建一个副本,不同是,对于值类型而言,这个副本就是整个原始值的复制。而对于引用类型而言,由于引用类型的实例在堆中,在栈上只有它的一个引用(一般情况下是指针),其副本也只是这个引用的复制,而不是整个原始对象的复制。


这便引出了值类型和引用类型(这不是在说值传递)的最大区别:值类型用做参数会被复制,但是很多人误以为这个区别是值类型的特性。其实这是值传递带来的效果,和值类型本身没有关系。只是最终结果是这样。


求值策略定义的是函数调用时的行为,并不对具体实现方式做要求,但是指针由于其汇编级支持的特性,成为实现引用传递方式的首选。但是纯理论上,你完全可以不用指针,比如用一个全局的参数名到对象地址的HashTable来实现引用传递,只是这样效率太低,所以根本没有哪个编程语言会这样做。(自己写来玩玩的不算)


综上所述,对于Java的函数调用方式最准确的描述是:参数藉由值传递方式,传递的值是个引用。(句中两个“值”不是一个意思,第一个值是evaluation result,第二个值是value content)


由于这个描述太绕,而且在字面上与Java总是传引用的事实冲突。于是对于Java,Python、Ruby、JavaScript等语言使用的这种求值策略,起了一个更贴切名字,叫Call by sharing。这个名字诞生于40年前。


前面讨论了各种求值策略的内涵。下面以C++为例:

       void ByValue(int a) {  a = a + 1; }  void ByRef(int& a) {  a = a + 1; }  void ByPointer(int* a) {  *a = *a + 1; } int main(int argv, char** args) {  int v = 1;  ByValue(v);  ByRef(v);   // Pass by Reference  ByPointer(&v);   // Pass by Value  int* vp = &v;  ByPointer(vp); }      

Main函数里的前两种方式没有什么好说,第一个是值传递,第二个函数是引用传递,但是后面两种,同一个函数,一次调用是Call by reference, 一次是Call by value。因为:


ByPointer(vp); 没有改变vp,其实是无法改变。


ByPointer(&v); 改变了v。(你可能会说,这传递的其实是v的地址,而ByPointer无法改变v的地址,所以这是Call by value。这听上去可以自圆其说,但是v的地址,是个纯数据,在调用的方代码中并不存在,对于调用者而言,只有v,而v的确被ByPointer函数改了,这个结果,正是Call by reference的行为。行为考虑,才是求值策略的本意。如果把所有东西都抽象成值,从数据考虑问题,那根本就没有必要引入求值策略的概念去混淆视听。


请体会一下,应该就明白上面一直在说的调用的行为的意思。


C语言不支持引用,只支持指针,但是如上文所见,使用指针的函数,不能通过签名明确其求值策略。C++引入了引用,它的求值策略可以确定是Pass by reference。于是C++的一个奇葩的地方来了,它语言本身(模拟的不算,什么都能模拟)支持Call by value和Call by reference两种求值策略,但是却提供了三种语法去做这俩事儿。


C#的设计就相对合理,函数声明里,有ref/out,就是引用传递,没有ref/out,就是值传递,与参数类型无关。


不过如果观察一下void ByRef(int& a)和void ByPointer(int* a)所生成的汇编代码,会发现在一定条件下其实是一样的。都是这个样子:

       ; 12   : {   push ebp  mov ebp, esp  sub esp, 192    ; 000000c0H  push ebx  push esi  push edi  lea edi, DWORD PTR [ebp-192]  mov ecx, 48     ; 00000030H  mov eax, -858993460    ; ccccccccH  rep stosd  ; 13   :  *a = *a + 1;   mov eax, DWORD PTR _a$[ebp]  mov ecx, DWORD PTR [eax]  add ecx, 1  mov edx, DWORD PTR _a$[ebp]  mov DWORD PTR [edx], ecx      

调用方的代码也是一样的。代码就不贴了。


这两种传递方式说完了,下面回到正题说好处。问题中“这种”指代不明,且认为是Java。


支持多种求值策略可以给语言带来更高的灵活性,但是同时也需要一个“灵活”的人来良好地驾驭。Java通过牺牲这种价值不大还可能带来问题的灵活性,带来了语言自身语法一致性、逻辑鲁棒性及更容易学习等多个好处。


不仅仅Java和C#,每个语言,在设计时都需要在这些特性间做出自己独特的取舍来体现自己的设计理念,并适应不同人,不同使用环境的要求。虽然说没有什么功能是一个语言可以做,而另一个语言做不了的。但是每个语言,都有它最适合的范畴与不适合的范畴。


user avatar   hollis-11 网友的相关建议: 
      

先下结论:电影想把Freddie塑造成一个有人性的神,却忘了真正的Freddie只是一个有神性的人

如果作为一部粉丝向的情怀片,《波》已经达到了满分,哪怕不谈对细节出色的把控,光是最后二十分钟的神级还原已经足够让所有的情怀在we are the champions中泪流满面

感受一下当时的直播:

Live Aid https://www.zhihu.com/video/1092941240030597120


Live Aid https://www.zhihu.com/video/1092941515751579648

但是作为一部传记片,《波》还是太流程化了,才华横溢的主角惊艳出场,遇到小人,遭遇挫折,众叛亲离,踢开小人,亲友重聚,完美收场。作为人物小传也算及格,但是对于Freddie这样的传奇人物的剖析还是不够大胆,想要表现其人性的一面,又不敢去探索Freddie其实也有自私功利的角落,想要表现其亦男亦女的魅力,却又只是浮于外表没有触碰到灵魂,以至于片子自始至终有种畏手畏脚的憋屈感。

不过不管受众是谁,《波西米亚狂想曲》至少是一部及格线以上的作品,再加上Queen的音乐加成,哪怕不至于血脉喷张,但让观众在电影院点点头抖抖腿还是绰绰有余了

看完电影之后,再看到波西米亚狂想曲的歌词,或许会有一些不一样的体会

Is this the real life

Is this just fantasy

Caught in a landslide. No escape from reality

Open your eyes.Look up to the skies and see

I'm just a poor boy, I need no sympathy

Because I'm easy come, easy go,A little high, little low,

Anyway the wind blows, doesn't really matter to me

freddie的生命像一场华丽的错觉,但他所留下来的,is not fantasy




  

相关话题

  大一学生想换 Linux 怎么样? 
  当你学会了什么之后感觉自己的编程算是入门了? 
  程序员必须掌握哪些算法? 
  按照现在硬件发展的速度,是否还需要高效的代码? 
  如何看待go语言泛型的最新设计? 
  为什么老板要教我们做互联网的程序员不要把工作和生活分开,这对于独立的程序员个体有什么好处? 
  学习编程被父母阻止怎么应对? 
  在函数的入口处对参数的合法性进行检查是一个值得提倡的好习惯吗? 
  现有的编程语言当中,哪些和自然语言更接近? 
  为什么上古编程语言(比如 COBOL)总喜欢把代码全部写成大写字母? 

前一个讨论
在C#中 String.Empty和 "" 有什么区别?
下一个讨论
为什么要先 git add 才能 git commit ?





© 2024-12-25 - tinynew.org. All Rights Reserved.
© 2024-12-25 - tinynew.org. 保留所有权利