也說C#中的Immutable

摘要:本文從String開始,由淺入深地闡述了作者對Immutable的見解。並結合C#語言的不同版本探討了Immutable的不同實現方式。

Keywords:

C#,immutable class,immutable field,System.String,readonly,const,Anonymous Type

 

有一種很簡單也很受用的編程(不僅僅是C#)宗旨,就是所謂的"Immutability"(不可變性質)。簡單來講,一個immutable的對象一旦被創建好,它的狀態將不會改變。反過來,如果一個類的實例是immutable的,那麼我們把這個類也稱作immutable class。

這樣說來,似乎immutable的確是一個相當簡單的東西,不過從以下幾個問題中你可以找到使用immutable對象的便利之處。我們可以想一下,爲什麼編寫一個多線程的應用程序要相對困難一些?那是因爲在訪問某些資源(對象或者其他OS掌管的資源)的時候線程間的同步問題總是會令人感到頭疼。那爲什麼會有線程訪問同步的問題呢?那是因爲在多線程多個對象之間,要保證他們的的多個讀和寫的操作不會引起衝突是一件很困難的事。那麼這些衝突爲什麼會造成我們不希望的結果呢,其實關鍵就在於這裏的“寫”操作,因爲只有它會改變對象的狀態,給我們帶來非預計的結果。設想假如沒有這個寫操作,或者說,假設對象的狀態不被這裏的寫操作所影響會是怎樣呢?那樣的話還有同步的必要麼?下面我們就來看看所謂的Immutable class的作用。

 

System.String

 

還是從這個“知名”的Immutable class開始談吧,這個經常使用的類型被設計爲immutable,當你改變一個String對象的時候,一個新的對象副本將被創建。儘管幾乎所有的C#教科書都會談及這個問題,但是有時候我們似乎並不在意,於是我們經常會編寫類似這樣的語句:

string str = "cnblogs";

str.Replace(
"cn""CN");

 

這裏的str本身並沒有改變,只是創建了一個"CNblogs"的副本…要取得這個我們所期望的副本,只需拿一個對象引用指向它:

 

str = str.Replace("cn""CN");

 

很顯然,對String頻繁進行這樣的操作會在內存中製造N多String對象,多數情況下那並不是我們所希望的。當然,這時候我們知道可以用System.Text.StringBuilder這樣一個安全的方式來構造可變的字符串對象。

OK,上述內容幾乎所有C#語言相關書籍上的說法都是一致的。但是String真的是完全immutable的麼?

我想,這個倒未必哦,至少有這麼幾個方式是可以使得String不那麼immutable的:

     

     1.直接操作指針

public class Program {
   
static unsafe void ToUpper( string str ) {
      
fixed ( char* pfixed = str )
         
for ( char* p = pfixed; *!= 0; p++ )
            
*= char.ToUpper(*p);
   }

   
static void Main() {
      
string str = "Hello";
      System.Console.WriteLine(str);
      ToUpper(str);
      System.Console.WriteLine(str);
   }

}

 

     2.使用反射

 


typeof(string).GetField("m_stringLength",
BindingFlags.NonPublic
|BindingFlags.Instance).SetValue(s, 5);

 

(上述兩種方式,在這裏可以看到完成的應用)

 

爲什麼String要被設計爲immutable呢?正如前面提到的那樣,因爲immutable使得程序員在對string使用上不至於陷入競態條件(race condition)。另外,也因爲這樣的String很適於在hashtable/Dictionary<K,V>中做key,因爲只有immutable的對象作爲hash的鍵,才能保證hash值始終爲常量。當然,通常hash的值是從對象的某些狀態(或者子狀態)計算而來,而對象的這些狀態(子狀態)應爲immutable。

String還有一個很酷的特徵:儘管System.String是一個繼承自object的class,String對象可以用等號(“==”)來比較是否匹配,就像值類型一樣。這樣設計是好理解的,因爲我們討論的類型immutable是指類型對象的狀態immutable,對於String來說,immutable是指它的immutable。

例如:

 

string str1 = "foofoo";
string strFoo = "foo";

string str2 = strFoo + strFoo;

//儘管這裏的str1和str2引用的是不同的對象
//下面的比較結果仍爲true
Debug.Assert(str1 == str2);

 

從以上對String的討論中我們至少可以得到以下幾條immutable的優勢

  • 便於多線程編程
  • 方便地作爲hashtable的key
  • 便於比較狀態

不過我還是想提醒一下,immutable還是有副作用的,就比如之前提到的產生很多垃圾對象,不過如果要就這個問題談論下去的話,今天我的文章就寫不完了:) 

 

