当前位置: 首页 > news >正文

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的引用

转载于:https://www.cnblogs.com/yzl050819/p/5896223.html

相关文章:

  • 委托在窗体通信方面的见解
  • Java编程里的类和对象
  • 计算某个生日是哪个星座的算法
  • this在方法赋值过程中无法保持(隐式丢失)
  • (三)uboot源码分析
  • Swift - 数组排序方法(附样例)
  • Image控件Stretch属性
  • C语言学习笔记--递归函数
  • UML-用例
  • 【Apache大系】Apache服务器面面观
  • MongoDB:实体对象(javabean)转DBObject
  • 关于TCP/IP协议
  • 【Python开发】Python PIL ImageDraw 和ImageFont模块学习
  • CSS学习(一)
  • 问题
  • AHK 中 = 和 == 等比较运算符的用法
  • canvas实际项目操作,包含:线条,圆形,扇形,图片绘制,图片圆角遮罩,矩形,弧形文字...
  • CSS选择器——伪元素选择器之处理父元素高度及外边距溢出
  • Electron入门介绍
  • iOS | NSProxy
  • JAVA多线程机制解析-volatilesynchronized
  • Js基础知识(一) - 变量
  • Median of Two Sorted Arrays
  • Netty 4.1 源代码学习:线程模型
  • oldjun 检测网站的经验
  • Redis在Web项目中的应用与实践
  • REST架构的思考
  • Spark VS Hadoop:两大大数据分析系统深度解读
  • 工作踩坑系列——https访问遇到“已阻止载入混合活动内容”
  • 类orAPI - 收藏集 - 掘金
  • 前端js -- this指向总结。
  • 一起参Ember.js讨论、问答社区。
  • 一些css基础学习笔记
  • [Shell 脚本] 备份网站文件至OSS服务(纯shell脚本无sdk) ...
  • 教程:使用iPhone相机和openCV来完成3D重建(第一部分) ...
  • ​TypeScript都不会用,也敢说会前端?
  • # 20155222 2016-2017-2 《Java程序设计》第5周学习总结
  • #在线报价接单​再坚持一下 明天是真的周六.出现货 实单来谈
  • ()、[]、{}、(())、[[]]命令替换
  • (+3)1.3敏捷宣言与敏捷过程的特点
  • (zt)最盛行的警世狂言(爆笑)
  • (仿QQ聊天消息列表加载)wp7 listbox 列表项逐一加载的一种实现方式,以及加入渐显动画...
  • (附源码)spring boot儿童教育管理系统 毕业设计 281442
  • (附源码)springboot青少年公共卫生教育平台 毕业设计 643214
  • (官网安装) 基于CentOS 7安装MangoDB和MangoDB Shell
  • (含react-draggable库以及相关BUG如何解决)固定在左上方某盒子内(如按钮)添加可拖动功能,使用react hook语法实现
  • (论文阅读笔记)Network planning with deep reinforcement learning
  • (一)appium-desktop定位元素原理
  • (转)Linux下编译安装log4cxx
  • (转)大道至简,职场上做人做事做管理
  • (转)四层和七层负载均衡的区别
  • .gitignore文件_Git:.gitignore
  • .NET Core SkiaSharp 替代 System.Drawing.Common 的一些用法
  • .NET Framework 的 bug?try-catch-when 中如果 when 语句抛出异常,程序将彻底崩溃
  • .NET中的Event与Delegates,从Publisher到Subscriber的衔接!