最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

第 43 講:結(jié)構(gòu)(二):更進(jìn)一步

2021-06-25 07:43 作者:SunnieShine  | 我要投稿

前文我們簡單對結(jié)構(gòu)的用法作了一個介紹,可以看到結(jié)構(gòu)和類的用法基本上一樣,只是部分細(xì)節(jié)需要注意,比如無參構(gòu)造器什么的。下面我們進(jìn)一步對結(jié)構(gòu)類型進(jìn)行探討,讓你對結(jié)構(gòu)有一個更深層的認(rèn)識。

本節(jié)內(nèi)容或多或少會從 C 語言和 C++ 那邊拿一點(diǎn)東西過來說明。如果你沒有對 C 語言和 C++ 很熟悉的話,可能有一些地方看不太懂。不過,這里主要還是在說 C#,所以即使你看到了不太明白的地方,也沒有啥大問題,暫時先跳過它們就行。

Part 1 可變和不可變結(jié)構(gòu)

和類一樣,結(jié)構(gòu)也具有完全一致的用法和書寫規(guī)則。不過,類里我們可以對字段設(shè)置 readonly,也可以不設(shè)置。如果不設(shè)置 readonly,那么這個字段就有可能可提供在以后修改和變動。

如果一個類型的所有數(shù)據(jù)成員都無法在以后修改,只能在聲明的時候(用 new 實(shí)例化出來的對象)才能指定數(shù)據(jù)的話,那么這樣的類型我們稱為不可變類型(Immutable Type);相反地,如果數(shù)據(jù)在實(shí)例化后,仍然可修改的話,那么這個數(shù)據(jù)類型我們稱為可變類型(Mutable Type)。

類和結(jié)構(gòu)一樣,既可以定義為可變類型,也可以定義為不可變類型。舉個例子,我們還是拿 Person 說明:

比如這個例子。我們把 AgeIsBoy 都設(shè)置成了可提供后續(xù)修改的、底層字段沒有 readonly 修飾的屬性。此時,Person 類型是可變的。

Part 2 值類型和引用類型的賦值行為

在寫代碼的時候,我們經(jīng)常碰到這樣的代碼:

這個代碼是想把右側(cè)的 a 賦值給左側(cè)的 b。那么,如果 a 是引用類型的話,這種賦值過程到底是想表達(dá)什么意思呢?具體行為是什么樣的呢?

引用類型就意味著類型是以引用形式傳遞的。我們之前說過,C# 是從 C 語言和 C++ 拓展起來的,因此我們?yōu)榱舜蠹夷苊靼紫旅娴臇|西,我們從 C 語言和 C++ 層面來介紹。

在 C++ 里,傳參模式分 3 種:

  • 值傳遞

  • 引用傳遞

  • 指針傳遞

C# 把這個傳參機(jī)制沿用了下來。

在 C 語言和 C++ 里,值傳遞指的是把數(shù)據(jù)的具體內(nèi)容全都拷貝一份過來,然后賦值給另外一個變量。這也就意味著,賦值方和接收方兩邊的數(shù)據(jù)都是完全一樣的,而且還是獨(dú)立開來的。

接著。C 語言里是沒有引用傳遞這個概念的,因此我們只能剖析 C++ 里的引用傳遞。在 C++ 里,引用傳遞是想用一個所謂的引用(Reference)來表示一個數(shù)據(jù)。它相當(dāng)類似于指針的地址數(shù)值,但也有不同。引用的好處在于,你可以把兩個類型一致的變量通過引用進(jìn)行賦值;但是,通過指針賦值的話,因為接收方是一個指針變量(即存儲的是地址),那么左右兩側(cè)的變量的書寫格式和規(guī)范就可能“不對稱”。

我們對比一下 C 語言和 C++ 里,指針變量的賦值過程,你就知道了。

在指針賦值的時候,比如 int *p = &a 里,由于 a 是一個普通的變量,而接收方 p 卻是一個指針變量,因此我們不得不對右側(cè)的變量 a 追加 &,即取地址符號。而此時,左側(cè)變量 p 和右側(cè)變量 a 語法上就不對等了:左側(cè)的 p 是一個單純的 p,但右邊的 a 必須寫 &a。

這一點(diǎn)賦值可能你還感受不出來有多大的問題。在 C 語言里,存在一種叫做數(shù)組指針的概念,即一個指針變量,用于指向一個數(shù)組。那么假設(shè)我們這個數(shù)組是一維數(shù)組的話,那么整個數(shù)組指針就得這么寫:

可以看到代碼的第 7 行,我們聲明了一個 pArr 數(shù)組指針變量,這個變量用來接收一個數(shù)組的地址,表示左邊的變量是指向右邊的這個變量的。此時這個 &arr 的地址符號 & 是不能省略的。

