最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

一看就懂:圖解C#中的值類型、引用類型、棧、堆、ref、out

 更新時(shí)間:2015年06月01日 09:28:40   投稿:junjie  
這篇文章主要介紹了一看就懂:圖解C#中的值類型、引用類型、棧、堆、ref、out,本文用淺顯易懂的語言組織介紹了這些容易混淆的概念,需要的朋友可以參考下

C# 的類型系統(tǒng)可分為兩種類型,一是值類型,一是引用類型,這個(gè)每個(gè)C#程序員都了解。還有托管堆,棧,ref,out等等概念也是每個(gè)C#程序員都會(huì)接觸到的概念,也是C#程序員面試經(jīng)常考到的知識(shí),隨便搜搜也有無數(shù)的文章講解相關(guān)的概念,貌似沒寫一篇值類型,引用類型相關(guān)博客的不是好的C#程序員。我也湊個(gè)熱鬧,試圖徹底講明白相關(guān)的概念。

程序執(zhí)行的原理

要徹底搞明白那一堆概念及其它們之間的關(guān)系似乎并不是一件容易的事,這是因?yàn)榇蟛糠諧#程序員并不了解托管堆(簡(jiǎn)稱“堆”)和線程棧(簡(jiǎn)稱“?!保?,或者知道它們,但了解得并不深入,只知道:引用類型保存在托管堆里,而值類型“通?!北4嬖跅@铩R忝靼啄且欢迅拍畹年P(guān)系,我認(rèn)為先要明白程序執(zhí)行的基本原理,從而理解棧和托管堆的作用,才能理清它們的關(guān)系??紤]下面代碼,Main調(diào)用Method1,Method1調(diào)用Method2:

復(fù)制代碼 代碼如下:

class Program
{
    static void Main(string[] args)
    {
        var num = 120;
        Method1(num);
    }
 
    static void Method1(int num)
    {
        var num2 = num + 250;
        Method2(num2);
        Console.WriteLine(num);
    }
 
    static void Method2(int i)
    {
        Console.WriteLine(i);
    }
}

大家都知道Windows程序通常是多個(gè)線程的,這里不考慮多線程的問題。程序由Main方法進(jìn)入開始執(zhí)行,這時(shí)這個(gè)(主)線程會(huì)分配得到一個(gè)1M大小的只屬于它自己的線程棧。這1M的的??臻g用于向方法傳遞參數(shù),定義局部變量。所以在Main方法進(jìn)入Method1前,大家心理面要有一個(gè)”內(nèi)存圖“:把num壓入線程棧,如下圖:

接著把num作為參數(shù)傳入Method1方法,同樣在Method1內(nèi)定義一個(gè)局部變量num2,調(diào)用加方法得到最后的值,所以在進(jìn)入Method2前,“內(nèi)存圖”如下,num是參數(shù),num2是局部變量

接著調(diào)用Method2的過程雷同,然后退出Method2方法,回到上圖的樣子,再退出Method1方法,再回到第一副圖的樣子,然后退出程序,整個(gè)過程如下圖:

所以去除那些if,for,多線程等等概念,只保留對(duì)象內(nèi)存分配相關(guān)概念的話,程序的執(zhí)行可以簡(jiǎn)單總結(jié)為如下:

程序由Main方法進(jìn)入執(zhí)行,并不斷重復(fù)著“定義局部變量,調(diào)用方法(可能會(huì)傳參),從方法返回”,最后從Main方法退出。在程序執(zhí)行過程中,不斷壓入?yún)?shù)和局部變量到線程棧里,也不斷的出棧。

注意,其實(shí)壓入棧的還有方法的返回地址等,這里忽略了。

引用類型和堆

上面的例子我只用了一種簡(jiǎn)單的int值類型,目的是為了只關(guān)注線程棧的壓棧(生長(zhǎng))和出棧(消亡)。很明顯C#還有種引用類型,引入引用類型,再考慮上面的問題,看下面代碼:

復(fù)制代碼 代碼如下:

static void Main(string[] args)
{
    var user = new User { Age = 15 };
    var num = 23;
    Console.WriteLine(user.Age);
    Console.WriteLine(num);
}
 
class User
{
    public int Age;
}