C#中immutable的實現

 

1.經典的immutable class

 

class Contact
{
    
public Contact(String fullName, String phoneNumber)
    
{
        
this.fullName= fullName;
        
this.phoneNumber= phoneNumber;
    }


    
public Contact ChangeNumber(String newNumber)
    
{
        
//創建一個新實例
        return new Contact (this.fullName, newNumber);
    }


    
readonly String fullName;
    
public String FullName get return fullName; }}

    
readonly String phoneNumber;
    
public uint PhoneNumberget return phoneNumber; }}
}

這個例子幾乎無須再解釋,每次changeNumber的時候就構造一個新的Contact對象。

C# 對immutability的支持離不開這兩個關鍵字: constreadonly。C#的編譯器使用這兩個關鍵字來確保某創建好的對象的狀態不會發生改變。之所以提供這兩個關鍵字,自然是因爲它們還是有所區別的。readonly允許在構造器中改變它的狀態(初始化),而const則不行。例如:

 

class cnblogs{
   Article(
string author,string title) 

      a_title
= title; 
      authorName 
= author; // 編譯此處會報錯
   }


   
readonly string a_title;
   
const string authorName = "Freesc";
}

(其他關於readonly和const的討論,見這裏

 

現在也許你會問,如果我的對象通過一個readonly的字段引用了另一個對象會怎樣呢?引用的對象的狀態會發生改變麼?答案是肯定的,看下面的例子:

 

public class C 

    
private static readonly int[] ints = new int[] 123 };
    
public static int[] Ints get return ints; }

 }

 

這裏如果我們嘗試在C中改變數組的值:C.ints = null;是無效的操作,這就是一種所謂的“引用不可變”,注意這裏只是說引用不可變,如果你嘗試在C外部使用:C.Ints[1] = 123;這樣的操作,你會發現數組本身其實是可以改變的。我們姑且可以把ints字段稱之爲“淺”不可變字段。所以你可以相對靈活的指定你需要immutable的字段,可以參考Eric Lippert的文章.

 

2. C# 2.0中的immutable

下面我們來看一個簡單的immutable演示程序,用到了諸如匿名方法這樣的C#2.0的語言特徵:

 

using System.Diagnostics;
class Program {
   
delegate int DelegateType(int x);
   
static DelegateType MakeAffine(int a, int b) {
      
return delegate(int x) return a * x + b; };
   }

   
static void Main() {
      DelegateType affine1 
= MakeAffine(21);
      DelegateType affine2 
= MakeAffine(34);
      Debug.Assert(affine1(
5== 11); // 2*5+1 == 11
      Debug.Assert(affine2(6== 22); // 3*6+4 == 22 
   }

}

每次改變係數a,b都返回的是一個新的delegateType委託實例,事實上,C#編譯器會生成一個如下類,來代替DelegateTType來實現它的功能:

 

 

這篇有關匿名方法的文章中詳細說明了這點。

 

3. C#3中的immutable 

C#3.0繼續發揚“匿名”的習慣,引入了匿名類型。C#3的編譯器生成的匿名類型是immutable的,所有的字段都是private的,所有的屬性都是隻能get(使用reflector可以看到)。下面的代碼將會在編譯時報錯:“Error 1 Property or indexer 'AnonymousType#1.A' cannot be assigned to -- it is read only ....”

 

        static void Main()
        
{
            var ab 
= new {A=1,B=2 };
            ab.A 
= 3;
        }
  

 

 

好了,暫且寫到這裏,其實immutable的概念是很廣的,在C#中也遠遠不只這些,歡迎大家來探討和賜教,最後推薦幾篇文章,有的是我在文章中引用過的:

enjoy it!

發佈了33 篇原創文章 · 獲贊 31 · 訪問量 18萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章