我的《冒號課堂》學習筆記 值與引用(2)語義型別

2022-09-12 14:30:28 字數 4434 閱讀 1639

值與引用

值語義的物件是獨立的,語義的物件卻是允許共享的。由於j**a不支援值型別物件,j**a程式設計師才更需要加強這方面的意識。語法和語義並不總是一致的——語法上的值型別可能在語義上是引用型別,語法上的引用型別可能在語義上是值型別。永遠不要忘記乙個基本原則:語法只是手段,語義才是目的。

為了判斷乙個型別的語義,那麼簡明的『石蕊測試法』便是乙個很好的選擇.在不影響程式正確性的前提下,乙個物件的復件能否代替原件?如果可以則該物件的型別是值語義的,否則是引用語義的。(這種判斷方法與語法無關,完全取決於物件設計者的用意。)

從命令式程式設計的角度看,乙個值語義變數的記憶體位址是無關緊要的,原件和復件的唯一差別在被清除後變得完全的等價,因而值語義又稱複製語義(copy semantics)。相對地,引用語義變數的記憶體位址至關重要,通常用指標來實現,因而引用語義又稱指標語義(pointer semantics)

從函式式程式設計的角度看,值應當是引用透明的,即乙個表示式隨時可被其值所替代。比如2+3總可以被5代替,「ab」.concat(「cd」)總可用「abcd」來代替。顯然,值的可替代性實質上抹殺了引用的作用。(在計算機術語中,透明transparency一詞很容易引起誤解,它不是指因透明而看得見,而是指透明得看不見或意識不到、不受影響)

從物件式程式設計的角度看,值語義與引用語義的區別在於物件標識(object identity)的重要程度。物件標識是乙個物件區別於其他物件的唯一的標識。它的每個物件都具備的乙個特質,反映了乙個物件作為實體的獨立性、可識別性和本體性,是物件的三大特性之一。oop中物件的三大特性是狀態(state)、行為(beh**ior)和標識(identity)。倘若乙個物件的標識在程式中沒有實際意義,意味著它的物件特性模糊、主體意識淡薄,更多地代表的是一種抽象的屬性(attribute)而非乙個具體的實體(entity),則它具有值語義。反而,則具有引用語義。值通過具體的資料來描述抽象的屬性,引用通過抽象的方式來指代具體的實體。

比如字串,j**a和c#中的string類雖然是引用型別,但它們的值語義是很明顯的。人們關心的是字串的內容,而非它的記憶體位址。j**a初學者最容易犯的乙個錯誤就是用相等運算子(equality operator)『==』來判斷字串的異同。用==比較的是字串的引用,用equals方法比較的才是字串的內容。因此,c#乾脆明智地過載(overload)了string的相等運算子,避免了這類錯誤。雖然c++同樣也過載了該運算子,但是c++中包括string在內的所有自定義型別本就是基於值語法的,它們的相等運算子自然不可能用於引用比較。c++中基於指標的char*字串型別的比較也不能用==運算子,而應該用strcmp函式。

值與引用還有乙個區別。值是不依賴記憶體位址的,即具有空間無關性。其實值還有時間無關性,即乙個值語義的物件在其生命週期中的狀態是固定的。也就是說,值語義型別一般是不可變的(immutable)。以j**a中的string為例:

string s1 = 「ab」;

string s2 = s2;//這行的賦值是基於引用的,因此s1與s2指向同乙個字串物件。

assert(s1=s2);

s2 += "cd";

assert(s1 != s2);

如果string是可變的,那麼當s2的內容從「ab」變成「abcd」後,s1的內容也會發生相應的變化。這就產生別名問題(aliasing problem),通常並非我們想要的結果,因此在s2完成自增運算後,系統便讓它換成另乙個字串物件,而原先的字串物件任然保持不變。這樣使我們省去了顯示深轉殖的過程。由此可見,不變性為引用型別貫徹值語義提供了變通的語法支援。