我想很多人都應(yīng)該知道,這時(shí)應(yīng)該引入托管堆的概念了,但這里我想跟上面一樣,先從棧的角度去考慮問題,所以在調(diào)用WriteLine前,“內(nèi)存圖”應(yīng)該是這樣的(地址是亂寫的):

這也就是人們常說的:對(duì)于引用類型,棧里保存的是指向在堆里的實(shí)例對(duì)象的地址(指針,引用)。既然只是個(gè)地址,那么要獲取一個(gè)對(duì)象的實(shí)例應(yīng)該有一個(gè)根據(jù)地址或?qū)ふ覍?duì)象的步驟,而事實(shí)正是這樣,如果Console.WriteLine(num),這樣獲取棧里的num的值給WriteLine方法算一步的話,要獲取上面user的實(shí)例對(duì)象,在運(yùn)行時(shí)是要分兩步的,也就是多了根據(jù)地址去尋找托管堆里實(shí)例對(duì)象的字段或方法的步驟。IL反編譯上面的Main方法,刪去一些無關(guān)代碼后:

復(fù)制代碼 代碼如下:

//load local 0=>獲取局部變量0(是一個(gè)地址)
IL_0012:  ldloc.0
// load field => 將指定對(duì)象中字段的值推送到堆棧上。
IL_0013:  ldfld      int32 CILDemo.Program/User::Age
IL_0018:  call       void [mscorlib]System.Console::WriteLine(int32)

復(fù)制代碼 代碼如下:

//load local 1=>獲取局部變量1(是一個(gè)值)
IL_001e:  ldloc.1
IL_001f:  call       void [mscorlib]System.Console::WriteLine(int32)

第二個(gè)WriteLine方法前,只需要一個(gè)ldloc.1(load local 1)讀取局部變量1指令即可獲取值給WriteLine,而第一個(gè)WriteLine前需要兩條指令完成這個(gè)任務(wù),就是上面說的分兩步。

當(dāng)然,大家都知道對(duì)我們來說,這是透明的,所以很多人喜歡畫這樣的圖去幫助理解,畢竟,我們是感覺不到那個(gè)0x0612ecb4地址存在的。

也有一種說法就是,引用類型分兩段存儲(chǔ),一是在托管堆里的值(實(shí)例對(duì)象),二是持有它的引用的變量。對(duì)于局部變量(參數(shù))來說,這個(gè)引用就在棧里,而作為類型的字段變量的話,引用會(huì)跟隨這個(gè)對(duì)象。

字段和局部變量(參數(shù))

上面圖的托管堆,大家應(yīng)該看到,作為值類型的Age的值是保存在托管堆里的,并不是保存在棧里,這也是很多C#新手所犯的錯(cuò)誤:值類型的值都是保存在棧里。

很明顯他們不知道這個(gè)結(jié)論是在我們上面討論程序運(yùn)行原理時(shí),局部變量(參數(shù))壓棧和出棧時(shí)這個(gè)特定的場(chǎng)景下的結(jié)論。我們要搞清楚,就像上面代碼一樣,除了可以定義int類型的num這個(gè)局部變量存儲(chǔ)23這個(gè)值外,我們還可以在一個(gè)類型里定義一個(gè)int類型Age字段成員來存儲(chǔ)一個(gè)整形數(shù)字,這時(shí)這個(gè)Age很明顯不是儲(chǔ)存在棧,所以結(jié)論應(yīng)該是:值類型的值是在它聲明的位置存儲(chǔ)的。即局部變量(參數(shù))的值會(huì)在棧里,作為類型成員的話,會(huì)跟隨對(duì)象。

當(dāng)然,引用類型的值(實(shí)例對(duì)象)總是在托管堆里,這個(gè)結(jié)論是正確的。

ref和out

C#有值類型和引用類型的區(qū)別,再有傳參時(shí)有ref和out這兩個(gè)關(guān)鍵字使得人們對(duì)相關(guān)概念的理解更加模糊。要理解這個(gè)問題,還是要從棧的角度去理解。我們分四種情況討論:正常傳遞值類型,正常傳遞引用類型,ref(out)傳遞值類型,ref(out)傳遞引用類型。

注意,對(duì)于運(yùn)行時(shí)來說,ref和out是一樣,它們的區(qū)別是C#編譯器對(duì)它們的區(qū)別,ref要求初始化好,out沒有要求。因?yàn)閛ut沒有要求初始化,所以被調(diào)用的方法不能讀取out參數(shù),且方法返回前必須賦值。

