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⽅法执⾏过程writeline输出数值变量
//没有创建临时变量,在⽅法中直接使⽤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关键字的⽤法有很多,具体可见:
  作为参数修饰符,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; }
  参考⽂档:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。