也说C#中的Immutable

也说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!

 

黄季冬

 

posted on 2008-07-26 18:29  J.D Huang  阅读(4314)  评论(12编辑  收藏  举报