默认情况下,CLR假设所有的方法参数都是按值传递参数的。当参数为引用类型的对象时,参数的传递是通过传递对象的引用(或)指针来完成的。这意味着方法可以改变引用对象,并且调用代码可以看到这种改变的结果。
对于值类型实例的参数来说,传递给方法的将是值类型实例的一个拷贝。这意味着方法会得到一份属于它自己的值类型实例的成员。而调用方法的代码中的实例不会受到影响。
除了按值传递参数外,CLR还允许我们按引用的方式来传递参数.在C#中可以使用out和ref关键字来做到这一点。out和ref的区别是:
- 一个方法的参数被标识为out,那么调用代码在调用该方法之前可以不初始化该参数,并且调用方法不能直接读取参数的值,它必须在返回之前为该参数赋值。
- 一个方法的参数被表示为ref,那么调用代码在调用该方法前必须首先初始化该参数。被调用方法可以任意读取该参数、或者为该参数赋值。
out关键字示例:
![ContractedBlock.gif](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![ExpandedBlockStart.gif](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
{
static void Main( string [] args)
{
int x; // x不必初始化
SetVal( out x);
Console.WriteLine(x); // 显示为10
}
static void SetVal( out int v)
{
// int x = v; // 该行代码会报错
v = 10 ; // SetVal方法必须初始化v;
}
}
上面代码中,x首先被声明在线程堆栈上。接着,x的地址被传递给SetVal。SetVal的参数v是一个指向int值类型的指针。在SetVal内部,v指向的int被赋值为10.当SetVal返回后,Main中的x的值为10,在值类型参数上使用out会提高代码的效率,因为它避免l哦值类型的字段在方法调用时的拷贝操作。
ref关键字示例:
![ContractedBlock.gif](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![ExpandedBlockStart.gif](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
{
static void Main( string [] args)
{
int x = 5 ; // x必须初始化
AddVal( ref x);
Console.WriteLine(x); // 显示为15
}
static void AddVal( ref int v)
{
int x = v; // addVal方法可以直接使用经过初始化的v而不报错
v += 10 ;
}
}
上面代码中,x首先被声明在线程堆栈上,紧接着被初始化为5,随后x的地址被传递给AddVal,AddVal的参数是一个指向int值类型的指针。在AddVal内部,v指向的必须是一个结果初始化的值,这样AddVal才可以在任何表达式中使用该初始值。当AddVal返回后,Main中x的值将为15;
CLR允许根据out和ref参数来重载方法,下面代码合法:
{
static void Add(Point p) { }
static void Add( ref Point p) { }
}
还有:
{
static void Add(Point p) { }
static void Add( out Point p)
{
p = new Point(); // 必须给out参数赋值
}
}
但是,仅仅通过out和ref来区分重载方法是不合法的,下面代码编译它不过:
{
static void Add( ref Point p) { }
static void Add( out Point p)
{
p = new Point(); // 必须给out参数赋值
}
}
因为经过jit编译后打代码是相同的。
在值类型参数上使用out和ref关键字与用传值的方式来传递引用类型的参数在某种程度上具有相同的行为,前一种情况,out和ref关键字允许被调用方法直接操作一个值类型实例,调用代码必须为该实例分配内存,被调用方法操作该内存。对于后一种情况,调用代码为引用类型对象分配内存,而被调用方法通过传入的引用(指针)来操作对象。看下面代码:
![ContractedBlock.gif](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![ExpandedBlockStart.gif](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
{
static void Main( string [] args)
{
FileStream fs;
// 打开第一个文件
StartProcessingFiles( ref fs);
for (; fs != null ; ContinueProcessingFiles( ref fs))
{
// 处理文件
fs.Read();
}
}
static void StartProcessingFiles( ref FileStream fs)
{
fs = new FileStream();
}
static void ContinueProcessingFiles( ref FileStream fs)
{
fs.Close(); // 关闭上一次操作的文件
// 打开下一个文件,没有返回null
if (noFiles)
fs = null ;
else
{
fs = new FileStream();
}
}
}
该代码最大的不同在于有着out和ref修饰的引用类型参数的方法创建一个对象后,指向新对象的指针会返回到调用代码。
下面代码演示了使用ref来交换两个引用:
{
object c = a;
a = b;
b = c;
}
CLR为了确保类型安全,按引用传递的变量必须和方法声明的参数类型完全相同,如下面代码将会报错:
![ContractedBlock.gif](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![ExpandedBlockStart.gif](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
{
static void Main( string [] args)
{
string s1 = " heaiping " ;
string s2 = " hap " ;
Swap( ref s1, ref s2);
}
static public void Swap( ref object a, ref object b)
{
object c = a;
a = b;
b = c;
}
}
因为object和string不匹配,要编译同步代码必须修改为:
![ContractedBlock.gif](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![ExpandedBlockStart.gif](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
{
static void Main( string [] args)
{
string s1 = " heaiping " ;
string s2 = " hap " ;
Swap( ref s1, ref s2);
Console.WriteLine(s1);
Console.WriteLine(s2);
}
static public void Swap( ref string a, ref string b)
{
string c = a;
a = b;
b = c;
}
}
修正后编译通过并得到期望值。