正常傳遞值類型

復(fù)制代碼 代碼如下:

static void Main(string[] args)
{
    var num = 120;
    Method1(num);
    Console.WriteLine(num);//輸出=>120
}
 
static void Method1(int num)
{
    Console.WriteLine(num);
    num = 180;
}

這種場(chǎng)景大家都熟悉,Method1的那句賦值是不起作用的,如果要畫圖的話,也跟上面第二幅圖類似:

也就是說傳參是把棧里的值復(fù)制到Method1的num參數(shù),Method1操作的是自己的參數(shù),對(duì)Main的局部變量完全沒有影響,即影響不到屬于Main方法的棧里的數(shù)據(jù)。

正常傳遞引用類型

復(fù)制代碼 代碼如下:

static void Main(string[] args)
{
    var user = new User();
    user.Age = 15;
    Method2(user);
    Debug.Assert(user != null);
    Console.WriteLine(user.Age);//輸出=> 18
}
 
static void Method2(User user)
{
    user.Age = 18;
    user = null;
}

留意這里的Method2的代碼,把Age設(shè)為18,影響到了Main方法的user,而把user設(shè)為null卻沒有影響。要分析這個(gè)問題,還是要先從棧的角度去看,棧圖如下(地址亂寫):

看到第二幅圖,大家應(yīng)該大概明白了這個(gè)事實(shí):無論值類型也好,引用類型也好,正常傳參都是把棧里的值復(fù)制給參數(shù),從棧的角度看的話,C#默認(rèn)是按值傳參的。

既然都是“按值傳參”,那么引用類型為什么表現(xiàn)出可以影響到調(diào)用方法的局部變量這個(gè)跟值類型不同的表現(xiàn)呢?仔細(xì)想想也不難發(fā)現(xiàn),這個(gè)不同的表現(xiàn)不是由傳參方式不同引起的,而是值類型和引用類型的局部變量(參數(shù))在內(nèi)存的存儲(chǔ)不同引起的。對(duì)于Main方法的局部變量user和Method2的參數(shù)user在棧里是各自儲(chǔ)存的,棧里的數(shù)據(jù)(地址,指針,引用)互不影響,但它們都指向同一個(gè)在托管堆里的實(shí)例對(duì)象,而user.Age = 18這一句操作的正是對(duì)托管堆里的實(shí)例對(duì)象的操作,而不是棧里的數(shù)據(jù)(地址,指針,引用)。num = 180操作的是棧里的數(shù)據(jù),而user.Age = 18卻是托管堆,就是這樣造成了不同的表現(xiàn)。

對(duì)于user = null這一句不會(huì)響應(yīng)Main的局部變量,看了第三幅圖應(yīng)該也很容易明白,user = null跟user.Age = 18不一樣,user = null是把棧里的數(shù)據(jù)(地址,指針,引用)設(shè)空,所以并不會(huì)影響Main的user。

這里再補(bǔ)充一下,對(duì)引用類型來說,var user = null,var user = new User(),user1 = user2都會(huì)影響棧里的數(shù)據(jù)(地址,指針,引用),第一個(gè)會(huì)設(shè)null,第二個(gè)會(huì)得到一個(gè)新的數(shù)據(jù)(地址,指針,引用),第三個(gè)跟上面?zhèn)鲄⒁粯?,都是棧?shù)據(jù)復(fù)制。

ref(out)傳遞值類型

復(fù)制代碼 代碼如下:

static void Main(string[] args)
{
    var num = 10;
    Method1(num);
    Console.WriteLine(num);//輸出=> 10
    Method3(ref num);
    Console.WriteLine(num);//輸出=> 28
}
 
static void Method1(int num)
{
    Console.WriteLine(num);
    num = 18;
}
 
static void Method3(ref int num)
{
    Console.WriteLine(num);
    num = 28;
}

代碼很簡(jiǎn)單,而且輸出應(yīng)該都很清楚,沒有難度。ref的使用看似簡(jiǎn)單平常,背后其實(shí)是C#為我們做了大部分工作。畫圖的話,“棧圖”如下(地址亂寫):

