C#中的ref以及out内解
来源:互联网 发布:vb borderstyle属性 编辑:程序博客网 时间:2024/06/15 07:08
说明问题之前,我们先来看一段代码:
class Program
{
static void Main(string[] args)
{
int a = 1, b = 2;
swap(a, b);
Console.WriteLine("a={0},b={1}", a, b);
}
static void swap(int a, int b)
{
int temp = a;
a = b;
b = temp;
}
}
结果依然是a=1,b=2,swap方法并没有将交换后的结果返回。
这是因为swap方法的形参是按传值形式被调用的。当swap方法准备执行时,形参b和a会被各自赋值(即2和1),然后压入堆栈中,swap方法执行完成后,形参a和b弹栈。这个过程中,Main中的a和b的值始终没有发生变化,所以输出时,依然是原始值。
在java中,我们只能通过return将修改后的值进行返回。但是在C#中,提供了更多的手段,可以不通过return而直接在调用方法中将值修改并影响原来的变量,这就是ref或者out关键字。
class Program
{
static void Main(string[] args)
{
int a = 1, b = 2;
swap(ref a, ref b);
Console.WriteLine("a={0},b={1}", a, b);
}
static void swap(ref int a, ref int b)
{
int temp = a;
a = b;
b = temp;
}
}
这里可以看到,a和b的值已经发生了交换。
那么ref和out的区别在哪里呢?
- ref的参数在调用之前必须先初始化,而out的不需要;
- 在调用方法内,ref的形参可以直接使用,而out的必须先赋值再使用。
比如下面两种错误:
错误一:
其中display方法:
错误二:
总结:
- ref和out提供了按引用传递的功能(这里的“引用”并不是指“引用类型”的引用),我们可以在调用方法内部对传递进来的参数进行修改;
- ref的使用场景:调用方法对参数param进行处理时,依赖param的初始值。比如,方法内部进行param++操作时;
- out的使用场景:调用方法对参数param进行处理时,不依赖param的初始值。比如,方法内部进行param=40这样的赋值操作时。
接下来看一看IL是怎么对待按值或者按引用传递的参数。比如这一段C#代码:
classClass
{
void Method(Class @class) { }
void Method(ref Class @class) { }
//void Method(out Class @class) { }
}
这一段代码是可以正常通过编译的,但是取消注释就不行了,因为IL是不区分ref和out的。
也正是因为这一种重载的可能性,所以在调用方也必须写明ref或out,不然编译器没法区分调用的是哪一个重载版本。
Class类的IL是这样的:
.classprivate auto ansi beforefieldinit CsConsole.Class
extends[mscorlib]System.Object
{
//Methods
.method private hidebysigstatic
void Method (
class CsConsole.Class'class'
) cil managed
{
//Method begins at RVA 0x20b4
// Code size 1 (0x1)
.maxstack 8
IL_0000: ret
} //end of method Class::Method
.method private hidebysigstatic
void Method (
class CsConsole.Class&'class'
) cil managed
{
//Method begins at RVA 0x20b6
// Code size 1 (0x1)
.maxstack 8
IL_0000: ret
} //end of method Class::Method
} // end of class CsConsole.Class
为了阅读方便,我把原有的默认无参构造函数去掉了。
可以看到两个方法的IL仅仅只有一个&符号的差别,这一个符号的差别也是两个方法可以同名的原因,因为它们的参数类型是不一样的。out和ref参数的类型则是一样的。
现在给代码里加一点内容,让差别变得更明显一些:
classClass
{
int i;
void Method(Class@class)
{
@class.i = 1;
}
void Method(ref Class @class)
{
@class.i = 1;
}
}
现在的IL是这样的:
.classprivate auto ansi beforefieldinit CsConsole.Class
extends[mscorlib]System.Object
{
//Fields
.field private int32 i
// Methods
.method private hidebysig
instance void Method (
class CsConsole.Class'class'
) cil managed
{
//Method begins at RVA 0x20b4
// Code size 8 (0x8)
.maxstack 8
IL_0000: ldarg.1
IL_0001: ldc.i4.1
IL_0002: stfld int32CsConsole.Class::i
IL_0007: ret
} //end of method Class::Method
.method private hidebysig
instance void Method (
class CsConsole.Class&'class'
) cil managed
{
//Method begins at RVA 0x20bd
// Code size 9 (0x9)
.maxstack 8
IL_0000: ldarg.1
IL_0001: ldind.ref
IL_0002: ldc.i4.1
IL_0003: stfld int32CsConsole.Class::i
IL_0008: ret
} //end of method Class::Method
} // end of class CsConsole.Class
带ref的方法里多了一条指令“ldind.ref”,关于这条指令MSDN的解释是这样的:
将对象引用作为 O(对象引用)类型间接加载到计算堆栈上。
简单来说就是从一个地址取了一个对象引用,这个对象引用与无ref版本的“arg.1”相同的,即按值传入的@class。
再来换一个角度看看,把代码改成这样:
classClass
{
void Method(Class @class)
{
@class = new Class();
}
void Method(ref Class @class)
{
@class = new Class();
}
}
IL是这样的:
.classprivate auto ansi beforefieldinit CsConsole.Class
extends[mscorlib]System.Object
{
//Methods
.method private hidebysig
instance void Method (
class CsConsole.Class'class'
) cil managed
{
//Method begins at RVA 0x20b4
// Code size 8 (0x8)
.maxstack 8
IL_0000: newobj instance void CsConsole.Class::.ctor()
IL_0005:starg.s'class'
IL_0007: ret
} //end of method Class::Method
.method private hidebysig
instance void Method (
class CsConsole.Class&'class'
) cil managed
{
//Method begins at RVA 0x20bd
// Code size 8 (0x8)
.maxstack 8
IL_0000: ldarg.1
IL_0001: newobj instance voidCsConsole.Class::.ctor()
IL_0006:stind.ref
IL_0007: ret
} //end of method Class::Method
} // end of class CsConsole.Class
这一次两方的差别就更大了。
无ref版本做的事很简单,new了一个Class对象然后直接赋给了@class。
但是有ref版本则是先取了ref引用留着待会用,再new了Class,然后才把这个Class对象赋给ref引用指向的地方。
在来看看调用方会有什么差异:
classClass
{
void Method(Class @class) { }
void Method(ref Class @class) { }
void Caller()
{
Class @class = new Class();
Method(@class);
Method(ref @class);
}
}
.method private hidebysig
instance void Caller () cil managed
{
//Method begins at RVA 0x20b8
// Code size 22 (0x16)
.maxstack 2
.locals init (
[0] class CsConsole.Class'class'
)
IL_0000: newobj instance void CsConsole.Class::.ctor()
IL_0005: stloc.0
IL_0006: ldarg.0
IL_0007: ldloc.0
IL_0008: call instance voidCsConsole.Class::Method(class CsConsole.Class)
IL_000d: ldarg.0
IL_000e: ldloca.s'class'
IL_0010: call instance voidCsConsole.Class::Method(class CsConsole.Class&)
IL_0015: ret
} // end of method Class::Caller
差别很清晰,前者从局部变量表取“值”,后者从局部变量表取“引用”。
四、引用与指针
说了这么久引用,再来看一看同样可以用来写Swap的指针。
很显然,ref参数和指针参数的类型是不一样的,所以这么写是可以通过编译的:
unsafestructStruct
{
void Method(ref Struct @struct) { }
void Method(Struct* @struct) { }
}
这两个方法的IL非常有意思:
.classprivate sequential ansisealed beforefieldinit CsConsole.Struct
extends[mscorlib]System.ValueType
{
.pack 0
.size 1
// Methods
.method private hidebysig
instance void Method (
valuetype CsConsole.Struct&'struct'
) cil managed
{
//Method begins at RVA 0x2050
// Code size 1 (0x1)
.maxstack 8
IL_0000: ret
} //end of method Struct::Method
.method private hidebysig
instance void Method (
valuetype CsConsole.Struct*'struct'
) cil managed
{
//Method begins at RVA 0x2052
// Code size 1 (0x1)
.maxstack 8
IL_0000: ret
} //end of method Struct::Method
} // end of classCsConsole.Struct
ref版本是用了取地址运算符(&)来标记,而指针版本用的是间接寻址运算符(*),含义也都很明显,前者传入的是一个变量的地址(即引用),后者传入的是一个指针类型。
更有意思的事情是这样的:
unsafestructStruct
{
void Method(ref Struct @struct)
{
@struct = default(Struct);
}
void Method(Struct* @struct)
{
*@struct = default(Struct);
}
}
.classprivate sequential ansisealed beforefieldinit CsConsole.Struct
extends[mscorlib]System.ValueType
{
.pack 0
.size 1
// Methods
.method private hidebysig
instance void Method (
valuetypeCsConsole.Struct& 'struct'
) cil managed
{
//Method begins at RVA 0x2050
// Code size 8 (0x8)
.maxstack 8
IL_0000: ldarg.1
IL_0001: initobjCsConsole.Struct
IL_0007: ret
} //end of method Struct::Method
.method private hidebysig
instance void Method (
valuetype CsConsole.Struct* 'struct'
) cil managed
{
//Method begins at RVA 0x2059
// Code size 8 (0x8)
.maxstack 8
IL_0000: ldarg.1
IL_0001: initobjCsConsole.Struct
IL_0007: ret
} //end of method Struct::Method
} // end of classCsConsole.Struct
两个方法体的IL是一模一样的!可以想见引用的本质到底是什么了吧~?
五、this和引用
这个有趣的问题是前两天才意识到的,以前从来没有写过类似这样的代码:
structStruct
{
void Method(ref Struct @struct) { }
publicvoid Test()
{
Method(refthis);
}
}
上面这段代码是可以通过编译的,但是如果像下面这样写就不行了:
classClass
{
void Method(ref Class @class) { }
void Test()
{
//无法将“<this>”作为 ref 或 out 参数传递,因为它是只读的
Method(refthis);
}
}
红字部分代码会报出如注释所述的错误。两段代码唯一的差别在于前者是struct(值类型)而后者是class(引用类型)。
前面已经说过,ref标记的参数在方法内部的修改会影响到方法外的变量值,所以用ref标记this传入方法可能导致this的值被改变。
有意思的是,为什么struct里的this允许被改变,而class里的this不允许被改变呢?
往下的内容和ref其实没啥太大关系了,但是涉及到值和引用,所以还是继续写吧:D
MSDN对“this”关键字的解释是这样的:
this 关键字引用类的当前实例
这里的“当前实例”指的是内存中的对象,也就是下图中的“值”或“引用类型对象”:
如果对值类型的this进行赋值,那么“值”被修改,“当前实例”仍然是原来实例对象,只是内容变了。
而如果对引用类型的this进行赋值,那么“B引用”被修改,出现了类似于这个图的情况,现在的“当前实例”已经不是原来的实例对象了,this关键字的含义就不再明确。所以引用类型中的this应该是只读的,确保“this”就是指向的“这个”对象。
- C#中的ref以及out内解
- c#中的ref out
- C#中的 ref 和 out
- C#中的ref 和out
- C# 中的关键字 out ref
- C#中的out和ref
- C# 中的ref 和 out
- C#中的ref和out
- c#中的ref和out
- c#中的out跟ref
- C# 中的ref & out关键字 的区别
- C#方法中的ref和out
- 关于C#中的out和ref
- C#中的ref out param关键字
- C#中的ref out param关键字
- 浅谈C#中的ref和out参数
- C# 中的 Out 和 Ref 参数
- C#中的两个关键字ref和out
- Timer
- MongoDB配置文件说明
- ThinkPHP--RBAC展示权限列表信息-权限维护
- 对于linux下system()函数的深度理解(整理) (http://blog.sina.com.cn/s/blog_8043547601017qk0.html
- 11992 - Fast Matrix Operations (线段树)
- C#中的ref以及out内解
- 回程路由 的作用 为什么 什么时候需要回程路由
- stuts 2的工作流程及开发步骤
- 设置navigationbar上得字体和颜色
- android 蓝牙激光笔的设计和实现
- Python基础入门之(数字、字符串)
- 得到PopupWindow的高度
- 再论时间延迟(Timing)
- VC 文档+视图 详细分析