好,现在开始吧。
我们知道,在C#中,类型有值类型(例如int)和引用类型(例如string)之分,传递参数有按值传递和按引用传递之分。这样,简单的组合一下,我们可以得到以下几种传递方式:(1)按值传递值类型。(2)按值传递引用类型。(3)按引用传递值类型。(4)按引用传递引用类型。一般来说,除非使用特定的关键字(ref和out)否则参数是按值传递的。也就是说,会传递一个副本。传递副本的一个好处是,可以避免误操作而影响了原始值。原因是在被调用的函数体内,操作的是副本的值,而不是原始值。当然,传递副本也是有副作用的,最为突出的应该是由于复制而产生的性能损耗,这点在大型的值类型身上尤为突出。那么C#的编译器的默认行为为什么不是使用按引用传递参数呢?呵呵,其实我没仔细深入思考过这个问题。我猜测,是因为安全因素吧,就是怕函数误操作了原始值。这点应该和C#的编译器要求显示使用关键字(ref和out)差不多,都是为了清楚地表达使用的意图,以避免误操作。使用ref等关键字,暗示函数调用者知道,在函数体内,也许存在修改原始值的语句,会改变参数的值(或者叫状态)。
用个简单的示例演示一下。
示例代码如下所示:
示例代码
1using System;
2
3namespace DonLiang
4{
5 class Sample
6 {
7 值类型测试函数#region 值类型测试函数
8 public static void foo(int x)
9 {
10 x = 10;
11 }
12
13 //*
14 public static void foo(ref int x)
15 {
16 x = 10;
17 }
18 //*/
19
20 /**//*
21 public static void foo(out int x)
22 {
23 x = 10;
24 }
25 //*/
26 #endregion
27
28 辅助引用类型#region 辅助引用类型
29 public class Point
30 {
31 private int m_x;
32 private int m_y;
33
34 public Point()
35 {
36 m_x = 0;
37 m_y = 0;
38 }
39
40 public Point(int x, int y)
41 {
42 m_x = x;
43 m_y = y;
44 }
45
46 public void Change(int x, int y)
47 {
48 m_x = x;
49 m_y = y;
50 }
51
52 public override string ToString()
53 {
54 return string.Format("The Point is ({0},{1})", m_x.ToString(), m_y.ToString());
55 }
56 }
57 #endregion
58
59 引用类型测试函数#region 引用类型测试函数
60 public static void foo(Point p)
61 {
62 p.Change(10, 10);
63 }
64
65 public static void foo(ref Point p)
66 {
67 p.Change(100, 100);
68 }
69
70 public static void other(Point p)
71 {
72 Point tmp = new Point(13, 16);
73 p = tmp;
74 }
75
76 public static void other(ref Point p)
77 {
78 Point tmp = new Point(138, 168);
79 p = tmp;
80 }
81 #endregion
82
83 Main#region Main
84 static void Main(string[] args)
85 {
86 int n = 5;
87
88 //call the foo(int x) method and check what happened.
89 Console.WriteLine("before call foo(int x) the n = " + n.ToString());
90 foo(n);
91 Console.WriteLine("after call foo(int x) the n = " + n.ToString());
92
93 Console.WriteLine("--------------------------------------------------------------");
94
95 //call the foo(ref int x) method and check what happened.
96 Console.WriteLine("before call foo(ref int x) the n = " + n.ToString());
97 foo(ref n);
98 //foo(out n);
99 Console.WriteLine("after call foo(ref int x) the n = " + n.ToString());
100
101 Console.WriteLine("--------------------------------------------------------------");
102
103 Point p = new Point(5, 5);
104 Point q = p;
105
106 //call the foo(Point p) method and check what happened.
107 Console.WriteLine("before call foo(Point p) the p = " + p.ToString());
108 foo(p);
109 Console.WriteLine("after call foo(Point p) the p = " + p.ToString());
110 Console.WriteLine("q = " + q.ToString());
111
112 Console.WriteLine("--------------------------------------------------------------");
113
114 //call the foo(ref Point p) method and check what happened.
115 Console.WriteLine("before call foo(ref Point p) the n = " + p.ToString());
116 foo(ref p);
117 Console.WriteLine("after call foo(ref Point p) the n = " + p.ToString());
118 Console.WriteLine("q = " + q.ToString());
119
120 Console.WriteLine("--------------------------------------------------------------");
121
122 //call the other(Point p) method and check what happened.
123 Console.WriteLine("before call other(Point p) the n = " + p.ToString());
124 other(p);
125 Console.WriteLine("after call other(Point p) the n = " + p.ToString());
126 Console.WriteLine("q = " + q.ToString());
127
128 Console.WriteLine("--------------------------------------------------------------");
129
130 //call the other(ref Point p) method and check what happened.
131 Console.WriteLine("before call other(ref Point p) the n = " + p.ToString());
132 other(ref p);
133 Console.WriteLine("after call other(ref Point p) the n = " + p.ToString());
134 Console.WriteLine("q = " + q.ToString());
135
136 Console.ReadLine();
137 }
138 #endregion
139 }
140}
1using System;
2
3namespace DonLiang
4{
5 class Sample
6 {
7 值类型测试函数#region 值类型测试函数
8 public static void foo(int x)
9 {
10 x = 10;
11 }
12
13 //*
14 public static void foo(ref int x)
15 {
16 x = 10;
17 }
18 //*/
19
20 /**//*
21 public static void foo(out int x)
22 {
23 x = 10;
24 }
25 //*/
26 #endregion
27
28 辅助引用类型#region 辅助引用类型
29 public class Point
30 {
31 private int m_x;
32 private int m_y;
33
34 public Point()
35 {
36 m_x = 0;
37 m_y = 0;
38 }
39
40 public Point(int x, int y)
41 {
42 m_x = x;
43 m_y = y;
44 }
45
46 public void Change(int x, int y)
47 {
48 m_x = x;
49 m_y = y;
50 }
51
52 public override string ToString()
53 {
54 return string.Format("The Point is ({0},{1})", m_x.ToString(), m_y.ToString());
55 }
56 }
57 #endregion
58
59 引用类型测试函数#region 引用类型测试函数
60 public static void foo(Point p)
61 {
62 p.Change(10, 10);
63 }
64
65 public static void foo(ref Point p)
66 {
67 p.Change(100, 100);
68 }
69
70 public static void other(Point p)
71 {
72 Point tmp = new Point(13, 16);
73 p = tmp;
74 }
75
76 public static void other(ref Point p)
77 {
78 Point tmp = new Point(138, 168);
79 p = tmp;
80 }
81 #endregion
82
83 Main#region Main
84 static void Main(string[] args)
85 {
86 int n = 5;
87
88 //call the foo(int x) method and check what happened.
89 Console.WriteLine("before call foo(int x) the n = " + n.ToString());
90 foo(n);
91 Console.WriteLine("after call foo(int x) the n = " + n.ToString());
92
93 Console.WriteLine("--------------------------------------------------------------");
94
95 //call the foo(ref int x) method and check what happened.
96 Console.WriteLine("before call foo(ref int x) the n = " + n.ToString());
97 foo(ref n);
98 //foo(out n);
99 Console.WriteLine("after call foo(ref int x) the n = " + n.ToString());
100
101 Console.WriteLine("--------------------------------------------------------------");
102
103 Point p = new Point(5, 5);
104 Point q = p;
105
106 //call the foo(Point p) method and check what happened.
107 Console.WriteLine("before call foo(Point p) the p = " + p.ToString());
108 foo(p);
109 Console.WriteLine("after call foo(Point p) the p = " + p.ToString());
110 Console.WriteLine("q = " + q.ToString());
111
112 Console.WriteLine("--------------------------------------------------------------");
113
114 //call the foo(ref Point p) method and check what happened.
115 Console.WriteLine("before call foo(ref Point p) the n = " + p.ToString());
116 foo(ref p);
117 Console.WriteLine("after call foo(ref Point p) the n = " + p.ToString());
118 Console.WriteLine("q = " + q.ToString());
119
120 Console.WriteLine("--------------------------------------------------------------");
121
122 //call the other(Point p) method and check what happened.
123 Console.WriteLine("before call other(Point p) the n = " + p.ToString());
124 other(p);
125 Console.WriteLine("after call other(Point p) the n = " + p.ToString());
126 Console.WriteLine("q = " + q.ToString());
127
128 Console.WriteLine("--------------------------------------------------------------");
129
130 //call the other(ref Point p) method and check what happened.
131 Console.WriteLine("before call other(ref Point p) the n = " + p.ToString());
132 other(ref p);
133 Console.WriteLine("after call other(ref Point p) the n = " + p.ToString());
134 Console.WriteLine("q = " + q.ToString());
135
136 Console.ReadLine();
137 }
138 #endregion
139 }
140}
程序的运行结果也是显而易见的,如下图所示:
接下来,简单分析一下这个结果:
(1)按值传递值类型
初始值为5,调用函数的时候,弄了个副本给函数折腾,于是,从函数返回后,值还是5。嗯,本来应该弄个堆栈图出来的,可是,图是在太难画,因此,我偷懒,用VS2008的调试监视器看看:
(2)按引用传递值类型
初始值还是5,这次换了个传递参数的方式——按引用传递,这次可不是副本了,而是原始值(的地址),这就类似大牌的武打演员总不能老是使用替身一样,偶尔还是要亲自上阵的。既然是亲自上阵,那么,值被修改为10就不足为奇了。正如结果图所示,n=10了。
(3)按值传递引用类型 和 按引用传递引用类型
之所以把这两个放在一起讲,是因为,如结果图所示,两种传递方式,都成功修改了值——这两个函数都分别调用了一个辅助修改的函数Change,去修改内部状态,即m_x,m_y的值,从5到10。呃,竟然都可以成功修改原始值,那么,为什么会存在两种方式呢?它们有什么区别吗?分别用在什么地方?为了说明他们的区别,我特意写了两个名为other的函数,在函数内new一个Point对象,并使从参数传递过来的引用这个新生成的Point对象。值得提醒的是,这个引用其定义在函数体外。其运行如上图我用方框框起来那个。
可以很清楚地看到,通过值传递方式,可以改变其值,却不能改变其本身所引用的对象;而按引用传递方式可以。
顺便提一下,代码中,有一段注释掉的代码,使用out关键字的。当你尝试将其两者一起写着,然后,编译,C#编译器是会提示错误的( error CS0663: 'foo' cannot define overloaded methods that differ only on ref and out)。其原因是,C#编译器,对ref和out生成的IL代码,是相同的;而在CLR层面,是没有ref和out的区别的。C#中,ref和out的区别,主要是,谁负责初始化这个参数使之能用——ref形式是函数外初始化,而out是函数内初始化。
希望本文对你有所帮助。