j**a和c#中的stringbuffer是可變的字串,角色的定位不是字串的持有者,而是字串的創造者,故而是可變的,並不具備值語義。c++中的string類由於是值型別的,對不可變的需求沒有那麼強烈。但除非特別需求,程式設計師還是應盡可能地保持字串物件的不可變性。除了在賦值、按值傳遞、作為返回值等是憑藉值型別的特點來保證字串的複製以外,還可利用關鍵字const來保證常量正常性(const-corectness)。

j**a和c#中的基本資料型別是值型別的,但有時需要以引用型別身份出現,這就需要乙個轉化過程,術語為封裝(boxing),逆過程稱為拆箱(unboxing)。裝箱後的物件雖然是引用型別的,但仍保持值語義,因此應該是不可變的。

乙個值語義的引用型別也有可能是可變的,比如j**a的日期型別date類。為了實現其值語義,需要手動進行必要的防禦性複製(defensive copying)。比如在getter方法返回物件的日期屬性之前、在setter方法傳入的日期引數賦值之前,都應拷貝乙份物件。雖然這就影響到程式效能,但程式的正確性永遠是第一位的。略諷刺意味的是,當初date類被設計成可變型別是為了減少物件的建立,但結果卻事與願違,反覆的防禦性複製也許會建立更多的物件。避免重複的防禦性複製的乙個方法就是在規範文件中進行相關說明。但這很不自然,同時不能保證客戶遵守規範。

值語義物件不是真正意義的物件,接下來就是關於值的時間無關性在語義上的意義。值是靜態而單純的,而引用是動態而複雜的。從這個角度上說,不可變性加強了值語義。比如整型是最常見的值型別之一,乙個整型就是乙個常量,理所應當是不可變的。同理,乙個值語義的物件也應該是乙個常量。舉個具體的例子,顏色型別color具備著典型的值語義,在j**a和c#中都是不可變的。你看得出x=color.red與x=1有什麼本質的區別嗎?無非是取值空間不一樣。當x被重新賦值為color.green時,原來的物件color.red並未發生變化,只是不再被x所引用。退一步講,即使乙個值語義物件是可變的,它與引用語義物件在概念上仍是有區別的:值語義物件的改變是一種新舊更替,即新物件更替舊物件,只是湊巧重用了後者所用的記憶體空間;引用語義物件的改變是一種自我更新。即乙個物件在保持同一性的前提下發生的狀態遷移或屬性改變。

