一、引言

  对于一些初学者(包括工作几年的人在内)来说,有时候对于方法之间的参数传递的问题感觉比较困惑的,因为之前在面试的过程也经常遇到参数传递的基础面试题,这样的面试题主要考察的开发人员基础是否扎实,对于C#中值类型和引用类型有没有深入的一个理解——这个说的理解并不是简单的对它们简单一个定义描述,而在于它们在内存中分布。所以本文章将带领大家深入剖析下C#中参数传递的问题,并分享我自己的一个理解,只有你深入理解了才能在不运行程序的情况就可以分析出参数传递的结果的。

二、按值传递

对于C#中的参数传递,根据参数的类型可以分为四类:

  • 值类型参数的按值传递

  • 引用类型参数的按值传递

  • 值类型参数的按引用传递

  • 引用类型参数的按引用传递

然而在默认情况下,CLR方法中参数的传递都是按值传递的。为了帮助大家全面理解参数的传递,下面就这四种情况一一进行分析。

2.1 值类型参数的按值传递

对于参数又分为:形参和实参,形参指的是被调用方法中的参数,实参指的是调用方法的参数,下面结合代码帮助大家理解形参和实参的概念:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class 
Program
    
{
        
static 
void 
Main(
string
[] args)
        
{
                                            
            
int 
addNum = 1;
            
// addNum 就是实参,
            
Add(addNum);
         
}
        
// addnum就是形参,也就是被调用方法中的参数
        
private 
static 
void 
Add(
int 
addnum)
        
{
            
addnum = addnum + 1;
            
Console.WriteLine(addnum);
        
}
}

对于值类型的按值传递,传递的是该值类型实例的一个拷贝,也就是形参此时接受到的是实参的一个副本,被调用方法操作是实参的一个拷贝,所以此时并不影响原来调用方法中的参数值,为了证明这点,看看下面的代码和运行结果就明白了:

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)
        
{
            
// 1. 值类型按值传递情况
            
Console.WriteLine(
"按值传递的情况"
);
            
int 
addNum = 1;
            
Add(addNum);
            
Console.WriteLine(addNum); 
                                     
            
Console.Read();
        
}
        
// 1. 值类型按值传递情况
        
private 
static 
void 
Add(
int 
addnum)
        
{
            
addnum = addnum + 1;
            
Console.WriteLine(addnum);
        
}

运行结果为:

11205654-c77e22d8398542c084fd517c6446c7c

从结果中可以看出addNum调用方法之后它的值并没有改变,Add 方法的调用只是改变了addNum的副本addnum的值,所以addnum的值修改为2了。然而我们的分析到这里并没有结束,为了让大家深入理解传递传递,我们有必要知道为什么值类型参数的按值传递不会修改实参的值,相信下面这张图可以解释你所有的疑惑:

11211025-526b78af59fe4cf69011b2bddc4762b


2.2 引用类型参数的按值传递

当传递的参数是引用类型的时候,传递和操作的是指向对象的引用(看到这里,有些朋友会觉得此时不是传递引用吗?怎么还是按值传递了?对于这个疑惑,此时确实是按值传递,此时传递的对象的地址,传递地址本身也是传递这个地址的值,所以此时仍然是按值传递的),此时方法的操作就会改变原来的对象。对于这点可能看文字描述会比较难理解下面结合代码和分析图来帮助大家理解下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class 
Program
    
{
        
static 
void 
Main(
string
[] args)
        
{
            
// 2. 引用类型按值传递情况
            
RefClass refClass = 
new 
RefClass();
            
AddRef(refClass);
            
Console.WriteLine(refClass.addnum);
        
}  
         
// 2. 引用类型按值传递情况
        
private 
static 
void 
AddRef(RefClass addnumRef)
        
{
            
addnumRef.addnum += 1;
            
Console.WriteLine(addnumRef.addnum);
        
}
}
class 
RefClass
    
{
        
public 
int 
addnum=1;
    
}

运行结果为:

11212300-fdf54fa1da19484b8660eb1e9b3fef1

为什么此时传递引用就会修改原来实参中的值呢?对于这点我们还是参数在内存中分布图来解释下:

