C# 引用参数
最近经常和同事讨论引用参数的问题,为了搞清楚,查了些资料,其中CLR via C#中讲的比较清楚,整理了下
----摘自(CLR via C#)
在默认情况下,CLR假设所有的方法参数都是按值传递的。当参数为引用类型的对象时,参数的传递时通过传递指向对象的引用来完成的(引用本身是按值传递的)。这意味着方法可以改变引用对象,并且调用代码可以看到这种改变的结果。
对于一个方法,我们必须知道它的每个参数是引用类型参数,还是值类型的参数,因为我们编写的操作参数的代码会因此有很大的差别。
除了按值传递参数外,CLR还允许我们按引用的方式来才传递参数。在C#中,我们可以用out和ref关键字来做到这一点。这两个关键字告诉C#编译器要产生额外的元数据来表示指定参数是按引用的方式来传递的:编译器将使用该信息来产生传递参数地址(而不是参数本身的值)的代码。
关键字out和ref的不同之处在于哪个方法负责初始化参数。如果一个方法的参数被标识为out,那么调用代码在调用该方法之前可以不初始化该参数,并且被调用方法不能直接读取参数的值,它必须在返回之前为该参数赋值。如果一个方法的参数被标识为ref,那么调用代码在调用该方法之前必须首先初始化该参数。被调用方法则可以任意选择读取该参数、或者为该参数赋值。
引用类型参数和值类型参数在使用out和ref关键字时的行为有很大的区别。下面我们先来看一看在值类型参数上使用out关键字时的行为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
class Program { static void Main( string [] args) { Int32 x; SetVal( out x); //x不必被初始化 Console.WriteLine(x); //显示“10” } static void SetVal( out Int32 v) { v = 10; ; //SetVal方法必须初始化 } } |
在上面的代码中,x首先被声明在线程的堆栈上。接着,x的地址被传递给SetVal。SetVal的参数v是一个指向Int32值类型的指针。在SetVal内部,v指向的Int32被赋值为10。当SetVal返回后,Main中的x的值将为10,控制台上的结果自然也将为“10”。在值类型参数上使用out关键字会为代码带来一定的效率提升,因为他避免了值类型实例的字段在方法调用时的拷贝操作。
我们再来看一看在值类型参数上使用ref关键字时的行为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
class Program { static void Main( string [] args) { Int32 x = 5; AddVal( ref x); //x必被初始化 Console.WriteLine(x); //显示“15” } static void AddVal( ref Int32 v) { v += 10; //AddVal方法可以直接使用经过初始化的v } } |
在上面的代码中,x首先被声明在线程的堆栈上,紧接着便初始化为5。随后x的地址被传递给AddVal。AddVal的参数v是一个指向Int32值类型的指针。在AddVal内部,v指向的Int32必须为一个经过初始化的值。这样AddVal才可以在任何表达式中使用该初始值,也可以改变它,并且改变后的值会被“返回”给调用代码。在上面的例子中,AddVal将10加到该初始值上。当AddVal返回后, Main中x的值将为15,自然在控制台上显示的结果也将为“15”。
从IL或者CLR的角度来看,out和ref关键字的行为实际上是一样的:它们都会导致指向实例的指针被传递给方法。两者的不同之处在于编译器会根据它们选择不同的机制来确保我们的代码是正确的,例如,下面的代码视图向一个需要ref参数的方法传递一个未经初始化的值,从而导致编译错误:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
class Program { static void Main( string [] args) { Int32 x; //x没有被初始化 //下面一行将导致编译失败,编译器将产生错误信息 //error cs0165:使用了未赋值的局部变量‘x’ AddVal( ref x); //x必被初始化 Console.WriteLine(x); //显示“15” } static void AddVal( ref Int32 v) { v += 10; //AddVal方法可以直接使用经过初始化的v } } |
另外,CLR允许我们根据out和ref参数来重载方法。例如,下面的代码就是合法的:
1
2
3
4
5
6
7
8
9
|
class Point { static void Add(Point p){ } static void Add( ref Point p) { } } |
但是仅通过区分out和ref来重载方法又是不合法的,因为它们经JIT编译后的代码是相同。所以我们不能在上面的point类型中再定义下面的方法:
1
|
static void Add( out Point p) { } |
在值类型参数上使用out和ref关键字与用传值的方式来传递引用类型的参数在某种程度上具有相同的行为,对于前一种情况,out和ref关键字允许被调用方法直接操作一个值类型实例。调用代码必须为该实例分配内存,而被调用方法操作该内存。对于后一种情况,调用代码负责为引用类型对象分配内存,而被调用方法通过传入的引用来操作对象。基于这种行为,只有当一个方法要“返回”一个它已知的对象引用时,在引用类型参数上使用out和ref关键字才有意义。看下面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
class App { static public void Main() { FileStream fs; //打开第一个待处理文件 StartProcessingFiles( out fs); //如果有更多需要处理的文件,则继续 for (; fs!= null ; ContinueProcessingFiles( ref fs)) { //处理文件 fs.Read(...); } } static void StartProcessingFiles( out FileStream fs) { fs= new FileStream(...); } static void ContinueProcessingFiles( ref FileStream fs) { fs.Close(); //关闭上一次操作的文件 //打开下一个文件:如果没有文件,则返回null if (noMoreFilesToProcess) fs = null ; else fs= new FileStream(...); } } |
如我们所见,这段代码中最大的不同在于有着out或者ref修饰的引用类型参数的方法创建一个对象后,指向新对象的指针会被返回给调用代码。另外注意ContinueProcessingFiles方法在返回新对象之前可以操作传入的对象,这是因为其参数被标识为ref。
下面的代码是上述代码的一个简化版本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
class App { static public void Main() { FileStream fs = null ; //初始化为null(必要的操作) //打开第一个待处理文件 ProcessingFiles( ref fs); for (; fs != null ; ProcessingFiles( ref fs)) { //处理文件 fs.Read(...); } } static void ProcessingFiles( ref FileStream fs) { //如果先前的文件打开的,则将其关闭 if (fs != null ) fs.Close(); //关闭上一次操作的文件 //打开下一个文件:如果没有文件,则返回null if (noMoreFilesToProcess) fs = null ; else fs= new FileStream(...); } } |
下面的例子演示了怎样使用ref关键字来交换两个引用类型:
1
2
3
4
5
6
|
static public void Swap( ref object a, ref object b) { object t = b; b = a; a = t; } |
要交换两个String对象引用,大家可能会考虑像下面怎样做:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
static public void SomeMethod() { string s1 = "jeff" ; string s2 = "rich" ; Swap( ref s1, ref s2); Console.WriteLine(s1); //显示rich Console.WriteLine(s2); //显示jeff } |
可以看到,修正后的SomeMethod会通过编译,并且会按我们所期望的行为执行,C#要求以引用方式传递的参数必须和方法期望的参数完全匹配的目的是为了确保类型安全。下面的代码展示了如果类型不匹配可能导致类型安全漏洞(不会通过编译)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
class SomeType { public int val; } class App { static void Main() { SomeType st; //下面一行将产生编译错误error cs1503: 参数‘1’: //无法从‘out SomeType’转换为‘out object’ GetAnObject( out st); Console.WriteLine(st.val); } static void GetAnObject( out object o) { o = new string ( 'X' , 100); } } |
在这段代码中,Main期望GetAnObject返回一个SomeType对象。但是,因为GetAnObject得签名表示的是一个指向object的引用,所以GetAnObject可以将o初始化为一个任何类型的对象。当GetAnObject返回到Main中时,st将指向一个string,这显然不是一个SomeType对象,对Console.WriteLine的调用自然会失败。幸运的是,C#编译器不会编译上面的代码,因为st是一个指向SomeType的引用,而GetAnObject要求的是指向object的引用
C# 引用参数的更多相关文章
- 常量函数、常量引用参数、常量引用返回值[C++]
1. 关于常量引用正像在C语言中使用指针一样,C++中通常使用引用 有一个函数... foo()并且这个函数返回一个引用...... & foo()...., 一个指向位图(Bitmap)的引 ...
- C#的值参数与引用参数
值参数:在使用值参数时,是把变量的值传给函数,函数中对此变量的任何修改都不影响该变量本身的值. 引用参数:使用引用参数时,在函数中对此变量的修改会影响变量的值. 说简单点,值参数,就是我把身份证复印件 ...
- 实例对比剖析c#引用参数的用法
c#引用参数传递的深入剖析值类型的变量存储数据,而引用类型的变量存储对实际数据的引用.(这一点很重要,明白了之后就能区分开值类型和引用类型的差别) 在参数传递时,值类型是以值的形式传递的(传递的是值, ...
- C#方法的六种参数,值参数、引用参数、输出参数、参数数组、命名参数、可选参数
方法的参数有六种,分别是值参数.引用参数.输出参数.参数数组.命名参数.可选参数. 值参数 值参数是方法的默认类型,通过复制实参的值到形参的方式把数据传递到方法,方法被调用时,系统作两步操作: 在栈中 ...
- C#_delegate - 值参数和引用参数
值参数不能加,引用参数可以. 引用参数是共享的 using System; using System.Collections.Generic; using System.Linq; using Sys ...
- C#值参数和引用参数
一.值参数 未用ref或out修饰符声明的参数为值参数. 使用值参数,通过将实参的值复制到形参的方式,把数据传递到方法.方法被调用时,系统做如下操作. 在栈中为形参分配空间. 复制实参到形参. 值参数 ...
- [C++学习历程]基础部分 C++中的函数中的值参数、地址参数、引用参数实际例子
本文地址:http://blog.csdn.net/sushengmiyan/article/details/20406269 作者:sushengmiyan // sushengmiyanTest. ...
- c#中引用类型作为值参数和引用参数问题
一.分类 C#的值类型包括:结构体(数值类型,bool型,用户定义的结构体),枚举,可空类型. C#的引用类型包括:数组,用户定义的类.接口.委托,object,字符串. 二.参数传递 对于引用类型, ...
- 非const引用参数传入不同类型编译不过的理解(拒绝将临时对象绑定为非const的引用的形参是有道理的)
int f (int & I) { cout<<I<<std::endl; } void main() { long L; f(L); // 编译不过 f((int)L ...
随机推荐
- 修改win7锁定界面背景
Regedit HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Windows/CurrentVersion/Authentication/LogonUI/Backgrou ...
- Java 集合 HashMap & HashSet 拾遗
Java 集合 HashMap & HashSet 拾遗 @author ixenos 摘要:HashMap内部结构分析 Java HashMap采用的是冲突链表方式 从上图容易看出,如果选择 ...
- MVC3+EF4.1学习系列(二)-------基础的增删改查和持久对象的生命周期变化
上篇文章中 我们已经创建了EF4.1基于code first的例子 有了数据库 并初始化了一些数据 今天这里写基础的增删改查和持久对象的生命周期变化 学习下原文先把运行好的原图贴来上~~ 一.创建 ...
- Java I/O 操作的一些基本知识
1.文件类:File ,也是唯一的单独的文件类.可以对文件进行操作.其方法有:exists(),delete(),isDirectory(),createNewFile(),getName(),get ...
- VC中获取窗口控件相对客户区的坐标
1: RECT rect; 2: GetDlgItem(item_id).GetWindowRect(&rect); 3: ScreenToClient(&rect);
- c# 操作word demo
/// <summary> /// 新创建word /// </summary> /// <param name="fileSaveDirectory" ...
- #define const extern
将父类中的常量放到. m文件,子类就不会重复包含了.之后再.h文件中用extern NSSting * const ILScoreShowStartTime;// extern 用来声明变量和函数.c ...
- 让上下两个DIV块之间有一定距离或没有距离
1.若想上下DIV块之间距离,只需设定:在CSS里设置DIV标签各属性参数为0div{margin:0;border:0;padding:0;}这里就设置了DIV标签CSS属性相当于初始化了DIV标签 ...
- chrome浏览器调试工具的使用
废话不多说,给大家介绍一下最基本的浏览器调试工具
- mybatis----增删改查
转: select使用 : xml代码: <!-- 查询学生,根据id --> <select id="getStudent" parameterType=&qu ...