具有常量性的类型很简单,它们自创建后便保持不变。如果在构造的时候就验证了参数的有效性,我们就可以确保从此之后它都处于有效的状态。因为我们不可能再更改其内部状态。通过禁止在构建对象之后更改对象状态,我们实际上可以省却许多必要的错误检查。具有常量性的类型同时也是线程安全的:多个reader可以访问同样的内容。因为如果内部状态不可能改变,那么不同线程也就没有机会获得同一数据的不同值。具有常量性的类型也可以安全地暴露给外界,因为调用者不可能改变对象的内部状态。具有常量性的类型在基于散列(hash)的集合中也表现得很好,因为由Object.GetHashCode()方法返回的值必须是一个不变量(参见条款10),而具有常量性的类型显然可以保证这一点。 然而,并非所有类型都可以为常量类型。如果那样的话,我们将需要克隆对象来改变程序的状态。这也就是为什么本条款同时针对具有常量性和原子性的值类型。我们应该将我们的类型分解为各种可以自然形成单个实体的结构。比如,Address类型就是这样的例子。一个Address对象是由多个相关字段组成的单一实体。其中一个字段的更改可能意味着需要更改其他字段。而Customer类型就不具有原子性。一个Customer类型可能包含许多信息:地址(address)、名称(name)以及一个或者多个电话号码(phone number)。这些独立信息中的任何一个都可能更改。一个Customer对象可能要更改它的电话号码,但并不需要更改地址;也可能更改它的地址,而仍然保留同样的电话号码;也可能更改它的名称,但保留同样的电话号码和地址。因此,Customer对象并不具有原子性。但它由各个不同的原子类型组成:一个地址、一个名称或者一组电话号码/类型对[13]。具有原子性的类型都是单一的实体:我们通常会直接替换一个原子类型的整个内容。但有时候也有例外,比如更改构成它的几个字段。 下面是一个典型的可变类型Address的实现: // 可变结构Address。 public struct Address { private string _line1; private string _line2; private string _city; private string _state; private int _zipCode; // 依赖系统产生的默认构造器。 public string Line1 { get { return _line1; } set { _line1 = value; } } public string Line2 { get { return _line2; } set { _line2 = value; } } public string City { get { return _city; } set { _city= value; } } public string State { get { return _state; } set { ValidateState(value); _state = value; } } public int ZipCode { get { return _zipCode; } set { ValidateZip( value ); _zipCode = value; } } // 忽略其他细节。 } // 应用示例: Address a1 = new Address( ); a1.Line1 = "111 S. Main"; a1.City = "Anytown"; a1.State = "IL"; a1.ZipCode = 61111 ; // 更改: a1.City ="Ann Arbor"; // ZipCode、State 现在无效。 a1.ZipCode = 48103; // State 现在仍然无效。 a1.State = "MI"; // 现在整个对象正常。 内部状态的改变意味着有可能违反对象的不变式 (invariant)——至少是临时性地违反。在我们将City字段更改之后,a1就处于无效的状态了。City更改后便不再与State或者ZipCode匹配。上面的代码看起来好像没什么问题,但是假设这段代码是一个多线程程序的一部分,那么任何在City更改过程中的上下文切换都可能导致 另一个线程看到不一致的数据视图。 即使我们并不是在编写多线程应用程序,上面的代码仍然存在问题。假设ZipCode的值无效,因此抛出了一个异常。这时候我们实际上仅做了一部分改变,对象将处于一个无效的状态。为了修复这个问题,我们需要在Address结构中添加相当多的内部校验代码。这无疑将增加代码的体积和复杂性。为了完全实现异常安全,我们还需要在所有改变多个字段的代码块处放上防御性的代码。线程安全也要求我们在每一个属性访问器(get和set)上添加线程同步检查。总而言之,这将是一个相当可观的工作——而且我们还要考虑随着时间的推移,功能的增加,以及代码可能的扩展。 相反,让我们将Address结构实现为常量类型。首先,要将所有的实例字段都更改为只读字段: public struct Address { private readonlystring _line1; private readonly string _line2; private readonly string _city; private readonly string _state; private readonly int _zipCode; // 忽略其他细节。 } 同时要删除每个属性的所有set访问器: public struct Address { // ... public string Line1 { get { return _line1; } } public string Line2 { get { return _line2; } } public string City { get { return _city; } } public string State { get { return _state; } } public int ZipCode { get { return _zipCode; } } } 现在我们得到了一个常量类型。为了让其可用,我们还需要添加必要的构造器来彻底初始化Address结构。目前看来,Address结构只需要一个构造器来为其每一个字段赋值。复制构造器就不必要了, 因为C#默认的赋值操作符已经足够高效了。记住,默认的构造器仍然是有效的。使用默认构造器创建的Address对象中所有的字符串将为null,而 zipCode将为0: public struct Address { private readonly string _line1; private readonly string _line2; private readonly string _city; private readonly string _state; private readonly int _zipCode; public Address( string line1, stringline2, string city, string state, int zipCode) { _line1 = line1; _line2 = line2; _city = city; _state = state; _zipCode = zipCode; ValidateState( state ); ValidateZip( zipCode ); } // 忽略其他细节。 } 要改变常量类型,我们需要创建一个新对象,而非在现有的实例上做修改: // 创建一个Address: Address a1 = new Address( "111 S. Main", "", "Anytown", "IL",61111 ); //使用重新初始化的方式来改变对象: a1 = new Address( a1.Line1, a1.Line2, "Ann Arbor","MI", 48103 ); 现在a1只可能处于以下两个状态中的一个:原来位于Anytown的位置,或者位于Ann Arbor的新位置。我们将不可能再像前面的例子中那样把一个现有的Address对象更改为任何无效的临时状态。那些无效的中间态只可能存在于 Address构造器的执行过程中,不可能出现在构造器之外。只要一个Address对象被构造好后,它的值将保持恒定不变。新版的Address也是异常安全的:a1或者为原来的值,或者为新构造的值。如果有异常在新的Address对象的构造过程中被抛出,那么a1将保持原来的值。 对于常量类型,我们还要确保没有任何漏洞会导致其内部状态被更改。由于值类型不支持派生类型,因此我们不必担心派生类型会更改其字段。但我们需要注意常量类型中的可变引用类型字段。当我们为这样的类型实现构造器时,需要对其中的可变类型进行防御性的复制。下面的例子假设Phone为一个具有常量性的值类型,因为我们只关心值类型的常量性: // 下面的类型为状态的改变留下了漏洞。 public struct PhoneList { private readonly Phone[] _phones; public PhoneList( Phone[] ph ) { _phones = ph; } public IEnumerator Phones { get { return_phones.GetEnumerator(); } } } Phone[] phones = new Phone[10]; // 初始化phones PhoneList pl = new PhoneList( phones ); // 改变phones数组: // 同时也改变了常量类型的内部状态。 phones[5] = Phone.GeneratePhoneNumber( ); 我们知道,数组是一个引用类型。这意味着 PhoneList结构内部引用的数组和外部的phones数组引用着同一块内存空间。这样开发人员就有可能通过修改phones来修改常量结构 PhoneList。为了避免这种可能性,我们需要对数组做一个防御性的复制。上面的例子展示的是一个可变集合类型可能存在的漏洞。如果Phone为一个可变的引用类型,那么将更具危害性。在这种情况下,即使集合类型可以避免更改,集合中的值仍然可能会被更改。这时候,我们就需要对这样的类型在所有构造器中做防御性的复制了——事实上只要常量类型中存在任何可变的引用类型,我们都要这么做: // 常量类型: 构造时对可变的引用类型进行复制。 public struct PhoneList { private readonly Phone[] _phones; public PhoneList( Phone[] ph ) { _phones = new Phone[ph.Length ]; // 因为Phone是一个值类型,所以可以直接复制值。 ph.CopyTo(_phones, 0 ); } public IEnumerator Phones { get { return_phones.GetEnumerator(); } } } Phone[] phones = new Phone[10]; // 初始化phones PhoneList pl = new PhoneList( phones ); // 改变phones数组: // 不会改变pl中的副本。 phones[5] = Phone.GeneratePhoneNumber( ); 当要返回一个可变的引用类型时,我们也要遵循同样的规则。例如,如果我们要添加一个属性来从PhoneList结构中获取整个数组,那么其中的访问器也要创建一个防御性的复制。更多细节可参见条款23。 初始化常量类型通常有三种策略,选择哪一种策略依赖于一个类型的复杂度。定义一组合适的构造器通常是最简单的策略。例如,上述的Address结构就是通过定义一个构造器来负责初始化工作。 我们也可以创建一个工厂方法(factory method)来进行初始化工作。这种方式对于创建一些常用的值比较方便。.NET框架中的Color类型就采用了这种策略来初始化系统颜色。例如,静态方法Color.FromKnownColor()和Color.FromName()可以根据一个指定的系统颜色名,来返回一个对应的颜色值。 最后,对于需要多个步骤操作才能完整构造出一个常量类型的情况,我们可以通过创建一个可变的辅助类来解决。.NET中的String类就采用了这种策略,其辅助类为System.Text.StringBuilder。我们可以使用StringBuilder类通过多步操作来创建一个String对象。在执行完所有必要的操作后,我们便可以通过StringBuilder类来获取期望的String对象。 具有常量性的类型使得我们的代码更加易于编写和维护。我们不应该盲目地为类型中的每一个属性都创建get和set访问器。对于目的是存储数据的类型来说,我们应该尽可能地将它们实现为具有常量性和原子性的值类型。在这些类型的基础上,我们可以很容易地构建更复杂的结构。 |
|
来自: 兰亭文艺 > 《改善C#程序的50种方法》