通過一個例項重新認識引用型別,值型別,陣列,堆疊,ref

我才是銀古發表於2016-10-12

  昨天在寫程式碼時候遇到了一個問題,百思不得其解,感覺顛覆了自己對C#基礎知識的認知,因為具體的情境涉及公司程式碼不便放出,我在這裡舉個例子,先上整個測試所有的程式碼,然後一一講解我的思考過程:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Text;
 4 
 5 namespace ConsoleApplication1
 6 {
 7     class Program
 8     {
 9         static void Main(string[] args)
10         {
11             var ps = new Test[] {new Test() {Age = 1, Name = "1"}, new Test() {Age = 5, Name = "5"}};
12 
13             Console.WriteLine("原始陣列");
14             foreach (var m in ps)
15             {
16                 Console.WriteLine("Name="+m.Name+"Age="+m.Age);
17             }
18             Console.WriteLine("================================");
19 
20             Console.WriteLine(@"private static void Test1(Test t)
21         {
22             t = new Test() { Age = 4, Name = 4 };
23         }");
24             ps = new Test[] { new Test() { Age = 1, Name = "1" }, new Test() { Age = 5, Name = "5" } };
25             Test1(ps[0]);
26             foreach (var m in ps)
27             {
28                 Console.WriteLine("Name=" + m.Name + "Age=" + m.Age);
29             }
30             Console.WriteLine("================================");
31 
32             Console.WriteLine(@"private static void Test2(Test t)
33         {
34                 t.Name = 4;
35                 t.Age = 4;
36             }
37             ");
38             ps = new Test[] { new Test() { Age = 1, Name = "1" }, new Test() { Age = 5, Name = "5" } };
39             Test2(ps[0]);
40             foreach (var m in ps)
41             {
42                 Console.WriteLine("Name=" + m.Name + "Age=" + m.Age);
43             }
44             Console.WriteLine("================================");
45 
46             Console.WriteLine(@"private static void Test3(ref Test t)
47         {
48             t = new Test() { Age = 4, Name = 4 };
49         }
50             ");
51             ps = new Test[] { new Test() { Age = 1, Name = "1" }, new Test() { Age = 5, Name = "5" } };
52             Test3(ref ps[0]);
53             foreach (var m in ps)
54             {
55                 Console.WriteLine("Name=" + m.Name + "Age=" + m.Age);
56             }
57             Console.WriteLine("================================");
58 
59             Console.ReadKey();
60 
61         }
62 
63         class  Test
64         {
65             public string Name { get; set; }
66             public int Age { get; set; }
67         }
68 
69         private static void Test1(Test t)
70         {
71             t = new Test() { Age = 4, Name = "4" };
72         }
73 
74         private static void Test2(Test t)
75         {
76             t.Name = "4";
77             t.Age = 4;
78         }
79 
80         private static void Test3(ref Test t)
81         {
82             t = new Test() { Age = 4, Name = "4" };
83         }
84     }
85 }

  這個例子比較簡單,要實現的功能就是為物件陣列中的某一個元素賦值。

  我遇到的問題相當於Test1函式,將陣列的元素傳入Test1之後,判斷,如果不符合要求就new一個新的物件,於是,問題來了。除錯發現,新new的物件並沒有真的替換掉陣列中對應的元素,有違常理啊,一個引用型別引數傳入函式,函式中修改物件的值應該是會體現在源物件上的,為啥值沒變呢?

  其實,這個理解也沒錯,但是有個前提,就是不new一個新物件賦值給引數的情況下,如Test2函式的做法,這樣是會改變物件值的。

  為什麼會這樣呢?必須先承認自己的基礎知識太差了。

  我們知道,引用型別的引用(類似指標)是存放在棧地址中的,而它真是的值是存放在堆地址中的,值型別沒有引用,它的值直接存放在棧地址中。Test2函式之所以能改變主函式中陣列元素的值是因為形參t傳入了陣列元素的引用,這個引用指向它對應的值的地址,直接修改t的值,其實也是在直接修改陣列元素的值,形參t只傳遞了引用,而值還是與陣列元素的共用一個的。但在Test1函式中就不一樣了,t作為形參傳入了陣列元素的引用,在函式中又重新new了一個物件,這就意味著,t所代表的引用已經從原來的陣列元素變為了新物件的引用,對t的值進行修改只會影響新物件,而與陣列元素毫無關係了,所以陣列元素經過Test2函式後值是不變的。

  既然存在這個問題,但是函式又不可能大改,畢竟牽一髮而帶動全身,那怎麼辦呢?

  很簡單為形參t加一個ref修飾,於是就成了Test3函式,Test3函式可以做到就算new一個新物件,也會改變陣列元素的值。

  這是為什麼呢?要搞清這個我們必須重新理解一下ref。

  看到我這個使用方式,很多人第一反應是ref不是給值型別用的,給引用型別用ref是幾個意思?其實不然,ref也可以給引用型別用,而且是有意義的。ref的本質是直接傳遞棧地址,值型別的值本身就放在棧地址中,所以ref對值型別起作用。對於引用型別,我們之前提到了,引用型別的引用(類似指標)是存放在棧地址中的,而它真是的值是存放在堆地址中的,在函式中,形參t傳遞的其實只是陣列元素的引用,也就是引用型別的棧地址部分,如果對引用型別使用ref就意味著,不管你在函式裡面是修改引用型別的值,還是引用,它都直接返回t當前的引用,而引用型別又是通過引用找到值,於是,就算你new一個新的物件,主函式中的陣列元素的值也會跟著改變,因為陣列元素的引用因為ref的存在而改變了。

  有些基礎知識雖然枯燥,但是一旦遇到了就會知道它的重要性,還是需要好好學習啊!

  最後,感謝深藍醫生在這個過程中提供的幫助,還有SOD框架高階群(18215717)裡的大家提供的幫助,謝謝大家!

相關文章