原文鏈接:https://www.c-sharpcorner.com/article/C-Sharp-heaping-vs-stacking-in-net-part-ii/
前言
雖然使用.NET Framework編程時我們不必主動的關注內存管理和垃圾回收,但是爲了更優的程序性能,我們還是應該對內存管理和GC有一定的瞭解。而且,從根本上理解了內存管理的工作方式也可以幫助我們理解程序中的每一個變量的行爲。在本篇中,將針對方法參數的行爲具體展開。
在第一節中我們就堆棧的工作方式有了基本的瞭解,以及值類型和引用類型在程序運行時的分配方式。我們還介紹了指針的基本概念。
看重點:參數
在第一節中我們已經介紹了方法在運行時的基本情況,現在我們就來看更詳細的內容。
當我們在執行一個方法時,發生了下面這些事:
- 在方法執行的時候,需要在棧中分配空間。其中包括一個相當於goto命令的指針,用來確保當我們的方法在線程中執行完畢以後,知道該回到哪裏以繼續執行。
- 方法的參數被複制了。這是我們需要注意的地方。
- 完成JIT編譯以後線程開始執行代碼。之後再去執行下一個方法。
看下面的代碼:
public int AddFive(int pValue)
{
int result;
result = pValue + 5;
return result;
}
棧看起來大概是這樣的:
注意:方法是不在棧中的,圖中的方法名只是爲了表示從這裏開始的。
在第一節的討論中,不論參數是值類型還是引用類型,都會被分配到棧中。值類型會生成一份拷貝,引用類型會生成一份引用的拷貝。
值類型的傳遞
先來看值類型...
首先,當我們在傳遞一個值類型的時候,會在棧中新分配一個空間用來存儲該類型的值。看接下來的這個栗子:
class Class1
{
public void Go()
{
int x = 5;
AddFive(x);
Console.WriteLine(x.ToString());
}
public int AddFive(int pValue)
{
pValue += 5;
return pValue;
}
}
當方法Go()被執行的時候,會在棧中爲x分配一個空間,存儲的值是5。
然後,AddFive()被壓入棧中,並且爲它的參數分配空間,存儲的是從x拷貝的值。
當AddFive()執行結束以後,線程繼續執行Go()方法,因爲AddFive()已經結束了,所以pValue實際上就已經被移除了。
你覺得輸出的是5嗎?看黑板,講重點:當參數傳遞給方法時,會生成一個一毛一樣的副本,原始的值會被保留着。
還有一件事需要注意:如果我們有一個很大的值類型,比如一個龐大的struct被傳遞到了棧中,無論分配內存還是其他處理,每一次都會帶來巨大的開銷。棧的可用空間是有限的,就像是往玻璃瓶裏裝水,裝多了是會溢出的。struct是一個可以很龐大的值類型,所以我們必須要懂得該怎樣處理它。
給你展示一個很大的結構體:
public struct MyStruct
{
long a, b, c, d, e, f, g, h, i, j, k, l, m;
}
看一看當在執行Go()方法的時候,都發生了什麼:
public void Go()
{
MyStruct x = new MyStruct();
DoSomething(x);
}
public void DoSomething(MyStruct pValue)
{
// DO SOMETHING HERE....
}
這樣的效率真的很低。想象一下我們把MyStruct傳遞了幾千次,你就應該能明白,這是一件多麼糟糕的事情了。
我們該怎樣避開這個問題呢?那就要通過引用傳遞來實現,如下:
public void Go()
{
MyStruct x = new MyStruct();
DoSomething(ref x);
}
public struct MyStruct
{
long a, b, c, d, e, f, g, h, i, j, k, l, m;
}
public void DoSomething(ref MyStruct pValue)
{
// DO SOMETHING HERE....
}
這樣在爲對象分配內存時效率就高多了。
這時候我們唯一需要注意的是,當我們通過引用來傳遞值類型時,我們就可以直接修改這個值類型的值了。
對pValue的修改就是對x的修改。通過下面的代碼,我們得到的結果是”12345“,因爲pValue.a指向的內存地址,就是變量x的地址。
public void Go()
{
MyStruct x = new MyStruct();
x.a = 5;
DoSomething(ref x);
Console.WriteLine(x.a.ToString());
}
public void DoSomething(ref MyStruct pValue)
{
pValue.a = 12345;
}
引用類型的傳遞
引用類型的傳遞和之前栗子中的通過引用傳遞值類型是相似的。
繼續看栗子
public class MyInt
{
public int MyValue;
}
在訪問Go()方法時,MyInt()被分配在堆(heap)上,因爲他是引用類型的:
public void Go()
{
MyInt x = new MyInt();
}
當我們在執行下面栗子中的Go方法時...
public void Go()
{
MyInt x = new MyInt();
x.MyValue = 2;
DoSomething(x);
Console.WriteLine(x.MyValue.ToString());
}
public void DoSomething(MyInt pValue)
{
pValue.MyValue = 12345;
}
看看發生了什麼...
- 在執行Go()方法的時候,首先爲變量x在棧上分配內存。
- 執行DoSomething()時,爲參數pValue在棧上分配內存。
- x的值(MyInt的地址)被拷貝到pValue
所以,當我們通過pValue來改變MyInt對象的MyValue變量的值時,和通過x來做修改是一樣的。我們將得到“12345”。
現在來看點更有趣的東東。當我們通過應用來傳遞一個引用類型時,會發生什麼呢?
讓我們嘗試一下。假設我們有一個Thing類,Animal類和Vegetable類都是繼承自Thing類:
public class Thing
{
}
public class Animal:Thing
{
public int Weight;
}
public class Vegetable:Thing
{
public int Length;
}
然後我們執行下面的Go()方法:
public void Go()
{
Thing x = new Animal();
Switcharoo(ref x);
Console.WriteLine(
"x is Animal : "
+ (x is Animal).ToString());
Console.WriteLine(
"x is Vegetable : "
+ (x is Vegetable).ToString());
}
public void Switcharoo(ref Thing pValue)
{
pValue = new Vegetable();
}
變量x編程了Vegetable。輸出結果:
x is Animal : False
x is Vegetable : True
再來看看都發生了什麼:
再來看看都發生了什麼:
- Go()方法開始執行的時候,指針x被分配在棧中。
- Animal被分配在堆中。
- 在執行Switcharoo()方法的時候,pValue被分配在棧中,並且指向x。
- Vegetable被分配在堆中。
- 通過pValue修改指針x指向的地址爲Vegetable。
如果我們不是通過引用來傳遞Thing,指針x將繼續指向Animal,結果就不是這樣了。
如果上面的代碼你理解不了,去前面的章節看一下關於變量引用的討論,這會對更好的理解引用類型的工作方式。
寫在最後
我們已經知道了傳遞參數時如何在內存上分配空間並且也知道了該注意些什麼。在接下來的一篇裏,我們將會關注棧中引用變量發生了什麼變化,以及如何克服在複製對象時遇到的問題。