值物件與引用物件

taney發表於2016-04-16

起因

之前一直寫C#,因為有GC,所以不用關心物件的複製問題,預設的淺複製就夠了,也就沒去深究struct、class、值、引用間的區別,前段時間寫了點C++,C++需要手動管理記憶體,如果類包含有指標或引用成員時就要遵循rule of three,實現複製、賦值複製和析構以正確管理資源,這種生命週期管理函式寫多了感覺挺枯燥的,而且我發現許多domain object沒有必要實現複製,因為大多數domain object並不適合用值語義來表達,首先它們本質不是一種“值”,並且我希望傳遞時傳遞同一個物件,而不是一個副本。

C#中同時保留了值型別和引用型別,而我幾乎從未寫過自定義的值型別。於是我就開始思考值物件和引用物件到底有什麼區別,隨即寫下此文分享本人的見解。

值物件(value object)

什麼是值物件

值物件是標識取決於狀態的物件

你現在拿出一張紙幣,它就是典型的值物件,雖然每張紙幣都有一個唯一的編號,但實際使用中這個編號是沒有意義的,我們關心的是它們的面值。

這裡紙幣的編號就對應值物件的標識(比如記憶體地址),紙幣的面值就對應值物件表示的值。

其它值物件的例子:IP地址、RGB顏色、GUID、地理座標、日期時間。

我的理解:值物件是用來表達一個資訊的物件,它的狀態是靜態的,且比較“透明”;我們使用值物件時,關心的是它所表達的資訊,而不是這個物件本身

值物件與不可變性(immutability)

值物件應該被設計成不可變的(immutable),因為

  • 從理論上講值物件存在的意義就在於表示一個值,它的狀態決定了它的標識,如果狀態修改了,那就是一個新的值,新的物件。
  • 簡化程式設計、避免bug
    如果值物件可變,當它被共享時,一處修改可能會影響另一處,修改操作就會產生“副作用(side effect)”,如java中的java.util.Date就是可變的引用型別,使用不當就會產生問題:

    這裡的問題在於我們關心的其實只是一個“日期值”而不是一個java.util.Date物件,所以在傳遞時應該是傳遞表示的日期值,而不是直接傳遞java.util.Date物件。註釋中的FIXME標註是一種解決辦法。

值物件必須要實現為不可變嗎?

但是如果把一個類實現為不可變的話,意味著修改一個成員就要建立一個新的物件,如果成員非常多的話,程式碼寫起來會比較繁瑣。
有些語言支援值語義(value semantics),物件傳遞時是傳遞的它所表示的值(傳值),而不是傳遞物件本身(傳引用),這就意味著傳遞過程就是“複製”,比如C++預設就是值語義、C#中的struct等,因為是複製,所以值物件不會被共享,也就沒有了上面提到的那個問題,這種情況下,值物件可變的話也是完全可以的,但是值物件仍可能會被“按引用”傳遞,所以把值物件實現為不可變是最保險的手段。

值物件的實現

  • C++
    C++預設就是值語義,如果類狀態較簡單,則可以不做任何特殊處理。如果比較複雜,比如成員包含指標或引用,則就要實現生命週期管理函式
  • C#
    • 如果類狀態較簡單,可直接用struct,因為struct正好就是值語義
    • 如果狀態較複雜則用class,類的設計者應該從介面上把它設計成不可變,如果沒有這樣設計,則使用者最好在使用時“手動實現值語義”(見上java.util.Date示例)。
  • java
    同C#中的class

與值物件相關的概念

struct與class

不管是C++和C#中的struct關鍵字,還是ruby中的Struct::new都是趨向用於定義簡單的複合型別,所以struct適合用來定義沒有複雜行為和狀態的值物件。而class更趨向用於定義具有豐富邏輯、複雜狀態的物件型別,struct、class和值物件、引用物件並不是一一對應的關係,但一般而言,值物件都不會太複雜。

字串

拋開具體的實現,字串是一個靜態的字元序列,它的狀態決定了它的相等性,是一種值。

問題1,為什麼在C#、java中字串都是引用型別呢?

“引用型別”是具體語言/平臺實現中的概念,“值物件”,“引用物件”是語言無關的,引用型別的物件也可能是值物件,只是引用型別的物件不具有原生的值語義

將字串實現為”引用型別“更多是出於效能考慮。

  1. 避免複製。傳引用,可以避免發生內容的拷貝。
  2. 可以實現String interning(字串扣留)。扣留操作會檢查一個全域性字串扣留池,看有沒有與給定字串內容相同的已被扣留的字串物件,如果有則返回,沒有則進行扣留。如果程式中有很多內容相同的字串物件,這樣能節約記憶體,這也是Flyweight模式的一個案例。
    java示例:

    C#示例:

問題2,為什麼C++中的std::basic_string和Ruby中的String都是可變的?

很簡單,這樣用起來更方便。對於C++,可以用const實現不可變性,而Ruby是通過Symbol來表示唯一的、不可變的字串。

C#中的值型別

C#語言中的型別分為兩類“值型別”和“引用型別”,strut和enum屬值型別,class屬引用型別。C#編譯器處理struct時讓該型別繼承了System.ValueType這個抽象類,而enum則繼承自System.Enum,System.Enum還是繼承自System.ValueObject。

值型別 和 引用型別的區別只有一個:一個是值語義(傳值,複製),一個是引用語義(傳引用),至於什麼“一個分配於棧,一個分配於堆”,這是具體實現的問題,而且值型別不一定分配於棧,比如作為引用型別的成員(被捕捉到閉包中同屬該情況)。

引用物件(reference object)

引用物件是相等性取決於它的標識的一種物件。

為什麼“相等性取決於標識”?因為在使用引用物件時,我們關心的是這個物件本身,在傳遞過程中,需要傳遞同一個物件,所以需要傳遞物件的“引用(即標識、一般是記憶體地址)”,這也就是“引用語義(reference semantics)”。

我們實際寫程式中,使用物件的目的在於對映問題域中的事物,基本關心的是物件本身,所以物件大多都屬於引用物件。

實現

  • 在C#和java中用class定義的型別叫“引用型別”,具有引用語義,建立的物件天生就是引用物件
  • 在C/C++中通常通過分配堆記憶體和傳遞指標實現

參考

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

任選一種支付方式

值物件與引用物件 值物件與引用物件

相關文章