C#在方法调用中,参数按值传递与按引用传递的区别是啥?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C#在方法调用中,参数按值传递与按引用传递的区别是啥?相关的知识,希望对你有一定的参考价值。

基本数据类型作为参数默认按什么方式传递?类对象作为参数默认按什么方式传递?类对象可以按值方式传递吗?基本数据类型参数按引用传递时,应该怎么做?关键字ref和out的区别是什么?

1.先来说下C#中的数据类型.分值类型和引用类型两大类.
  值类型:直接存储数据的值,保存在内存中的stack(堆栈)中
  引用类型:存储对值的引用,实际上存储的就是一个内存的地址.引用类型的保存分成两块,实际值保存在托管堆(heap)中.实际值的内存地址保存在stack中
  当使用引用类型时先找到stack中的地址,再找到heap中的实际值.
  也就是说保存引用类型时要用到stack和heap,但使用引用类型时我们实际上只用到stack中的值,然后通过这个值间接的访问heap中的值
2.C#预定义的简单类型,像int,float,bool,char都是值类型,另外enum(枚举),struct(结构)也是值类型
  string,数组,自定义的class就都是引用类型了.其中的string是比较特殊的引用类型.C#给它增加个字符恒定的特性.
  C#函数的参数如果不加ref,out这样的修饰符显式申明参数是通过引用传递外,默认都是值传递.
3.这里要注意的一个问题是,参数的类型是值类型还是引用类型和传参数时用值传递还是引用传递是两个不同的概念.
  假如有void FunTest(int [] array) 和void FunTest(int a)
这两个函数.参数array是引用类型,a是值类型.但是他们传递时都是按值传递.
  我们来举个例子说明下
  按值传递参数:
  class Program
  
  public static void ChangeInt(int num)
  
  num = 123;
  
  public static void ChangeArray(int[] array)
  
  array[0] = 10;
  array = new int[] 6, 7, 8, 9 ;
  
  static void Main(string[] args)
  
  int anum = 1;
  int[] aarray = 1, 2, 3 ;
  ChangeInt(anum);
  ChangeArray(aarray);
  Console.WriteLine("value of num: " + anum);
  Console.Write("value of aarray: ");
  foreach (int i in aarray)
  Console.Write(i + " ");
  
  
  结果是:value of anum : 1
  value of aarray :10 2 3
  可能看到结果会有点奇怪.我们一般认为值传递就是把值拷贝一份,然后不管在函数中对传入的参数做啥改变,参数之前的值不会受啥影响,所以anum没有变成123,仍然是1
  但是aarray[0]为啥却变成10了呢?
  前面我们有说到引用类型在内存中是保存为两个部分,一个是stack中的内存地址,另一个是heap中的实际值.用时我们只直接用stack中的值,我们假如stack中的值为0xabcdefgh
,就说是aaraay指向它吧. 那么我们按值传递时就是把这个stack的值拷贝成另一份就假如是array指向它吧.跟拷贝anum的值1一样.
  但是我们操作内存地址这样的值时不会像整数一样直接操作它,而只会通过它去找heap中的实际值.
  于是我们array[0] = 10.改变了实际上还是heap中数组的值了. 但array = new int []
6,7,8,9没有对之前传的aarray产生影响.这个操作的意义是在heap中重新开辟一块内存,保存着值6,7,8,9.
这这块内存的地址赋给array,于是它之前的值0xabcdefgh被改写了.但aarray指的值stack值仍没变,仍是0xabcdefgh
  按引用传递参数
  可以用out或ref显式指定.它们大部分时候可以通用,只是有一点细小区别.
  先用ref 来举例吧,还用上面的例子,只是加个了关键字ref
  class Program
  
  public static void ChangeInt(ref int num)
  
  num = 123;
  
  public static void ChangeArray(ref int[] array)
  
  array[0] = 10;
  array = new int[] 6, 7, 8, 9 ;
  
  static void Main(string[] args)
  
  int anum = 1;
  int[] aarray = 1, 2, 3 ;
  ChangeInt(ref anum);
  ChangeArray(ref aarray);
  Console.WriteLine("value of num: " + anum);
  Console.Write("value of aarray: ");
  foreach (int i in aarray)
  Console.Write(i + " ");
  
  
  结果是:value of anum : 123
  value of aarray :6 7 8 9
  跟按值传递的结果完全不同吧
  num = 123我们是容易理解.我们再来说下aarray的值为啥变了吧
  按引用传递时aarray指向的stack中的值不会复制一份,而是直接传过去.这样array[0]= 10这样赋值时也同样改变了heap中 1 2 3
的值,变为10 2 3,如果
  没有array = new int [] 6,7,8,9
这个语句,则它的结果跟上面按值传递是完全一样的.但有个这句话后就不一样,我们知道上面说了它的含义,在heap中开辟一块新内存
  值是6 7 8 9,而aarray指向的stack的值被改写了,改为指向保存6 7 8 9的内存地址了.那含有10 2
