C#创建常量、原子性的值类型
从类型设计谈起
从Class到Struct
假如我们要设计一个存储收信人地址的类型(Type), 我们管这个类型叫 Address。它应该包含这样几个属性:
Province 省
City 市
Zip 邮编
要求对Zip的格式进行控制(必须全为数字,且为6位),大家该如何设计呢?我想很多人会写成这样吧:
public class Address {
private string province;
private string city;
private string zip;
public string Province {
get { return province; }
set { province = value; }
}
public string City {
get { return city; }
set { city = value; }
}
public string Zip {
get { return zip; }
set {
CheckZip(value); // 验证格式
zip = value;
}
}
// 检测是不是正确的 zip
private void CheckZip(string value) {
string pattern = @"d{6}";
if(!Regex.IsMatch(value, pattern))
throw new Exception("Zip is invalid! ");
}
public override string ToString() {
return String.Format("Province: {0}, City: {1}, Zip: {2}", province, city, zip);
}
}
这里已经存在第一个问题:当我们声明一个类时,更多的是定义一系列相关的操作(或者叫行为、方法),当然类中也会包含字段和属性,但这些字段通常都是为类的方法所使用,而属性则常用于表示类的状态(比如StringBuilder的Length),类的能力(比如StringBuilder的 Capacity),方法进行的状态或者阶段。而定义一个结构时,我们通常仅仅是用它来保存数据,而不提供方法,或者是仅提供对其自身进行操作或者转换的方法,而非对其它类型提供服务的方法。
Address 不包含任何的方法,它仅仅是将Provice、City、Zip这样的三个数据组织起来成为一个独立的个体,所以最好将其声明为一个Struct而非是一个Class。(这里也有例外的情况:如果Address包含二十个或者更多的字段,则考虑将其声明为Class,因为Class在参数传递时是传引用,而Struct是传值。在数据较小的情况下,传值的效率更高一些;而在数据较大的时候,传引用占据更小的内存空间。)
所以我们首先可以将Address声明为一个Struct而非Class。
数据不一致的问题
我们接下来使用一下刚刚创建的Address类型:
Address a = new Address();
a.Province = "陕西";
a.City = "西安";
a.Zip = "710068";
Console.WriteLine(a.ToString()); // Province: 陕西, City: 西安, Zip: 710068
看上去是没有问题的,但是回想下类型的定义,在给Zip属性赋值时是有可能抛出异常的,所以我们还是把它放在一个Try Catch语句中,同时我们给Zip赋一个错误的值,看会发生什么:
try {
a.City = "青岛";
a.Zip = "12345"; // 这里触发异常
a.Province = "山东";
} catch {
}
Console.WriteLine(a.ToString());//Province: 陕西, City: 青岛, Zip: 710068
结果是出现了数据不一致的问题,当为Zip赋值的时候,因为引发了异常,所以对Zip以及其后的Province的赋值都失败了,但是对City的赋值是成功的。结果就是出现了Provice是陕西,City却是青岛这种情况。
即是在赋值Zip时没有引发异常,也会出现问题:在多线程情况下,当当前线程执行到修改了 City为“青岛”,但还没有修改 Zip 和 Province的时候(Zip仍为 “710068”、Province仍为“陕西”)。如果此时其他线程访问类型实例a,那么也将会读取到不一致的数据。
常量性和原子性
我们现在已经知道了上面存在的问题,那么接下来该如何改进呢?我们先来看看作者对常量性和原子性给的定义:
对象的原子性:对象的状态是一个整体,如果一个字段改变,其他字段也要同时做出相应改变。简单来说,就是要么不改,要么全改。
对象的常量性:对象的状态一旦确定,就不能再次更改了。如果想再次更改,需要重新构造一个对象。
我们已经知道了对象的原子性和常量性这两个概念,那么接下来该如何去实施呢?对于原子性,我们实施的办法是添加一个构造函数,在这个构造函数中为对象的所有字段赋值。而为了实施常量性,我们不允许在为对象赋值以后还能对对象状态进行修改,所以我们将属性中的set访问器删除掉,同时将字段声明为readonly:
public struct Address {
private readonly string province;
private readonly string city;
private readonly string zip;
public Address(string province, string city, string zip) {
this.city = city;
this.province = province;
this.zip = zip;
CheckZip(zip); // 验证格式
}
public string Province {
get { return province; }
}
public string City {
get { return city; }
}
public string Zip {
get { return zip; }
}
// 其余略 ...
}
这样,我们对Address对象的创建,将所有字段的赋值都在构造函数中作为一个整体来进行;而当我们需要改变单个字段的值时,也需要重新创建对象再赋值。我们看下下面的测试:
Address a = new Address("陕西", "西安", "710068");
try {
a = new Address("青岛", "山东", "22233");// 发生异常,对a重新赋值失败,但状态保持一致
} catch {
}
Console.WriteLine(a.ToString()); // 输出:Province: 陕西, City: 西安, Zip: 710068
避免外部类型对类型内部的访问
上面的方法解决了数据不一致的问题,但是还漏掉了一点:当类型内部维护着一个引用类型字段,比如说数组。尽管我们将它声明为了readonly,类型外部还是可以对它进行访问(如果你不清楚值类型和引用类型的区别,请参考 C#类型基础)。现在我们修改Address 类,添加一个数组phones,存储电话号码:
private readonly string[] phones;
public Address(string province, string city, string zip, string[] phones) {
// 略...
this.phones = phones;
}
public string[] Phones {
get { return phones; }
}
我们接下来做个测试:
string[] phones = { "029-88401100", "029-88500321" };
Address a = new Address("陕西", "西安", "710068", phones);
Console.WriteLine(a.Phones[0]); // 输出: 029-88401100
string[] b = a.Phones;
b[0] = "029-XXXXXXXX"; // 通过b修改了 Address的内容
Console.WriteLine(a.Phones[0]); // 输出: 029-XXXXXXXX
可以看到,尽管 phones字段声明为了readonly,并且也只提供了get属性访问器。我们仍然可以通过 Address对象a外部的变量b,修改了a对象内部的内容。如何避免这种情况的发生呢?我们可以通过深度复制的方式来解决,在Phones的get属性访问器中添加如下代码:
public string[] Phones {
get {
 
补充:软件开发 , C# ,