c#基礎(chǔ)系列之值類型和引用類型的深入理解
前言
不知不覺已經(jīng)踏入坑已10余年之多,對(duì)于c#多多少少有一點(diǎn)自己的認(rèn)識(shí),寫出來渴求同類抨擊,對(duì)自己也算是個(gè)十年之癢的一個(gè)總結(jié)。
C#把數(shù)據(jù)類型分為值類型和引用類型
1.1:從概念上來看,其區(qū)別是值類型直接存儲(chǔ)值,而引用類型存儲(chǔ)對(duì)值的引用。
1.2:這兩種類型在內(nèi)存的不同地方,值類型存儲(chǔ)在堆棧中,而引用類型存儲(chǔ)在托管對(duì)上。存儲(chǔ)位置的不同會(huì)有不同的影響。
下面話不多說了,來一起看看詳細(xì)的介紹吧
基本概念
CLR支持兩種類型:值類型和引用類型。 面試過很多5年左右的同學(xué),有很多連值類型和引用類型的基本概念都回答不上來,難道現(xiàn)在的c#開發(fā)人員基礎(chǔ)這么弱了嗎?還是大家都不重視基礎(chǔ)呢?這個(gè)隨便找一篇博客都可以基礎(chǔ)入門的。
引用類型
哪些類型是引用類型呢?其實(shí)一個(gè)可以稱為”類“的類型都是引用類型。 引用類型總是從托管堆上分配的,常用的語法就是New XX(). C#的new 操作符會(huì)返回對(duì)象的指針 - 也就是指向?qū)ο髷?shù)據(jù)的內(nèi)存地址的一個(gè)引用。引用類型的傳遞其實(shí)傳遞的是對(duì)象的指針(string類型比較特殊),所以在特定的場(chǎng)景下性能是高于值類型的。一個(gè)引用類型在創(chuàng)建時(shí)默認(rèn)為null,也就是說當(dāng)前變量不指向一個(gè)有效的對(duì)象,也就是我們常遇到的異?!拔磳?duì)象引用設(shè)置到對(duì)象的實(shí)例”。
值類型
因?yàn)橐妙愋妥兞慷夹枰M(jìn)行一次堆內(nèi)存的分配,這會(huì)給GC造成很大的壓力,所以CLR提供了輕量級(jí)類型“值類型”。 值類型一般在線程棧上分配。(注意:值類型可以嵌入一個(gè)引用對(duì)象中)一個(gè)值類型變量其實(shí)就包含了值類型實(shí)例的值,所以它沒有引用類型的指針(大家猜想值類型需不需要類型對(duì)象指針呢?)
相同點(diǎn)和不同點(diǎn)
相同點(diǎn)
- 值類型和引用類型都是System.Object的子類
- 值類型和引用類型都可以繼承接口。(很多人都認(rèn)為值類型不能繼承接口)
interface Itest
{
void test();
}
struct TestStruct : Itest
{
public void test()
{
throw new NotImplementedException();
}
}
不同點(diǎn)
- 值類型分配在堆棧上,引用類型是在托管堆上分配的。這里需要指出一點(diǎn):如果一個(gè)引用類型中的某個(gè)屬性是值類型,這個(gè)值類型的屬性是分配在托管堆上的。
- 所有的值類型都是隱式密封的(sealed),例如 :你不可能繼承int 來構(gòu)造自己的類型。
- 值類型的每一次賦值都會(huì)執(zhí)行一次逐字段的復(fù)制,所以如果是頻繁賦值也會(huì)造成性能上的壓力,引用類型的賦值只是指針的傳遞,其實(shí)也是生成新的指針實(shí)例。
- 引用類型額外有類型對(duì)象指針和同步塊索引,值類型是沒有的。所以我們平時(shí)使用lock 鎖的對(duì)象不可能是值類型,因?yàn)橹殿愋蜎]有同步塊索引
性能
有的同學(xué)說值類型的性能高于引用類型,那為什么不都用值類型呢?引用類型也是如此。任何東西都有兩面性,只有合適的類型,沒有萬能的類型。
1、值類型:所謂的.net Framework中的“輕量類型”,為什么說是“輕量”呢,這和他的內(nèi)存分配有直接關(guān)系,因?yàn)橹殿愋褪欠峙湓跅I?,所以在GC的控制之外,不會(huì)對(duì)GC造成壓力。那是不是可以隨便用呢?當(dāng)然不是,舉個(gè)例子:我自定義一個(gè)struct 類型作為一個(gè)方法的參數(shù)會(huì)發(fā)生什么呢?每次調(diào)用都會(huì)發(fā)生全字段的賦值,這是不可接受的,這也是典型的值類型勿用場(chǎng)景。
2、引用類型:引用類型分配在堆中,所以會(huì)影響GC,如果頻繁的初始化引用類型,對(duì)GC的壓力是很大的,因?yàn)槊恳淮畏峙涠加锌赡軙?huì)強(qiáng)制執(zhí)行一次垃圾收集操作。另外提一點(diǎn),引用類型的所占內(nèi)存,并非所有屬性/字段的和,堆上分配的每個(gè)對(duì)象都有一些額外的成員,這些成員必須初始化。(類型對(duì)象指針和內(nèi)存塊索引)。
3、裝箱拆箱:所謂裝箱就是將值類型轉(zhuǎn)化為引用類型的過程。拆箱則相反(只是概念上相反,實(shí)際編譯器的操作不一樣)。有的同學(xué)說裝箱拆箱影響性能,那到底是裝箱影響呢還是拆箱呢還是都影響呢?
裝箱發(fā)生了什么過程呢:
- 在托管堆中分配好內(nèi)存,分配的內(nèi)存量是值類型的各個(gè)字段需要的內(nèi)存量加上托管堆上所以對(duì)象的兩個(gè)額外成員(類型對(duì)象指針,同步塊索引)需要的內(nèi)存量
- 值類型的字段復(fù)制到新分配的堆內(nèi)存中
- 返回對(duì)象的地址,這個(gè)地址就是這個(gè)對(duì)象的引用
拆箱發(fā)生了什么過程呢:
- 獲取已經(jīng)裝箱的值類型實(shí)例的指針
- 把獲取到的值復(fù)制到棧
所以裝箱是比較耗費(fèi)性能的,還有可能引發(fā)一次GC操作,而拆箱只是一個(gè)獲取指針的過程耗費(fèi)資源要比裝箱小的多。注意:一個(gè)對(duì)象拆箱之后只能還原為原先未裝箱之前的類型,例如:你不能把int32類型裝箱后還原為int16類型。 所以面試的時(shí)候可以和面試官裝B一下了~~
測(cè)試?yán)?/strong>
值類型引用類型分別初始化N次消耗的時(shí)間,代碼如下
static void Main(string[] args)
{
Console.WriteLine("test start");
int totalCount = 10000000;
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < totalCount; i++)
{
TestRef temp = new TestRef() { Id = i, Name = "test" };
}
sw.Stop();
Console.WriteLine($"引用類型耗時(shí):{sw.ElapsedMilliseconds}");
sw.Reset();
sw.Start();
for (int i = 0; i < totalCount; i++)
{
TestVal temp = new TestVal() { Id = i, Name = "test" };
}
sw.Stop();
Console.WriteLine($"值類型耗時(shí):{sw.ElapsedMilliseconds}");
Console.Read();
}
class TestRef
{
public int Id { get; set; }
public string Name { get; set; }
}
struct TestVal
{
public int Id { get; set; }
public string Name { get; set; }
}
運(yùn)行結(jié)果:
引用類型耗時(shí):205
值類型耗時(shí):152
可見初始化速度值類型是優(yōu)于引用類型的,也可能是引用類型引發(fā)了GC導(dǎo)致。
作為方法參數(shù)傳遞,代碼如下:
static void Main(string[] args)
{
Console.WriteLine("test start");
long totalCount = 1000000000;
Stopwatch sw = new Stopwatch();
sw.Start();
TestRef tempRef = new TestRef() { Id = 1, Name = "test" , Name2="r3rewfdsafdsa", Name3="fsrewfdsafdsafdsa", Name4="fdafdasfdsafdsa", Name5="432tretsfds", Name6="fdsafdasfdasfd" };
for (int i = 0; i < totalCount; i++)
{
TestR(tempRef);
}
sw.Stop();
Console.WriteLine($"引用類型耗時(shí):{sw.ElapsedMilliseconds}");
sw.Reset();
sw.Start();
TestVal tempVal = new TestVal() { Id = 1, Name = "test", Name2 = "r3rewfdsafdsa", Name3 = "fsrewfdsafdsafdsa", Name4 = "fdafdasfdsafdsa", Name5 = "432tretsfds", Name6 = "fdsafdasfdasfd" };
for (int i = 0; i < totalCount; i++)
{
TestV(tempVal);
}
sw.Stop();
Console.WriteLine($"值類型耗時(shí):{sw.ElapsedMilliseconds}");
Console.Read();
}
static void TestR(TestRef r)
{
return;
}
static void TestV(TestVal v)
{
return;
}
class TestRef
{
public int Id { get; set; }
public string Name { get; set; }
public string Name2 { get; set; }
public string Name3 { get; set; }
public string Name4 { get; set; }
public string Name5 { get; set; }
public string Name6 { get; set; }
}
struct TestVal
{
public int Id { get; set; }
public string Name { get; set; }
public string Name2 { get; set; }
public string Name3 { get; set; }
public string Name4 { get; set; }
public string Name5 { get; set; }
public string Name6 { get; set; }
}
運(yùn)行結(jié)果:
引用類型耗時(shí):4437
值類型耗時(shí):5226
可見在普通情況下,作為參數(shù)值類型和引用類型用時(shí)差距不大,但是,如果值類型的實(shí)例屬性比較多的情況下差距降進(jìn)一步拉大。
非正式環(huán)境測(cè)試用例,結(jié)果僅供參考
應(yīng)用場(chǎng)景
不止是面試的時(shí)候經(jīng)常問應(yīng)用場(chǎng)景這個(gè)問題,就是自己平時(shí)寫程序也應(yīng)該清楚。程序設(shè)計(jì)選擇的時(shí)候大部分場(chǎng)景都是用引用類型,但是如果你滿足下列條件,值類型可能更適用:
- 類型不會(huì)派生出任何其它類型,也就是說不會(huì)有被繼承的可能
- 類型不需要繼承其他類型
- 類型的實(shí)例比較小,并且不會(huì)被作為方法參數(shù),不會(huì)被頻繁賦值
- 你永遠(yuǎn)不會(huì)用到類型釋放時(shí)候的通知,因?yàn)橐妙愋屠梦鰳?gòu)函數(shù)可以利用其他手段可以得到釋放時(shí)候的通知。
- 如果你的類型實(shí)例不會(huì)發(fā)生值的改變或者可以認(rèn)為是readonly性質(zhì)的,值類型或許是首選。
其他
- 所有的值類型都從System.ValueType 派生,
System.ValueType繼承System.Object,但是System.ValueType重寫了Equals 和GetHashCode 方法,其實(shí)在這里才是真正和引用類型的分割線。 - 因?yàn)橹殿愋陀醒b箱拆箱的操作,所以像ArrayList這樣的集合性能是非常令人擔(dān)憂的。所以c# 2.0 出現(xiàn)了泛型 例如:List .....來保證了類型安全,同時(shí)又避免了拆箱裝箱,因?yàn)椴皇俏叶x的類型 ,你TMD根本連編譯器那一關(guān)都過不了 哈哈哈~~~~
順便說一句,好久不寫博客,樣式真實(shí)花時(shí)間啊,后來干脆寫markdown格式的,請(qǐng)大家見諒??!
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問大家可以留言交流,謝謝大家對(duì)我們的支持。
上一篇:自定義WPF窗體形狀的實(shí)戰(zhàn)記錄
欄 目:C#教程
本文標(biāo)題:c#基礎(chǔ)系列之值類型和引用類型的深入理解
本文地址:http://www.jygsgssxh.com/a1/C_jiaocheng/5087.html
您可能感興趣的文章
- 01-10輕松學(xué)習(xí)C#的基礎(chǔ)入門
- 01-10C#開源的AOP框架--KingAOP基礎(chǔ)
- 01-10C#進(jìn)階系列 WebApi身份認(rèn)證解決方案推薦:Basic基礎(chǔ)認(rèn)證
- 01-10C#線程處理系列之線程池中的I/O線程
- 01-10C#基礎(chǔ)知識(shí)之new關(guān)鍵字介紹
- 01-10C#基礎(chǔ)知識(shí)之this關(guān)鍵字介紹
- 01-10C#基礎(chǔ)知識(shí)之base關(guān)鍵字介紹
- 01-10C#基于TCP協(xié)議的服務(wù)器端和客戶端通信編程的基礎(chǔ)教程
- 01-10C# 設(shè)計(jì)模式系列教程-簡(jiǎn)單工廠模式
- 01-10C# 設(shè)計(jì)模式系列教程-外觀模式