11213929-f8802f7821b04a11b67363b7ad566b2

2.3 .String引用类型的按值传递的特殊情况

对于String类型同样是引用类型,然而对于string类型的按值传递时,此时引用类型的按值传递却不会修改实参的值,可能很多朋友对于这点很困惑,下面具体看看下面的代码:

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)
        
{
              
// 3. String引用类型的按值传递的特殊情况
            
string 
str = 
"old string"
;
            
ChangeStr(str);
            
Console.WriteLine(str);
                               
        
}
                           
         
// 3. String引用类型的按值传递的特殊情况
        
private 
static 
void 
ChangeStr(
string 
oldStr)
        
{
            
oldStr = 
"New string"
;
            
Console.WriteLine(oldStr);
        
}
}

运行结果为:

11220359-33564f962e7d4b9aa43602ca3d88f82

对于为什么原来的值没有被改变主要是因为string的“不变性”,所以在被调用方法中执行 oldStr="New string"代码时,此时并不会直接修改oldStr中的"old string"值为"New string",因为string类型是不变的,不可修改的,此时内存会重新分配一块内存,然后把这块内存中的值修改为 “New string”,然后把内存中地址赋值给oldStr变量,所以此时str仍然指向 "old string"字符,而oldStr却改变了指向,它最后指向了 "New string"字符串。所以运行结果才会像上面这样,下面内存分布图可以帮助你更形象地理解文字表述:

11221632-08b90aa7a4614697b1ebf4029708dd5

三、按引用传递

不管是值类型还是引用类型,我们都可以使用ref 或out关键字来实现参数的按引用传递,然而按引用进行传递的时候,需要注意下面两点:

方法的定义和方法调用都必须同时显式使用ref或out,否则会出现编译错误

CLR允许通过out 或ref参数来实现方法重载。如:

1
2
3
4
5
6
7
8
9
10
11
#region CLR 允许out或ref参数来实现方法重载
        
private 
static 
void 
Add(
string 
str)
        
{
            
Console.WriteLine(str);
        
}
        
// 编译器会认为下面的方法是另一个方法,从而实现方法重载
        
private 
static 
void 
Add(
ref 
string 
str)
        
{
            
Console.WriteLine(str);
        
}
        
#endregion

按引用传递可以解决由于值传递时改变引用副本而不影响引用本身的问题,此时传递的是引用的引用(也就是地址的地址),而不是引用的拷贝(副本)。下面就具体看看按引用传递的代码:

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
class 
Program
    
{
        
static 
void 
Main(
string
[] args)
        
{
            
#region 按引用传递
            
Console.WriteLine(
"按引用传递的情况"
);
            
int 
num = 1;
            
string 
refStr = 
"Old string"
;
            
ChangeByValue(
ref 
num);
            
Console.WriteLine(num);
            
changeByRef(
ref 
refStr);
            
Console.WriteLine(refStr);
            
#endregion
            
Console.Read();
        
}
        
#region 按引用传递
        
// 1. 值类型的按引用传递情况
        
private 
static 
void 
ChangeByValue(
ref 
int 
numValue)
        
{
            
numValue = 10;
            
Console.WriteLine(numValue);
        
}
        
// 2. 引用类型的按引用传递情况
        
private 
static 
void 
changeByRef(
ref 
string 
numRef)
        
{
            
numRef = 
"new string"
;
            
Console.WriteLine(numRef);
        
}
        
#endregion
}

运行结果为:

11222853-e9fc447c359440dc847ba14a701748f

从运行结果可以看出,此时引用本身的值也被改变了,通过下面一张图来帮忙大家理解下按引用传递的方式:

11232410-02587071b4dc4498ae8f5958b922918

四、总结

到这里参数的传递所有内容就介绍完了。总之,对于按值传递,不管是值类型还是引用类型的按值传递,都是传递实参的一个拷贝,只是值类型时,此时传递的是实参实例的一个拷贝(也就是值类型值的一个拷贝),而引用类型时,此时传递的实参引用的副本。对于按引用传递,传递的都是参数地址,也就是实例的指针