C#中的值传递与引用传递(in、out、ref)
在C#中,方法、构造函数可以拥有参数,当调用方法或者构造函数时,需要提供参数,而参数的传递方式有两种(以方法为例):
值传递
值类型对象传递给方法时,传递的是值类型对象的副本而不是值类型对象本身。常用的一个例子:
public struct MyStruct
{
public int Value { get; set; }
}
static void Invoke(MyStruct myStruct, int i)
{
//MyStruct和int都是值类型
myStruct.Value = 1;
i = 2;
Console.WriteLine($"Modify myStruct.Value = {myStruct.Value}");
Console.WriteLine($"Modify i = {i}");
}
static void Main(string[] args)
{
var myStruct = new MyStruct();//Value=0
var i = 0;
Invoke(myStruct, i);
Console.WriteLine($"Main myStruct.Value = {myStruct.Value}");
Console.WriteLine($"Main i = {i}"); //输出:
//Modify myStruct.Value = 1
//Modify i = 2
//Main myStruct.Value = 0
//Main i = 0
}
对于引用类型对象,很多人认为它是引用传递,其实不对,它也是按值传递的,但是不像值类型传递的是一个副本,引用类型传递的是一个地址(可以认为是一个整型数据),在方法中使用这个地址去修改对象的成员,自然就会影响到原来的对象,这也是很多人认为它是引用传递的原因,一个简单的例子:
public class MyClass
{
public int Value { get; set; }
}
static void Invoke(MyClass myClass)
{
myClass.Value = 1;
Console.WriteLine($"Modify myClass.Value = {myClass.Value}");
}
static void Main(string[] args)
{
var myClass = new MyClass();//Value=0
Invoke(myClass);
Console.WriteLine($"Main myClass.Value = {myClass.Value}"); //输出:
//Modify myClass.Value = 1
//Main myClass.Value = 1
}
需要注意的是,如果值类型对象中含有引用类型的成员,那么当值类型对象在传递给方法时,副本中克隆的是引用类型成员的地址,而不是引用类型对象的副本,所以在方法中修改此引用类型对象成员中的成员等也会影响到原来的引用类型对象。
引用传递
引用传递可以理解为就是对象本身传递,而非一个副本或者地址,一般使用 in、out、ref 关键字声明参数是引用传递。
在说 in、out、ref 之前,先看看引用传递与值传递的区别,以更好的理解引用传递。
对于值类型对象,看一个最简单的变量值交换的例子:
static void Swap(int i,int j)
{
var temp = i;
i = j;
j = temp;
}
static void Main(string[] args)
{
int i = 1;
int j = 2;
Swap(i, j);//交换i,j
Console.WriteLine($"i={i}, j={j}"); //输出:i=1, j=2
}
可以看到,i,j的值没有交换,因为值类型值传递传的是一个副本,这就好比,值对象的数据保存在一个房间中,比如桌子凳子椅子,作为方法参数传递时,会将这个房间包括里面的桌子凳子椅子全部克隆一份得到一个新房间,然后将这个新房间搬走使用,对新房间的装修挥霍自然对原房间没有影响。
上面的代码可以翻译为:
static void Main(string[] args)
{
int i = 1;
int j = 2; //这是Swap方法执行过程
//先创建两个临时变量,赋值为i,j
//在方法中使用的是这两个临时变量
int m = i, n = j;
{
var temp = m;
m = n;
n = temp;
} Console.WriteLine($"i={i}, j={j}");//输出:i=1, j=2
}
再看看引用传递的例子:
static void Swap(ref int i,ref int j)
{
var temp = i;
i = j;
j = temp;
}
static void Main(string[] args)
{
int i = 1;
int j = 2;
Swap(ref i, ref j);
Console.WriteLine($"i={i}, j={j}");//输出:i=2, j=1
}
可以看到,i,j的值交换成功,因为这里搬走使用的不再是克隆出来的新房间,而是原房间!
这里的代码可以翻译为:
static void Main(string[] args)
{
int i = 1;
int j = 2; //这是Swap方法执行过程
//没有创建临时变量,在方法中直接使用i,j
//注:这里是有创建临时变量,只是变量是引用,等价于原对象的一个别名
{
var temp = i;
i = j;
j = temp;
}
Console.WriteLine($"i={i}, j={j}");//输出:i=2, j=1
}
再看看引用类型对象,在值传递中,引用类型传递的是地址,在方法中可以通过这个地址去修改对象成员而影响到原对象的成员,但是无法影响到整个对象,看下面的例子:
public class MyClass
{
public int Value { get; set; }
}
static void Invoke(MyClass myClass)
{
myClass = new MyClass() { Value = 1 };
}
static void Main(string[] args)
{
MyClass myClass = new MyClass();//Value=0
Invoke(myClass);
Console.WriteLine($"myClass.Value={myClass.Value}");//输出:myClass.Value=0
}
可以看到,Main方法中将myClass对象传入Invoke方法,在Invoke方法中给Invoke方法赋值,但是这并没有影响到Main方法中的myClass对象,这就好比,引用类型对象的数据保存在房间A中,作为方法参数传递时,会新建一个房间B,房间B保存的是房间A的地址,对房间B的任何修改会转向这个地址去修改,也就是房间A的修改,现在将房间B保存的地址换成房间C的地址,对房间B的操作自然跟房间A没有关系了。
可以将上面的Main方法大致翻译成这样子:
static void Main(string[] args)
{
MyClass myClass = new MyClass();//Value=0 //这是Invke方法执行过程
//创建临时变量,在方法中使用临时变量
MyClass temp = myClass;
{
temp = new MyClass() { Value = 1 };
} Console.WriteLine($"myClass.Value={myClass.Value}");//输出:myClass.Value=0
}
但如果是引用传递,结果就不一样了:
static void Invoke(ref MyClass myClass)
{
myClass = new MyClass() { Value = 1 };
}
static void Main(string[] args)
{
MyClass myClass = new MyClass();//Value=0
Invoke(ref myClass);
Console.WriteLine($"myClass.Value={myClass.Value}");//输出:myClass.Value=1
}
这是因为引用传递传的是对象本身,而不是地址,这就是说,在传递时,没有创建一个房间B,而是直接使用的房间A!(准确说,是给房间A取了一个别名)
上面的Main方法可以翻译为:
static void Main(string[] args)
{
MyClass myClass = new MyClass();//Value=0 //这是Invke方法执行过程
//没有创建临时变量,在方法中直接使用myClass
//注:这里是有创建临时变量,只是变量是引用,等价于原对象的一个别名
{
myClass = new MyClass() { Value = 1 };
} Console.WriteLine($"myClass.Value={myClass.Value}");//输出:myClass.Value=1
}
可以理解为,引用类型对象的引用传递,其实就是给对象取了一个别名,其它与原对象一模一样。
到这里,应该能对值传递和引用传递区分开了,接下来看看引用传递的 in、out、ref 的用法。
in
在C#中,可以在下面这些地方使用in关键字:
1、在泛型接口和委托的泛型参数中使用in关键字作为逆变参数,如:Action<in T>
2、作为参数修饰符,这是接下来要说的
3、在foreach中使用in迭代
4、在Linq表达式中的join、from子句中使用in关键字
作为参数修饰符,in修饰的参数表示参数通过引用传递,但是参数是只读的,所以in修饰的参数在调用方法时必须先初始化!
public struct MyStruct
{
public int Value { get; set; }
}
public class MyClass
{
public int Value { get; set; }
}
static void Invoke(in MyClass myClass, in MyStruct myStruct, in int i)
{
//in参数是只读的,下面的赋值将会报错
//myClass = new MyClass();
//myStruct = new MyClass();
//i = 1; //类成员可以直接读写
myClass.Value = myClass.Value + 2;
//结构体成员只能读,直接写会报错
var value = myStruct.Value + 1;
//结构体成员在不安全代码中可以使用指针实现写操作
unsafe
{
fixed (MyStruct* p = &myStruct)
{
(*p).Value = myStruct.Value + 1;//可以写
}
}
}
在调用时,我们需要满足下面的条件:
1、传递之前变量必须进行初始化
2、多数情况下调用in关键字可以省略,当使用in关键字时,变量类型应与参数类型一致
3、可以使用常量作为参数,但是要求常量可以隐式转换成参数类型,编译器会生成一个临时变量来接收这个常量,然后使用这个临时变量调用方法
如:
MyClass myClass = new MyClass();
MyStruct myStruct = new MyStruct();
int i = 1; Invoke(in myClass, in myStruct, in i);
Invoke(myClass, myStruct, i);
Invoke(in myClass, in myStruct, 2);
out
在C#中,out参数可以用作:
1、在泛型接口和委托的泛型参数中使用out关键字作为协变参数,如:Func<out T>
2、作为参数修饰符,这是接下来要说的
作为参数修饰符,out修饰的参数表示参数通过引用传递,但是参数是必须是一个变量,且在方法中必须给这个变量赋值,但是在调用方法时无需初始化:
public struct MyStruct
{
public int Value { get; set; }
}
public class MyClass
{
public int Value { get; set; }
}
static void Invoke(out MyClass myClass, out MyStruct myStruct, out int i)
{
//out参数必须在返回之前赋一个值
myClass = new MyClass() { Value = 1 };
myStruct = new MyStruct() { Value = 2 };
i = 1; //赋值之后,类成员、结构体成员都可以直接读写
}
在调用时:
1、必须声明out关键字,且变量类型应与参数类型一致
2、变量无需初始化,只需声明即可
3、如果不关注out参数的返回值,我们常使用弃元
例如:
//参数需要初始化
MyClass myClass;
MyStruct myStruct;
int i;
Invoke(out myClass, out myStruct, out i); //等价写法
Invoke(out MyClass myClass, out MyStruct myStruct, out int i); bool isInt = long.TryParse("1", out _);//判断字符串是否是整型而不需要结果
bool isBool = bool.TryParse("true", out _);//判断字符串是否是布尔型而不关注结果
ref
ref关键字的用法有很多,具体可见:C#中ref关键字的用法
作为参数修饰符,ref修饰的参数表示参数通过引用传递,但是参数是必须是一个变量。
ref 可以看做是 in 和 out 的结合体,但是与 in 和 out 又有些区别:
1、ref和in都是引用传递,而且要求调用方法前需要提前初始化,但是与in不同的是,调用时ref关键字不能省略,且参数必须是变量,不能是常量
2、ref和out都是引用传递,且在调用是,ref和out关键字不能省略,且参数必须是变量,不能是常量,但是ref要求调用方法前需要提前初始化,且无需在调用方法结束前赋值
3、与in和out不同的是,在调用方法中时,可以读写整个ref参数对象及它的成员
看看上面变量值交换的例子应该就清晰了。
in、out、ref的限制
C#中规定,引用传递(即in、out、ref)使用时有下面的限制:
1、异步方法,即使用async修饰的方法中,参数不能使用in、out、ref关键字,但是可以在那些没有使用async关键字且返回Task或者Task<T>类型的同步方法中使用
2、迭代器方法,即使用yield return和yield break返回迭代对象的方法中,,参数不能使用in、out、ref关键字
3、如果拓展方法的第一个参数(this)是结构体,且非泛型参数,则可使用in关键字,否则不能使用in关键字
4、拓展方法的第一个参数(this)不能使用out关键字
5、如果拓展方法的第一个参数(this)非结构体,也非约束为结构体的泛型参数,则不能使用ref关键字
此外,in、out、ref不能作为重载的标识,也就是说,如果两个方法,除了这三个关键字修饰的不同,其他如方法名,参数个数、类型等都相同,但是不能算重载:
//下面的三个方法,除了in、out、ref,其他都一样,但是不能算重载,编译不通过
public void Method1(in string str) { }
public void Method1(out string str) { str = ""; }
public void Method1(ref string str) { } //下面的三个方法,除了in、out、ref,其他都一样,但是不能算重载,编译不通过
public void Method2(in string str, out int i) { i = 0; }
public void Method2(out string str, in int i) { str = ""; }
public void Method2(ref string str, ref int i) { }
但是,一个不使用in、out、ref使用的方法,和一个使用in、out、ref参数的方法可以构成重载:
//下面的两个方法算重载,调用这样的重载,需要在调用是指定in、out、ref来区分调用
public static void Method1(string str) { }
public static void Method1(in string str) { }//可以使用in、out、ref //下面的三个方法算重载,调用这样的重载,需要在调用是指定in、out、ref来区分调用
public static void Method2(string str, int i) { i = 0; }
public static void Method2(string str, out int i) { i = 0; }
public static void Method2(in string str, out int i) { i = 0; }
参考文档:
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/in-parameter-modifier
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/out-parameter-modifier
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/ref
C#中的值传递与引用传递(in、out、ref)的更多相关文章
- Java中引用类型变量,对象,值类型,值传递,引用传递 区别与定义
一.Java中什么叫做引用类型变量?引用:就是按内存地址查询 比如:String s = new String();这个其实是在栈内存里分配一块内存空间为s,在堆内存里new了一个Stri ...
- java中值传递和引用传递
最近工作中使用到了值传递和引用传递,但是有点懵,现在看了下面的文章后清晰多了.一下是文章(网摘) 1:按值传递是什么 指的是在方法调用时,传递的参数是按值的拷贝传递.示例如下: public clas ...
- Java中的值传递和引用传递
这几天一直再纠结这个问题,今天看了这篇文章有点思路了,这跟C++里函数参数为引用.指针还是有很大区别. 当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里 ...
- java中方法的参数传递机制(值传递还是引用传递)
看到一个java面试题: 问:当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递? 答:是值传递.Java 编程语言只有值传递参 ...
- java中函数是值传递还是引用传递?
相信有些同学跟我一样,曾经对这个问题很疑惑.在网上也看了一些别人说的观点,评论不一.有说有值传递和引用传递两种,也有说只有值传递的,这里只说下个人见解 先看一个例子 public class Test ...
- C++中值传递、指针传递、引用传递的总结
C++中值传递.指针传递.引用传递的总结 指针传递和引用传递一般适用于:函数内部修改参数并且希望改动影响调用者.对比值传递,指针/引用传递可以将改变由形参"传给"实参(实际上就 ...
- 一道笔试题来理顺Java中的值传递和引用传递
题目如下: private static void change(StringBuffer str11, StringBuffer str12) { str12 = str11; str11 = ...
- java中的值传递和引用传递有什么区别呀?
值传递: (形式参数类型是基本数据类型和String):方法调用时,实际参数把它的值传递给对应的形式参数,形式参数只是用实际参数的值初始化自己的存储单元内容,是两个不同的存储单元,所以方法执行中形式参 ...
- Java中的值传递与引用传递
1.基本类型和引用类型在内存中的保存 Java中数据类型分为两大类,基本类型和对象类型.相应的,变量也有两种类型:基本类型和引用类型. 基本类型的变量保存原始值,即它代表的值就是数值本身: 而引用类型 ...
- java中参数传递--值传递,引用传递
java中的参数传递——值传递.引用传递 参数是按值而不是按引用传递的说明 Java 应用程序有且仅有的一种参数传递机制,即按值传递. 在 Java 应用程序中永远不会传递对象,而只传递对象引用. ...
随机推荐
- 【Linux】【Basis】块存储,文件存储,对象存储
1. 块存储: 定义:这种接口通常以QEMU Driver或者Kernel Module的方式存在,这种接口需要实现Linux的Block Device的接口或者QEMU提供的Block Driver ...
- i++ 和 ++i 探究原理
先看一个例子: package com.test; public class AutoIncrement { public static void main(String[] args) { int ...
- 小程序的事件 bindtap bindinput
一.bindtap事件 在wxml文件里绑定: <view class='wel-list' bindtap='TZdown'> <image src="/images/w ...
- java 对 final 关键字 深度理解
基础理解 : 1.修饰类 当用final去修饰一个类的时候,表示这个类不能被继承.处于安全,在JDK中,被设计为final类的有String.System等,这些类不能被继承 .注意:被修饰的类的成员 ...
- 莫烦python教程学习笔记——learn_curve曲线用于过拟合问题
# View more python learning tutorial on my Youtube and Youku channel!!! # Youtube video tutorial: ht ...
- Jenkins制品管理
目录 一.简介 二.Jenkins管理制品 三.Nexus maven上传 jenkins上传 管理Docker镜像 管理raw 四.拷贝制品 五.版本号 Version Number 一.简介 制品 ...
- 解决android studio no debuggable process
这个问题可能是由多种因素造成的. 一.可能是buildtypes配置或选择错误,在对应module的build.gradle中确认如下配置 debug { debuggable true//一定要配置 ...
- LuoguP7257 [COCI2009-2010#3] FILIP 题解
Content 有两个十进制三位数 \(a,b\),请输出这两个数翻转之后的较大数. 数据范围:\(100\leqslant a,b\leqslant 999\),\(a,b\) 中不包含 \(0\) ...
- org.apache.taglibs.standard.tlv.JstlBaseTLV.validate
exception org.apache.jasper.JasperException: PWC6033: Error in Javac compilation for JSP org.apache. ...
- Xftp设置指定记事本(notepad++)打开文件
右键