改善C#程序的50种方法 条款7:将值类型尽可能实现为具有常量性和原子性的类型

转载 2007年09月21日 11:29:00

具有常量性的类型很简单,它们自创建后便保持不 变。如果在构造的时候就验证了参数的有效性,我们就可以确保从此之后它都处于有效的状态。因为我们不可能再更改其内部状态。通过禁止在构建对象之后更改对 象状态,我们实际上可以省却许多必要的错误检查。具有常量性的类型同时也是线程安全的:多个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 readonly string  _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,

    string line2,

    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访问器。对于目的是存储数据的类型来说,我们应该尽可能地将它们实现为具有常量性和原子性的值类型。在这些类型的基础上,我们可以很容易地构建更复杂的结构。

 

相关文章推荐

创建常量、原子性的值类型

概述 本文是《Effective C#》一书第七节的读书笔记。通过这篇文章,我主要想向大家说明一个我们平时可能不太会注意到的问题:创建具有常量性和原子性的值类型。 从类型设计谈起 从Class到...

long和double类型变量的非原子性

“深入java虚拟机”中提到,int等不大于32位的基本类型的操作都是原子操作,但是某些jvm对long和double类型的操作并不是原子操作,这样就会造成错误数据的出现。  错误数据出现的原因是:...

java int short long float double 类型描述以及原子性说明

在32位操作系统中,64位的long 和 double 变量由于会被JVM当作两个分离的32位来进行操作,所以不具有原子性,其他的都具有原子性。而使用AtomicLong能让long的操作保持原子型。...
  • mawming
  • mawming
  • 2015年04月08日 22:01
  • 896

改善C#程序的50种方法

为什么程序已经可以正常工作了,我们还要改变它们呢?答案就是我们可以让它们变得更好。我们常常会改变所使用的工具或者语言,因为新的工具或者语言更富生产力。如果固守旧有的习惯,我们将得不到期望的结果。对于C...

多线程程序中操作的原子性

04/15/2010并行编程原子操作多核多线程编程线程安全多线程程序中操作的原子性0. 背景原子操作就是不可再分的操作。在多线程程序中原子操作是一个非常重要的概念,它常常用来实现一些同步机制,同时也是...

多线程程序中操作的原子性

多线程程序中操作的原子性 0. 背景 原子操作就是不可再分的操作。在多线程程序中原子操作是一个非常重要的概念,它常常用来实现一些同步机制,同时也是一些常见的多线程Bug的源头。本文...

PHP程序的原子性和PHP的文件锁

PHP程序的原子性和PHP的文件锁

多线程程序中操作的原子性

0. 背景 原子操作就是不可再分的操作。在多线程程序中原子操作是一个非常重要的概念,它常常用来实现一些同步机制,同时也是一些常见的多线程Bug的源头。本文主要讨论了三个问题:1. 多线程程序中对变量...

《Effective C#:改善C#程序的50种方法》读书笔记

《Effective C#中文版:改善C#程序的50种方法》读书笔记

改善C#程序,提高程序运行效率的50种方法

改善C#程序,提高程序运行效率的50种方法 转自:http://blog.sina.com.cn/s/blog_6f7a7fb501017p8a.html一、用属性代替可访问的字段   1、.NE...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:改善C#程序的50种方法 条款7:将值类型尽可能实现为具有常量性和原子性的类型
举报原因:
原因补充:

(最多只允许输入30个字)