可以看到這種書寫格式完全就“亂套”了。左邊的聲明居然寫成 int (*pArr)[3] 這樣復(fù)雜的東西,而右邊卻又是一個很簡單很簡單的 &arr 的取地址表達(dá)式。在語法上來說,它們寫法完全不同,我把這種行為稱為“不對稱的語法”。

可是,C# 引入了一種新的賦值模式:引用。賦值過程就會變得相當(dāng)簡單了:

因為 C# 里,數(shù)組符號 [] 必須靠向聲明類型一側(cè),所以寫成 int[] arr 而沒有寫成 int arr[];而另一方面,在引用賦值的時候,refArr 這個變量是直接用 = arr 賦值的。在 C++ 和 C 語言里,我們無法這么賦值,因為左側(cè)的數(shù)組必須要么通過初始化器賦值,要么隨后賦值,反正必須得定義長度,而不能用變量賦值給它:因為 C 語言和 C++ 里數(shù)組是一個指針(首地址)表達(dá)的一個存在,而直接賦值給左邊,就變成了“指針賦值給數(shù)組”的錯誤過程。

即使 C++ 里有引用這個東西,我們?nèi)匀恢荒軐懗深愃茢?shù)組指針一樣復(fù)雜的格式:int (&refArr)[3] = arr,還是很長也很不好看。但是這點(diǎn)在 C# 里,語法就很輕松了。

回到這里。引用傳遞和指針傳遞,實(shí)際上都只是把地址傳遞了一份過去,因此,這兩個變量(賦值方和接收方)是使用的同一塊內(nèi)存空間的。比如 C# 里,數(shù)組是引用類型,那么必然是引用傳遞:

想必你很快就能知道輸出結(jié)果是多少。沒錯,答案是 0。因為你在更改 refArr 指向的內(nèi)存空間的第二個元素,但 arrrefArr 是引用傳遞,因此修改 refArr 里的元素,就等效于在修改 arr 里的元素。因為它們指向的同一塊內(nèi)存空間,修改里面的元素其實(shí)是在更改指向的內(nèi)存空間,這里面的東西。

因此,引用類型是傳遞引用,而引用傳遞和地址傳遞(指針)的區(qū)別是,地址傳遞會導(dǎo)致語法不對稱,而引用傳遞不會,因此引用傳遞是一種非常方便便捷的賦值行為。而值傳遞則直接是把內(nèi)存空間的數(shù)據(jù)全部拷貝一份過去,因此數(shù)據(jù)是獨(dú)立的兩個個體,不會有引用傳遞那樣“更改一邊會影響另一邊”的問題。

總之,值類型是值傳遞,而引用類型是引用傳遞,因此如果遇到最開頭的那句話:

如果 Person 是值類型,那么 ba 就是完全獨(dú)立的兩個個體;而 Person 是引用類型的話,ba 就指向同一塊內(nèi)存空間。

Part 3 復(fù)制構(gòu)造器

C# 里,我們允許對類指定一個帶有一個同類型參數(shù)的構(gòu)造器,這樣的構(gòu)造器稱為復(fù)制構(gòu)造器。

比如這樣??墒?,這樣的賦值還是有點(diǎn)啰嗦,因為每個字段都要對應(yīng)到 another 變量上去,然后挨個賦值。C# 的值類型有一種獨(dú)特的賦值方式:

是的,C# 的引用類型里,this 引用是無法修改的。因為 GC。我之前就說過,GC 只看堆內(nèi)存。而引用類型剛好就是堆內(nèi)存存儲的數(shù)據(jù)。在 GC 回收一次后,漏出來的部分可能會通過“緊湊”處理把空閑部分拼起來放在一起??蓡栴}在于,此時所有引用類型的對象的地址就有可能產(chǎn)生變化。this 本身就指代了一個對象,因為我們直接在類里賦值,所以這種感覺其實(shí)并不能感受到。我們實(shí)際上在書寫 Age = age 的時候,實(shí)際上是在對我們特有的一個對象(用 this 表示的)來賦值的。

地址的變動與否是 GC 控制的,那么 C# 這個編程語言自然比我們更懂處理過程,所以,C# 不允許我們手動修改 this 的指向。

但是,這一點(diǎn)對于值類型來說就沒有那么重要了。值類型可放在棧內(nèi)存里,這使得對象并不會受到 GC 的“干擾”。那么,這么賦值就沒問題了。

Part 4 盡量不要用可變結(jié)構(gòu)

可變結(jié)構(gòu)和不可變結(jié)構(gòu)到底有什么區(qū)別?而標(biāo)題這個說法,又是為什么?下面我們來對這個說法作一個介紹。

