在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)的更多相关文章

  1. Java中引用类型变量,对象,值类型,值传递,引用传递 区别与定义

    一.Java中什么叫做引用类型变量?引用:就是按内存地址查询       比如:String s = new String();这个其实是在栈内存里分配一块内存空间为s,在堆内存里new了一个Stri ...

  2. java中值传递和引用传递

    最近工作中使用到了值传递和引用传递,但是有点懵,现在看了下面的文章后清晰多了.一下是文章(网摘) 1:按值传递是什么 指的是在方法调用时,传递的参数是按值的拷贝传递.示例如下: public clas ...

  3. Java中的值传递和引用传递

    这几天一直再纠结这个问题,今天看了这篇文章有点思路了,这跟C++里函数参数为引用.指针还是有很大区别. 当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里 ...

  4. java中方法的参数传递机制(值传递还是引用传递)

    看到一个java面试题: 问:当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?  答:是值传递.Java 编程语言只有值传递参 ...

  5. java中函数是值传递还是引用传递?

    相信有些同学跟我一样,曾经对这个问题很疑惑.在网上也看了一些别人说的观点,评论不一.有说有值传递和引用传递两种,也有说只有值传递的,这里只说下个人见解 先看一个例子 public class Test ...

  6. C++中值传递、指针传递、引用传递的总结

    C++中值传递.指针传递.引用传递的总结   指针传递和引用传递一般适用于:函数内部修改参数并且希望改动影响调用者.对比值传递,指针/引用传递可以将改变由形参"传给"实参(实际上就 ...

  7. 一道笔试题来理顺Java中的值传递和引用传递

      题目如下: private static void change(StringBuffer str11, StringBuffer str12) { str12 = str11; str11 = ...

  8. java中的值传递和引用传递有什么区别呀?

    值传递: (形式参数类型是基本数据类型和String):方法调用时,实际参数把它的值传递给对应的形式参数,形式参数只是用实际参数的值初始化自己的存储单元内容,是两个不同的存储单元,所以方法执行中形式参 ...

  9. Java中的值传递与引用传递

    1.基本类型和引用类型在内存中的保存 Java中数据类型分为两大类,基本类型和对象类型.相应的,变量也有两种类型:基本类型和引用类型. 基本类型的变量保存原始值,即它代表的值就是数值本身: 而引用类型 ...

  10. java中参数传递--值传递,引用传递

    java中的参数传递——值传递.引用传递   参数是按值而不是按引用传递的说明 Java 应用程序有且仅有的一种参数传递机制,即按值传递. 在 Java 应用程序中永远不会传递对象,而只传递对象引用. ...

随机推荐

  1. Linux学习 - fdisk分区

    一.fdisk命令分区过程 系统一旦重启,分区将消失 1 添加新硬盘 直接在虚拟机上添加 2 查看新硬盘 fdisk -l 3 分区 fdisk /dev/sdb fdisk进入/dev/sdb硬件设 ...

  2. ReactiveCocoa操作方法-秩序

    doNext:      执行Next之前,会先执行这个Block doCompleted:      执行sendCompleted之前,会先执行这个Block - (void)doNext { [ ...

  3. zabbix之被动模式之编译安装proxy

    #:准备源码包,编译安装 root@ubuntu:/usr/local/src# ls zabbix-4.0.12.tar.gz root@ubuntu:/usr/local/src# tar xf ...

  4. clickhouse 输入输出格式

    TabSeparated.TabSeparatedRaw.TabSeparatedWithNames和TabSeparatedWithNamesAndTypes TabSeparated 默认格式,缩 ...

  5. ES6——>let,箭头函数,this指向小记

    let let允许你声明一个作用域被限制在块级中的变量.语句或者表达式. 还是那个经典的问题:创建5个li,点击不同的li能够打印出当前li的序号. 如果在for循环中使用**var**来声明变量i的 ...

  6. 『与善仁』Appium基础 — 20、Appium元素定位

    目录 1.by_id定位 2.by_name定位 3.by_class_name定位 4.by_xpath定位 5.by_accessibility_id定位 6.by_android_uiautom ...

  7. Windows内存管理-分段

    0x01原因 分段的产生原属于安全问题. 一个程序可以自由的访问不属于它的内存位置,甚至可以对那些内容进行修改.这也导致安全问题 促使一种内存隔离的手段 分段的产生. 0x02分段原理 处理器要求在加 ...

  8. MQTT协议 - arduino ESP32 通过精灵一号 MQTT Broker 进行通讯的代码详解

    前言 之前研究了一段时间的 COAP 协议结果爱智那边没有测试工具,然后 arduino 也没有找到合适的库,我懒癌发作也懒得修这库,就只能非常尴尬先暂时放一放了.不过我在 爱智APP -> 设 ...

  9. [BUUCTF]PWN——bjdctf_2020_babystack2

    bjdctf_2020_babystack2 附件 步骤: 例行检查,64位程序,开启了nx保护 尝试运行一下程序,看看情况 64位ida载入,习惯性的先检索程序里的字符串,发现了bin/sh,双击跟 ...

  10. 【01】SpringBoot2核心技术-基础入门

    SpringBoot 2 1. SpringBoot2核心技术-基础入门 01 Spring与SpringBoot 1.Spring 能做什么 1.1 Spring的能力 微服务:将一个应用的所有功能 ...