如果說不可變性讓語法上的引用型別傾向於值語義,那麼不可複製性則讓語法上的值型別具備明顯的引用語義。這非常自然,值語義的特點是復件具有等效性,引用語義的特點是復件不具等效性。當然,不可複製性是一種比較極端的情形,因為一邊引用語義的型別也是允許複製的,只不過不能替代原件而已。由於語法的緣故(j**a沒有自定義的值型別,而c#中的值型別通常都是遵循值語義的。),具有不可複製性的值型別主要出現在c++上,著名的boost c++庫提供了noncopyable類。

對於前面一直在提的「值型別物件」和「引用型別物件」之所以沒有簡稱「值物件」和「引用物件」,是因為害怕產生歧義。因為後者常用於代表「值語義物件」和「引用語義物件」。尤其是值物件(value object,簡稱vo),更是常在系統設計中作為通用的術語被使用。

資料傳輸物件與值物件之間的區別:

從實際用途的角度考察,傳輸物件的主要目的是攜帶多項資料以減少網路開銷,值物件的主要目的是描述其他物件。

從內容傳遞的角度考察,傳輸物件因為重視跨層傳輸而更關心序列化問題,值物件因為重視值語義而更關心賦值問題。

從抽象程度的角度考察,傳輸物件不過是簡單的資料載體,資料的透明度高、內聚度低,即使內部結構直接裸露在外也未嘗不可。資訊隱藏可有可無,甚至連getter、seter方法都可省去,絲毫不具備抽象性。

從設計模式的角度考察,傳輸物件有時是可變的。因為乙個傳輸物件的內容可能需要多步填充或多次更新。值物件則不同,不可變性是乙個應盡量遵守的原則,避免產生別名問題而破壞值語義。

從對立概念的角度考察,傳輸物件在實踐中常與業務層的業務物件(bussiness object,簡稱bo)相提並論。業務物件又稱領域物件(domin object),是代表業務邏輯或關係的實體。嚴格說來,領域物件比業務物件更廣泛,因為有些問題領域沒有所謂的「業務」,如與**、軍隊、衛生保健、科研等部門相關的專案。儘管傳輸物件與業務物件在結構和資料上經常有重合的部分,但乙個是具有值語義、具體而短暫的輔助物件,乙個是具有引用語義、抽象而持久的核心物件(業務物件是應用系統中最核心的一類物件,常常與資料庫中的表有直接的對應關係),具有強力的對立性。在看值物件,與之對立的概念是引用物件,在領域驅動設計(domin-driven design,簡稱ddd)中又稱實體(entity)。

從表現特徵的角度考察,引用物件是資料、運算、標識的三位一體,值物件則被抽去了標識,而傳輸物件連運算都被抽去了,只剩下資料。

從關注焦點的角度考察,傳輸物件的焦點在於「有什麼」,值物件的焦點在於「是什麼」,而引用物件的焦點在於"是哪個"。

不同的物件是通過引用來區分的,是否可以說物件的標識也就是它的記憶體位址?首先,標識不等於引用。標識是一種抽象的oop概念,多用物件建模中;引用是一種具體的語法機制,多用於編碼實現中。另外,雖然標識大都通過引用來實現,但也有其他的實現方式,比如通過j**a中的陣列索引(array index)。其次,引用不等於記憶體位址。c++中的引用通過指標來實現,指標即記憶體位址,但j**a對c#中的引用則一般不是原始位址(raw address),而是不透明指標(opaque pointer),以保證記憶體的安全性。如果牽扯到物件的持久化或序列化問題,情況就更複雜了。比如在通過orm來獲取物件時,相同的主鍵對應的物件未必有相同的實體地址。如果是從同乙個持久化上下文(persistence context)下獲取的物件,因為快取(cache)的緣故,它們具有相同的引用。再例如乙個物件需要跨系統傳輸,不同機器之間記憶體位址的比較更是無從談起。如何協調這類邏輯標識與物理標識之間的矛盾,正是orm和分布式引用的重要課題之一。一直避談標識的具體實現,強調引用的抽象指代,也是基於這方面的考慮。

我的《冒號課堂》學習筆記 值與引用(1)語法型別

值與引用 值 value 與引用 reference 因其天生的對立性,提供了乙個二分法 dichotomy 的準則。把資料分成兩類 值 具有某種型別的資料 引用 可用來獲取特定資料的值 把變數分成兩類 值變數 表示值的變數 引用變數 表示引用的變數 把資料型別分成兩類 值型別 能直接被訪問的資料型...

右值引用與模板學習筆記

include using namespace std intmain1 void 右值是乙個和運算過程相匹配的臨時物件,這個臨時物件在於其所對應的語句執行完畢之後,就銷毀了 所以,我們無法從語法層面上直接訪問。左值是乙個有名字,有固定位址的表示式 int a 10 int b a const in...

我的python學習筆記2

在工作裡,有乙個檔案裡的漢字是多餘的,由於資料量很大 幾千甚至上萬 行資料,並且不一定每行都有漢字,所以想到了用python處理剔除行內漢字。由於資料是每行用逗號 分隔的,且漢字只會出現在固定的位置,所以就想到了用csv標準模組處理。由於csv.reader讀取到的結果是乙個list,然後再提出漢字...