3的那一块内存其实还继续存在,只是没有谁引用到它了.到时垃圾回收器会把它回收的.
  补充:
  说下out 和ref的细小区别
  ref 传进来的参数必须要先赋值.
  像上面 的例子中如果这样写
  int num;
  ChangeInt(ref int num);
  就会出错,必须先给num给个值1.
  而且out传进来的参数可以不先赋值.
  out num;
  ChangeInt(out int num);是对的
  另外还有个区别就是如果用out的时候ChangeInt函数中必须有某个地方给num赋值了,而用ref不一定需要在函数中给num赋值
  其实这样做的目的很好理解.C#为了确保在任何情况下num必须有个值,不能为空.
  因为用ref,在调用函数前必须保证参数有值,所以在函数中就不必要求它一定再赋值
  而用out由于在调用函数前不用保证参数必须有值,所以在函数中必须保证给它个值
  ChangeInt(ref int num)和ChangeInt(out int num)虽然不一样,但是不同共存,不能当作两个不同的函数
  而ChangeInt(int num)和上面 的两个函数是完全不一样的,可以放到一起共存
  这样的话调用的时候ref ,out这样的关键字不能省的.必须匹配
参考技术A 基本数据类型默认是值传递,类对象在.net中是不能值传递的因为对象指针在堆上,传递只传指针 参考技术B 基本数据传递引用在声明和调用的地方都加上ref,大部分是调用com接口的时候 参考技术C 如果说ref和out传递参数的时候都是传个地址,非得找个区别的话,ref进去可以不赋值,只用传进去的参数,但是out传递不赋值变异不过本回答被提问者采纳 参考技术D 按顺序回答一,.net中引用传递和值传递和c一样,一个传过去值不会发生改变引用传递传过去值会发生改变,但是在.net中大部分的实例化的对象传递都默认是引用传递,ref一个关键字我见的最多的是在和com接口使用的时候

Common Lisp:按值传递与按引用传递[重复]

【中文标题】Common Lisp:按值传递与按引用传递[重复]【英文标题】:Common Lisp: pass by value vs pass by reference [duplicate] 【发布时间】:2021-12-16 03:58:29 【问题描述】:

我想用 Common Lisp 编写一个函数,它会破坏性地修改它的参数。在 C 或 Rust 中,我会使用一个指向对象的指针/引用,它可以在函数体内被取消引用。我在 CL 中写道:

(defun foo (lst)
  (setf lst NIL))

但是在评估这个表格之后:

(let ((obj (list "a" "b")))
  (foo obj)
  obj) => ("a" "b")

看到函数foo 没有效果。我可以通过按值语义来解释这一点,我们修改推入函数堆栈的参数的本地副本。 如果我们定义另一个函数:

(defun bar (lst)
  (setf (car lst) NIL))

并评估类似的形式

(let ((obj (list "a" "b")))
  (bar obj)
  obj) => (NIL "b")

我们将清楚地看到lst 被修改,就好像我们将使用按引用传递语义一样。所以,(setf lst NIL) 没用,但 (setf (car lst) NIL) 没用。你能解释一下原因吗?

【问题讨论】:

问题出在setf。第一个参数需要place。在foo 中,符号lst 脱离了上下文,但在bar 中,该位置仍然存在于外部上下文中。 setf 用符号作为位置表示您想要改变变量指向的内容,而不是它绑定的对象。例如。 lst = 0(setf lst nil) 相同。 lst 是一个对象,所以(setf (car lst) ...) 改变了对象的汽车访问器,因此你现在更新的是对象而不是绑定。 【参考方案1】:

Common Lisp 按值传递参数,但在大多数情况下(除了原始值类型,如 fixnums 或 floats),值是引用。这与大多数托管语言(例如 Java、Python、JS)的工作方式相同。

LET-form 中,变量OBJ 的值是对列表的引用,而不是列表本身。该引用是按值传递的,因此在函数内部,参数LST 的值是对同一列表的另一个引用。

FOO中,引用值被替换为NIL,但是之前引用的列表完全没有被触及。在BAR 中,从堆中检索列表并将其CAR 替换为NIL。由于OBJ 持有对同一列表的引用,因此修改也会对其产生影响。

【讨论】:

感谢您的回答。有一件事还不是很清楚。我可以自己决定,setf 是否应该跟随一个指针,从堆中检索一个对象并修改它,或者它应该只修改它的绑定?如何修改函数foo,使其参数设置为NIL? 如果你使用一个普通的变量名作为SETF的位置,它总是只会修改变量,而不是被引用的对象。要修改对象,必须使用一些访问器(例如CARCDRAREF 等)。要让FOO 像你想要的那样工作,你必须将列表包装在某种盒子对象中,然后你可以修改(例如零秩数组或结构),但更惯用的 Lisp 只返回修改后的根据Rainer Joswig的对象。【参考方案2】:

对于某些操作,通常的样式是这样的:

(let ((obj (list "a" "b")))
  (setf obj (nbar obj))  ; nbar is destructively modifying the list argument
  (foo obj))

上面使用SETF使用返回值来改变obj的绑定,因为函数nbar本身是做不到的。

由于NBAR 无法访问词法变量obj,因此无法更改它,因此应该从NBAR 返回对象,然后将变量设置为结果值——覆盖前一个绑定。

此规则适用的示例:调用sortnreverse、...

【讨论】:

以上是关于C#在方法调用中,参数按值传递与按引用传递的区别是啥?的主要内容,如果未能解决你的问题,请参考以下文章

在 C# 中将 REF 和 OUT 关键字与按引用传递和按值传递一起使用

Java:按值传递与按引用传递

Common Lisp:按值传递与按引用传递[重复]

java学习笔记:java的方法参数传递

java的按值传递与按引用传递

哪个更快?按引用传递与按值传递 C++