閱讀排行
- 1C語言 while語句的用法詳解
- 2java 實(shí)現(xiàn)簡(jiǎn)單圣誕樹的示例代碼(圣誕
- 3利用C語言實(shí)現(xiàn)“百馬百擔(dān)”問題方法
- 4C語言中計(jì)算正弦的相關(guān)函數(shù)總結(jié)
- 5c語言計(jì)算三角形面積代碼
- 6什么是 WSH(腳本宿主)的詳細(xì)解釋
- 7C++ 中隨機(jī)函數(shù)random函數(shù)的使用方法
- 8正則表達(dá)式匹配各種特殊字符
- 9C語言十進(jìn)制轉(zhuǎn)二進(jìn)制代碼實(shí)例
- 10C語言查找數(shù)組里數(shù)字重復(fù)次數(shù)的方法
本欄相關(guān)
- 01-10C#通過反射獲取當(dāng)前工程中所有窗體并
- 01-10關(guān)于ASP網(wǎng)頁無法打開的解決方案
- 01-10WinForm限制窗體不能移到屏幕外的方法
- 01-10WinForm繪制圓角的方法
- 01-10C#實(shí)現(xiàn)txt定位指定行完整實(shí)例
- 01-10WinForm實(shí)現(xiàn)仿視頻播放器左下角滾動(dòng)新
- 01-10C#停止線程的方法
- 01-10C#實(shí)現(xiàn)清空回收站的方法
- 01-10C#通過重寫Panel改變邊框顏色與寬度的
- 01-10C#實(shí)現(xiàn)讀取注冊(cè)表監(jiān)控當(dāng)前操作系統(tǒng)已
隨機(jī)閱讀
- 01-10delphi制作wav文件的方法
- 01-11ajax實(shí)現(xiàn)頁面的局部加載
- 08-05dedecms(織夢(mèng))副欄目數(shù)量限制代碼修改
- 01-11Mac OSX 打開原生自帶讀寫NTFS功能(圖文
- 08-05DEDE織夢(mèng)data目錄下的sessions文件夾有什
- 01-10SublimeText編譯C開發(fā)環(huán)境設(shè)置
- 01-10使用C語言求解撲克牌的順子及n個(gè)骰子
- 08-05織夢(mèng)dedecms什么時(shí)候用欄目交叉功能?
- 01-10C#中split用法實(shí)例總結(jié)
- 04-02jquery與jsp,用jquery