前面我們介紹了值類型的傳遞模式是值傳遞。如果是可變結(jié)構(gòu)的話,某些地方的賦值模式會使得編譯器對代碼分析上出現(xiàn)一些問題。

假如我創(chuàng)建了一個 Person 結(jié)構(gòu)構(gòu)成的數(shù)組。

假如這里有 firstsecondthird 三個變量,那么這個賦值過程就是成功的。不過,如果我這么寫代碼:

那么,請問 thirdOne 修改到 persons 里的 third 了嗎?這個問題就好比我有一個 int[],然后我取出其中一個元素,然后修改掉元素的數(shù)值,問你是不是修改了原始的數(shù)據(jù)一樣。

實(shí)際上,并沒有。我們這里的等號兩側(cè)是 Person 類型,是一個值類型。按照值類型的執(zhí)行規(guī)則,我們是復(fù)制副本,因此 thirdOnethird 應(yīng)該是兩個完全獨(dú)立的個體,只是數(shù)值是一樣的。

另外,照著這個規(guī)則行事的話,我們?nèi)L試修改了 thirdOne 里的 Age 屬性,改成 30,也只是改掉了副本的數(shù)據(jù),原始數(shù)據(jù)并沒有變動。

這就是為什么不要使用可變結(jié)構(gòu)??勺兘Y(jié)構(gòu)在這個代碼體現(xiàn)上會有和我們自身理解沖突的不一樣的地方。C# 世界里有這么一句話:

Mutable structs are evil.

可變結(jié)構(gòu)都是惡魔。

因此,不要嘗試去定義可變值類型,雖然語法沒有阻止你這么定義。

Part 5 類型的默認(rèn)數(shù)值和 default 表達(dá)式

在之前 switch 語句里,我們用到了 default 的一種用法,是表達(dá)默認(rèn)情況?,F(xiàn)在我們介紹一下第二種用法:default(T)。其中的 T 指的是一個數(shù)據(jù)類型的名字。這表示獲取這個數(shù)據(jù)類型的默認(rèn)值是多少。

在值類型里,我們使用 default(T) 表達(dá)式,得到的結(jié)果一定是和 new T() 一樣的結(jié)果;但是在引用類型里,default(T) 一定是 null。下面我們來說一下,這是什么意思。

假設(shè) Person 是值類型,那么這三個賦值語句里,第三個是錯誤的賦值:你無法把 null 當(dāng)成 Person 的數(shù)值賦值過去,因為它表示“對象沒有分配內(nèi)存空間”,但值類型是受到方法自動控制的,所以它一定是有內(nèi)存分配的,因此最后一個寫法是錯的。而前面兩個是對的。

實(shí)際上,Person 是值類型的話,這兩個寫法是等價的,這表示 b 的所有字段都是默認(rèn)數(shù)值(_namenull,_age 是 0,而 _isBoyfalse)。

但是,如果 Person 是引用類型的話,第三種寫法就是正確的了。在引用類型里,default 表達(dá)式的默認(rèn)結(jié)果一定是 null,因此第一種和第三種等價。而第二種,如果 Person 類里擁有無參構(gòu)造器的話,那么就會自動執(zhí)行無參構(gòu)造器的行為;但如果沒有的話,就會產(chǎn)生編譯器錯誤??傊?,值類型和引用類型在這里有不一樣的地方。

Part 6 裝箱和拆箱

要想知道裝箱和拆箱的規(guī)則,我們就得先了解一下值類型和引用類型的繼承關(guān)系。

引用類型是默認(rèn)從 object 類型派生的。如果你定義了繼承關(guān)系,那么這個基類型就得從 object 派生;如果這個基類型也有繼承關(guān)系,那么就繼續(xù)往上倒回去找基類型。總之只要有一個沒有自己寫繼承關(guān)系的地方,就從 object 派生。

但是值類型不同。值類型是不能自己定義繼承關(guān)系的。這一點(diǎn)對于值類型來說相當(dāng)不方便,但也是沒有辦法的事情。值類型本身體現(xiàn)的是一組可以直接復(fù)制副本的輕量級數(shù)據(jù)類型。都輕量級了,那么肯定就沒有辦法從基類型繼承下來一些東西,不然這個類型不就太大了。所以,值類型是無法自定義繼承關(guān)系的。但是,它默認(rèn)是從一個叫做 ValueType 的類派生下來的。換句話說,繼承關(guān)系是這樣的:

總的來說,我們不能對值類型自定義繼承關(guān)系,但可以對引用類型定義繼承關(guān)系。但值類型默認(rèn)從 ValueType 類型派生,而它自動從 object 類型派生;而引用類型可以隨便定義繼承關(guān)系,但最終都是從 object 類型派生下來的

