《Effective C#》读书笔记——条目20:保证值类型的常量性和原子性<.NET资源管理>
"常量性"指的是:对象自创建后,它的值就保持不变。如果在构造函数中就验证了参数的有效性,那么就能够保证之后该变量值始终是有效的——因为已经不能再改变它的内部状态了。这样做有很多好处:
- 常量性的类型可以减少必要的错误检查。
- 常量性的类型也是线程安全的类型,上下文切换中线程读取的数据一致。
- 常量性的类型可以安全的暴露给外界,因为调用者不能改变对象的内部状态。
- 常量性的类型在基于散列(hash)的集合中表现良好,因为Object.GetHashCode()方法返回的就是一个不变量。
1.应用场景
我们不可能将所以的类型都设置为常量类型,我们需要对类型使用的场景来分析,这里我们指的是:同时具有常量性和原子性的值类型。我们应该将类型分解成各个能自然形成的单个实体结构。一个对象可能并不具备原子性,但是它的各个组成部分由不同的原子类型组成的(具有原子性的类型都是单一实体)。
2.设计常量类型示例
Address类版本1
下面我们来看一个简单的示例,来了解如何保证值类型的常量性和原子性。这是我们的第一个版本:
1 public struct Address 2 { 3 private string state; 4 private int zipCode; 5 6 public string Line1 7 { 8 get; 9 set; 10 } 11 12 public string Line2 13 { 14 get; 15 set; 16 } 17 18 public string City 19 { 20 get; 21 set; 22 } 23 24 public string State 25 { 26 get { return state; } 27 set 28 { 29 //ValidateState(value);//验证数据的有效性 30 state = value; 31 } 32 } 33 public int ZipCode 34 { 35 get { return zipCode; } 36 set 37 { 38 //ValidateZip(value); 39 zipCode = value; 40 } 41 } 42 }
我们运行第一个版本:
1 //示例 2 Address a1 = new Address(); 3 a1.Line1 = "111 S. Main"; 4 a1.City = "Anytown"; 5 a1.State = "IL"; 6 a1.ZipCode = 51111; 7 //修改 8 a1.City = "Ann arbor"; 9 a1.ZipCode = 48103; 10 a1.State = "MI"; 11 Console.WriteLine(string.Format("City:{0},ZipCode:{1},State:{2}", a1.City, a1.ZipCode, a1.State)); 12 Console.Read();
我们可以看到对象的内部状态被改变了,在更改City字段后,a1就处于无效状态——因为更改的State和ZipCode不能匹配;假如这段代码在多线程环境中任何在City更改过程中的上下文切换可能会导致另一个线程看到不一致的数据。同时如果ZipCode值无效,那么将会抛出异常;为了避免出现这种问题我们需要编写更多的内部校验代码,线程的安全也要求我们在每个属性访问器上添加线程同步检查,这导致代码的复杂度的增加。
Address类版本2
这个时候我们需要一个常量类型,在上面第一个版本的基础上我们将所以得实例字段的set访问器修改为private:
1 public struct Address2 2 { 3 public Address2(string line1, string line2, string city,string state, int zipCode) 4 : this() 5 { 6 this.Line1 = line1; 7 this.Line2 = line2; 8 this.City = city; 9 this.State = state; 10 this.ZipCode = zipCode; 11 } 12 13 public string Line1 14 { 15 get; 16 private set; 17 } 18 19 public string Line2 20 { 21 get; 22 private set; 23 } 24 25 public string City 26 { 27 get; 28 private set; 29 } 30 31 public string State 32 { 33 get; 34 set; 35 } 36 public int ZipCode 37 { 38 get; 39 set; 40 } 41 }
经过上面的改变,我们只能通过该类的构造函数来对其对象进行初始化工作,并且不能从外部修改一个对象的状态,除非创建一个新的对象,将新对象的引用付给需要修改的对象才可以:
1 //创建一个地址对象 2 Address2 a2 = new Address2("111 S. Main", "", "Anytown", "IL", 5111); 3 //改变,重新创建一个对象 4 a2 = new Address2(a2.Line1, a2.Line2, "Ann arbor", "MI", 48103);
Address类版本3
在版本2中我们的Address类型并不是严格的不可变。隐式属性中包含private setter仍然包含修改内部状态的方法,如果想完全实现不可变类型我们可以这样:将隐式属性改为显式属性,并将该属性的后台存储字段更改为readonly即可:
1 public struct Address3 2 { 3 public string Line1 4 { 5 get { return line1; } 6 } 7 private readonly string line1; 8 9 public string Line2 10 { 11 get { return line2; } 12 } 13 private readonly string line2; 14 15 public string City 16 { 17 get { return city; } 18 } 19 private readonly string city; 20 21 public string State 22 { 23 get { return state; } 24 } 25 private readonly string state; 26 27 public int ZipCode 28 { 29 get { return zipCode; } 30 } 31 private readonly int zipCode; 32 33 public Address3(string line1, string line2, string city,string state, int zipCode) 34 : this() 35 { 36 this.line1 = line1; 37 this.line2 = line2; 38 this.city = city; 39 this.state = state; 40 this.zipCode = zipCode; 41 } 42 }
3.包含引用类型字段的常量类型设计
在设计常量类型时,要确保没有漏洞会导致其内部状态被外界更改。我们需要格外注意常量类型中的可变引用类型字段,在为这样的类型设计构造函数时,需要对其中可变类型进行防御性的复制。我们来看一个示例:Phone是一个具有常量性的值类型:
1 public struct Phone 2 { 3 private readonly int phoneNumber; 4 5 public int PhoneNumber 6 { 7 get { return phoneNumber; } 8 } 9 10 public Phone(int phoneNumber):this() 11 { 12 this.phoneNumber = phoneNumber; 13 } 14 }
下面是PhoneList类:
版本1
1 public struct PhoneList 2 { 3 private readonly Phone[] phones; 4 5 public PhoneList(Phone[] ph) 6 { 7 this.phones = ph; 8 } 9 10 public IEnumerable<Phone> Phones 11 { 12 get 13 { 14 return phones; 15 } 16 } 17 }
运行程序:
1 //初始化phones 2 Phone[] Phones = new Phone[10]; 3 PhoneList p1 = new PhoneList(Phones); 4 5 //值被改变 6 Phones[5] = new Phone(123456789); 7 Console.WriteLine(p1.Phones.ToArray()[5].PhoneNumber);
我们发现p1的值被改变了,这是由于数组时引用类型,这表示PhoneList结构内部引用的数组和外部的phones数组引用的是同一块内存空间,因此,外部就有机会通过改变phones来修改常量结构PhoneList。如果Phone是引用类型那么着将更容易出现问题。为了避免出现这样的情况,我们需要对数组进行一次防御性的复制。
实时上只要常量类型中存在任何可变的引用类型都应该在该类型的所有构造函数中进行防御性的复制。
现在我们将PhoneList结构修改如下:
版本2
1 public struct PhoneList2 2 { 3 private readonly Phone[] phones; 4 5 public PhoneList2(Phone[] ph) 6 { 7 phones = new Phone[ph.Length]; 8 //拷贝一个值的副本,因为Phone是一个值类型 9 ph.CopyTo(phones, 0); 10 } 11 12 public IEnumerable<Phone> Phones 13 { 14 get 15 { 16 return phones; 17 } 18 } 19 }
对于一些常量值我们也可以通过创建工厂方法来实现,.NET 中的Color类就是采用这种方式来初始化系统颜色的。对于那些需要多个步骤操作才能完整构造出的常量类型,我们可以创建一个辅助类——String类就是采用这种方式的——借助StringBuilder类。
小节
常量类型使我们的代码更简洁,不需要盲目的为类型中的每个属性都创建get和set访问器,对于那些用于存储数据的类型,应该尽可能的保证其常量性和原子性。在这些常量类型的基础上我们可以更容易的创建更复杂的结构。