看到這圖,不少人應(yīng)該迷惑了,Method3的參數(shù)明明寫的是int類型的num,怎么在棧里卻是一個(gè)指針(地址,引用)呢?這其實(shí)C#“欺騙”了我們,IL反編譯看看:

可以看到,加了ref(out)的Method3編譯出來的方法參數(shù)是不一樣,再來看看方法里對(duì)參數(shù)取值的IL代碼:

復(fù)制代碼 代碼如下:

//這是Method1的代碼
//load arg 0=>讀取索引0的參數(shù),直接就是一個(gè)值
IL_0001:  ldarg.0
 
//這是Method3的代碼
//load arg 0=>讀取索引0的參數(shù),這是一個(gè)地址
IL_0001:  ldarg.0
//將位于上面地址處的 int32 值作為 int32 加載到堆棧上。
IL_0002:  ldind.i4


可以看到,同樣是獲取參數(shù)值給WriteLine,Method1只需一個(gè)指令,而Method3則需要2個(gè),即多了一個(gè)根據(jù)地址去尋值的步驟。不難想到,賦值也有同樣的區(qū)別:

復(fù)制代碼 代碼如下:

//Method1
//把18放入棧中
IL_0008:  ldc.i4.s   18
//store arg=> 把值賦給參數(shù)變量num
IL_000a:  starg.s    num
 
//Method3
//load arg 0=>讀取索引0的參數(shù),這是一個(gè)地址
IL_0009:  ldarg.0
//把28放入棧中
IL_000a:  ldc.i4.s   28
//在給定的地址存儲(chǔ) int32 值。
IL_000c:  stind.i4

沒錯(cuò),雖然同樣是num = 5這樣一個(gè)對(duì)參數(shù)的賦值語句,有沒有ref(out)關(guān)鍵字,實(shí)際上運(yùn)行時(shí)發(fā)生的事情是不一樣的。有ref(out)的方法跟上面取值一樣有給定地址然后去操作(這里是賦值)的指令。

看到這里大家應(yīng)該明白,給參數(shù)加了ref(out)后,參數(shù)才是引用傳遞,這時(shí)傳遞的是棧地址(指針,引用),否則就是正常的值傳遞--棧數(shù)據(jù)復(fù)制。

ref(out)傳遞引用類型

加了ref(out)的引用類型的參數(shù)有什么奧秘,這個(gè)留給大家去思考??梢钥隙ǖ氖?,還是從棧的角度去考慮的話,跟值類型是沒有區(qū)別的,都是傳遞棧地址。

我個(gè)人認(rèn)為,貌似給引用類型加ref(out)沒什么用處。惡魔

總結(jié)

在考慮這一大堆概念問題時(shí),我們首先要搞明白程序執(zhí)行的基本原理,只不過是棧的生長(zhǎng)和消亡的過程。明白這個(gè)過程后,要學(xué)會(huì)“從棧的角度”去思考問題,那么很多事情將會(huì)迎刃而解。為什么叫“值”類型和“引用”類型呢?其實(shí)這個(gè)“值”和“引用”是從棧的角度去考慮的,在棧里,值類型的數(shù)據(jù)就是值,引用類型在棧里只是一個(gè)地址(指針,引用)。還要注意到,變量除了可以是一個(gè)局部變量(參數(shù))外,還可以作為一個(gè)類型的字段成員存在。知道這些后,“值類型的對(duì)象是存儲(chǔ)在那里?”這些問題應(yīng)該就一清二楚了。最后就是明白C#默認(rèn)是按值傳參的,也就是把棧里的數(shù)據(jù)賦值給參數(shù),這跟在同一個(gè)方法內(nèi)把一個(gè)變量賦值給同一類型的另一個(gè)變量是一樣的,而加了ref(out)為什么這個(gè)神奇,其實(shí)是C#背后做了更多的事情,編譯成不同的IL代碼了。