搞懂這一點(diǎn)之后,我們來說明一下裝箱(Box)和拆箱(Unbox)。

  • 裝箱:值類型對象轉(zhuǎn)換和賦值給引用類型基類型的過程。

  • 拆箱:引用類型對象轉(zhuǎn)換和賦值給值類型子類型的過程。

這么說不太明白的話,我們來舉個例子。首先我們知道 int 按照這個繼承關(guān)系的話,一定是從 object 派生的。那么我們可以這么定義一個多態(tài)的賦值關(guān)系:

這個賦值是沒有問題的。因為 10 是 int 類型的字面量,而 int 本身就是從 object 派生下來的,因此按照繼承關(guān)系和多態(tài)的規(guī)則,這么賦值是沒有問題的。

而這個時候,因為 10 是 int 類型(值類型),而 oobject 類型(引用類型)的關(guān)系,這么做會產(chǎn)生裝箱操作。所謂的裝箱,你可以理解成“打包”。你要把飯桌上的東西打包帶回去,總得有東西裝起來吧。這里的 o 變量好比是“家”這個容器(object 類型),而飯桌上的東西則當(dāng)成一個值類型。裝箱操作大概就是,用一個塑料袋把它們包裝起來,然后帶回去。

反過來,拆箱就是從家里帶酒水去外面的飯桌。顯然你帶出去還得拆開包裝盒吧,所以拆箱就是這么一個反方向的執(zhí)行行為。

拆箱的書寫代碼是這樣的:

即直接把類型轉(zhuǎn)回去。因為 oobject 類型的,而什么類型都可以往 object 賦值過去,所以要想拆箱,就必須加強(qiáng)制轉(zhuǎn)換,來告訴編譯器“我這么做是對的”。

這就是裝箱和拆箱。概念其實(shí)很好理解,但很多時候,我們不一定能夠察覺。我們來舉個例子。

C# 里提供了一種數(shù)據(jù)類型叫做 ArrayList。這個數(shù)據(jù)類型你可以當(dāng)成一個順序表,它比數(shù)組好一點(diǎn)的地方在于,它可以增刪數(shù)據(jù)。用法是這樣的:

使用 Add 這個實(shí)例方法,就可以對前面的這個 al 對象追加元素進(jìn)去了??蓡栴}在于,ArrayList 是為了通用才產(chǎn)生的,顯然我們不可能一定只放 int 數(shù)據(jù)進(jìn)去吧。所以,什么數(shù)據(jù)都可以放進(jìn)去的話,這個數(shù)組整體就必須找一個通用的數(shù)據(jù)類型才可以。

對了,object 類型剛好滿足需求。因此 ArrayListAdd 方法調(diào)用的時候,接收的參數(shù)類型是 object 類型的。那么,你傳入的這個 1、2、10,就會產(chǎn)生裝箱行為。

而如果要想取出這個 ArrayList 類型的數(shù)據(jù)元素的話,因為 ArrayList 本身是不知道里面的元素是什么類型的,因此你需要強(qiáng)制轉(zhuǎn)換:

當(dāng)然,C# 甚至還允許我們這么寫:

下面這種和上面這種其實(shí)是等價的。雖然看起來并不等價(畢竟 foreach 后面跟的類型完全不一樣了),但實(shí)際上,foreach 循環(huán)最終會被翻譯成自動強(qiáng)制轉(zhuǎn)換的過程,所以兩者是等價的。

而不管怎么說,這么書寫代碼總會類型轉(zhuǎn)換,所以會導(dǎo)致拆箱操作。

裝箱和拆箱和我們包裝食物和拆食物的包裝一樣,雖然這么做語法上和運(yùn)行上都沒有問題,但耗費(fèi)了不必要的性能,因此我們?yōu)榱损B(yǎng)成好習(xí)慣,要避免裝箱和拆箱行為。

第 43 講:結(jié)構(gòu)(二):更進(jìn)一步的評論 (共 條)

分享到微博請遵守國家法律
二连浩特市| 陵川县| 扶余县| 萍乡市| 澄江县| 晴隆县| 云南省| 福泉市| 浮梁县| 申扎县| 寿光市| 平邑县| 柳江县| 江西省| 鹿泉市| 武隆县| 上犹县| 永康市| 浙江省| 隆昌县| 福清市| 诏安县| 东乌珠穆沁旗| 东光县| 长岭县| 松江区| 安仁县| 交城县| 黄梅县| 满城县| 浦县| 平陆县| 正宁县| 亳州市| 德江县| 岑巩县| 平塘县| 工布江达县| 阿尔山市| 北辰区| 庆安县|