Immutable in C#

    技术2022-06-09  45

    摘要:本文从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  PhoneNumber get   return  phoneNumber; } } }

    这个例子几乎无须再解释,每次changeNumber的时候就构造一个新的Contact对象。

    C#  对immutability的支持离不开这两个关键字: const 和readonly 。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 []  1 2 3  } ;      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( 2 1 );       DelegateType affine2  =  MakeAffine( 3 4 );       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# 中也远远不只这些,欢迎大家来探讨和赐教,最后推荐几篇文章,有的是我在文章中引用过的:

    AnonymousMethods readonly vs. const [C# ] http://blogs.msdn.com/kevinpilchbisson/archive/2007/11/20/enforcing-immutability-in-code.aspx http://blogs.msdn.com/sreekarc/archive/2007/04/03/immutable -the-new-anonymous-type.aspx

    最新回复(0)