相關(guān)文章

  • C#擴(kuò)展方法實(shí)例分析

    C#擴(kuò)展方法實(shí)例分析

    這篇文章主要介紹了C#擴(kuò)展方法,結(jié)合實(shí)例形式分析了C#擴(kuò)展方法的功能、使用方法及相關(guān)注意事項(xiàng),需要的朋友可以參考下
    2017-06-06
  • C# 進(jìn)行圖片壓縮的示例代碼(對(duì)jpg壓縮效果最好)

    C# 進(jìn)行圖片壓縮的示例代碼(對(duì)jpg壓縮效果最好)

    這篇文章主要介紹了C# 進(jìn)行圖片壓縮的示例代碼,幫助大家更好的利用c# 處理圖片,提高辦公效率,感興趣的朋友可以了解下
    2020-11-11
  • C#基礎(chǔ)之委托詳解(Delegate)

    C#基礎(chǔ)之委托詳解(Delegate)

    這篇文章主要介紹了C#基礎(chǔ)之委托(Delegate),具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2025-03-03
  • C#實(shí)現(xiàn)Base64編碼與解碼及規(guī)則

    C#實(shí)現(xiàn)Base64編碼與解碼及規(guī)則

    這篇文章主要介紹了C#實(shí)現(xiàn)Base64編碼與解碼,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2021-08-08
  • C#獲取HTML文本的第一張圖片與截取內(nèi)容摘要示例代碼

    C#獲取HTML文本的第一張圖片與截取內(nèi)容摘要示例代碼

    在日常web開發(fā)的時(shí)候,經(jīng)常會(huì)遇到需要獲取保存的HTML文本中的第一張圖片,并且截取內(nèi)容摘要的效果,例如織夢(mèng)的后臺(tái)添加完詳細(xì)內(nèi)容后就是自動(dòng)讀取內(nèi)容摘要,并保存第一張圖片為縮略圖,那么這篇文章跟大家分享下利用C#如何實(shí)現(xiàn),感興趣的朋友們下面來一起看看吧。
    2016-10-10
  • Unity的IPreprocessBuildWithReport實(shí)用案例深入解析

    Unity的IPreprocessBuildWithReport實(shí)用案例深入解析

    這篇文章主要為大家介紹了Unity的IPreprocessBuildWithReport實(shí)用案例深入解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-05-05
  • C#如何刪除指定文件或文件夾

    C#如何刪除指定文件或文件夾

    這篇文章主要介紹了C#如何刪除指定文件或文件夾,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-01-01
  • C#中重寫tospring方法的實(shí)現(xiàn)

    C#中重寫tospring方法的實(shí)現(xiàn)

    重寫ToString方法允許你自定義對(duì)象的字符串表示形式,本文主要介紹了C#中重寫tospring方法的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下
    2024-08-08
  • C#實(shí)現(xiàn)將Word轉(zhuǎn)化分享為電子期刊

    C#實(shí)現(xiàn)將Word轉(zhuǎn)化分享為電子期刊

    曾經(jīng)由一個(gè)項(xiàng)目,要求實(shí)現(xiàn)制作電子期刊定期發(fā)送給企業(yè)進(jìn)行閱讀,由編輯人員使用 Microsoft Word先生成PDF文件,然后將生成的PDF文件轉(zhuǎn)化為JPEG文件,最后將JPEG文件生成電子書模式,本文給大家介紹了C#實(shí)現(xiàn)將Word轉(zhuǎn)化分享為電子期刊,需要的朋友可以參考下
    2023-12-12
  • C#?HttpClient超時(shí)重試機(jī)制詳解

    C#?HttpClient超時(shí)重試機(jī)制詳解

    超時(shí)重試的實(shí)現(xiàn)方式可以使用循環(huán)結(jié)構(gòu),在請(qǐng)求發(fā)起后等待一定時(shí)間,若超時(shí)未收到響應(yīng),則再次發(fā)起請(qǐng)求,循環(huán)次數(shù)可以根據(jù)實(shí)際情況進(jìn)行設(shè)置,一般建議不超過三次,這篇文章主要介紹了C#?HttpClient超時(shí)重試,需要的朋友可以參考下
    2023-06-06

最新評(píng)論

沙河市| 灵宝市| 大兴区| 南雄市| 若尔盖县| 新邵县| 乳山市| 海城市| 西宁市| 德州市| 景谷| 冕宁县| 平远县| 延寿县| 涿州市| 常熟市| 汝城县| 南开区| 新巴尔虎右旗| 阿拉善盟| 盐亭县| 新源县| 高清| 长葛市| 合川市| 山西省| 澳门| 枝江市| 凤城市| 上虞市| 定兴县| 姚安县| 佛教| 札达县| 深州市| 延庆县| 仁布县| 营山县| 永兴县| 明光市| 淮安市|