技术掘金,专注于Web互联网开发,分享技术,致力于打造好的技术博客
Effective C# 原则24：选择申明式编程而不是命令式编程
与命令式编程相比，申明式编程可以用更简单，更清楚的方法来描述软件的行为。申明式编程就是说用申明来定义程序的行为，而不是写一些指令。在C\#里，也和其它大多数语言一样，你的大多数程序都是命令式的：在程序中写一个方法来定义行为。在C\#中，你在编程时使用特性就是申明式编程。你添加一个特性到类，属性，数据成员，或者是方法上，然后.Net运行时就会为你添加一些行为。这样申明的目的就是简单易用，而且易于阅读和维护。让我们以一个你已经使用过的例子开始。当你写你的第一个ASP.NetWeb服务时，向导会生成这样的代码：[WebMethod] public string HelloWorld() { return "Hello World"; } VS.net的Web服务向导添加了[WebMethod]特性到HelloWorld()方法上，这就定义了HelloWorld是一个web方法。ASP.net运行时会为你生成代码来响应这个特性。运行时生成的Web服务描述语言(WSDL)文档，也就是包含了对SOAP进行描述的文档，调用HelloWorld方法。ASP.net也支持运行时发送SOAP请求HelloWorld方法。另外，ASP.net运行时动态的生成HTML面页，这样可以让你在IE里测试你的新Web服务。而这些全部是前面的WebMethod特性所响应的。这个特性申明了你的意图，而且运行时确保它是被支持的。使用特性省了你不少时间，而且错误也少了。这并不是一个神话，ASP.net运行时使用反射来断定类里的哪些方法是web服务，当它们发现这些方法时，ASP.net运行时就添加一些必须的框架代码到这些方法上，从而使任何添加了这些代码的方法成为web方法。[WebMethod]特性只是.Net类库众多特性之一，这些特性可能帮助你更快的创建正确的程序。有一些特性帮助你创建序列化类型(参见原则25)。正如你在原则4里看到的，特性可以控制条件编译。在这种情况以下其它一些情况下，你可以使用申明式编程写出你所要的更快，更少错误的代码。你应该使用.Net框架里自带的一些特性来申明你的意图，这比你自己写要好。因为这样花的时间少，更简单，而且编译器也不会出现错误。如果预置的特性不适合你的需求，你也可以通过定义自己的特性和使用反射来使用申明式编程结构。做为一个例子，你可以创建一个特性，然而关联到代码上，让用户可以使用这个特性来创建默认可以排序的类型。一个例子演示了如何添加这个特性，该特性定义了你想如何在一个客户集合中排序：[DefaultSort( "Name" )] public class Customer { public string Name { get { return _name; } set { _name = value; } } public decimal CurrentBalance { get { return _balance; } } public decimal AccountValue { get { return calculateValueOfAccount(); } } } DefaultSort特性，Nane属性，这就暗示了任何Customer的集合应该以客户名字进行排序。DefaultSort特性不是.Net框架的一部份，为了实现它，你创建一个DefaultSortAttribute类：[AttributeUsage( AttributeTargets.Class | AttributeTargets.Struct )] public class DefaultSortAttribute : System.Attribute { private string _name; public string Name { get { return _name; } set { _name = value; } } public DefaultSortAttribute( string name ) { _name = name; } } 同样，你还必须写一些代码，来对一个集合运行排序，而该集合中的元素是添加了DefaultSort特性的对象。你将用到反射来发现正确的属性，然后比较两个不同对象的属性值。一个好消息是你只用写一次这样的代码。下一步，你要写一个实现了IComparer接口的类。(在原则26中会详细的充分讨论比较。)ICompare有一个CompareTo()方法来比较两个给定类型的对象，把特性放在实现了IComparable的类上，就可以定义排序顺序了。构造函数对于通用的比较，可以发现默认的排序属性标记，而这个标记是基于已经比较过的类型。Compare方法对任何类型的两个对象进行排序，使用默认的排序属性：internal class GenericComparer : IComparer { // Information about the default property: private readonly PropertyDescriptor _sortProp; // Ascending or descending. private readonly bool _reverse = false; // Construct for a type public GenericComparer( Type t ) : this( t, false ) { } // Construct for a type // and a direction public GenericComparer( Type t, bool reverse ) { _reverse = reverse; // find the attribute, // and the name of the sort property: // Get the default sort attributes on the type: object [] a = t.GetCustomAttributes( typeof( DefaultSortAttribute ),false ); // Get the PropertyDescriptor for that property: if ( a.Length > 0 ) { DefaultSortAttribute sortName = a[ 0 ] as DefaultSortAttribute; string name = sortName.Name; // Initialize the sort property: PropertyDescriptorCollection props = TypeDescriptor.GetProperties( t ); if ( props.Count > 0 ) { foreach ( PropertyDescriptor p in props ) { if ( p.Name == name ) { // Found the default sort property: _sortProp = p; break; } } } } } // Compare method. int IComparer.Compare( object left, object right ) { // null is less than any real object: if (( left == null ) && ( right == null )) return 0; if ( left == null ) return -1; if ( right == null ) return 1; if ( _sortProp == null ) { return 0; } // Get the sort property from each object: IComparable lField = _sortProp.GetValue( left ) as IComparable; IComparable rField = _sortProp.GetValue( right ) as IComparable; int rVal = 0; if ( lField == null ) if ( rField == null ) return 0; else return -1; rVal = lField.CompareTo( rField ); return ( _reverse ) ? -rVal : rVal; } }这个通用的比较对任何Customers集合可以进行排序，而这个Customers是用DefaultSort特性申明了的：CustomerList.Sort(new GenericComparer(typeof( Customer ))); 实现GenericComparer的代码利用了一些高级的技术，使用反射(参见原则43)。但你必须写一遍这样的代码。从这个观点上看，你所要做的就是添加空上属性到其它任何类上，然而你就可以对这些对象的集合进行能用的排序了。如果你修改了DefaultSort特性的参数，你就要修改类的行为。而不用修改所有的算法。这种申明式习惯是很有用的，当一个简单的申明可以说明你的意图时，它可以帮助你避免重复的代码。再参考GenericComparer类，你应该可以为你创建的任何类型，写一个不同的(而且是是直接了当的)排序算法。这种申明式编程的好处就是你只用写一次能用的类型，然后就可以用一个简单的申明为每个类型创建行为。关键是行为的改变是基于单个申明的，不是基于任何算法的。GenericComparer可以在任何用DefaultSort特性修饰了的类型上工作，如果你只须要在程序里使用一两次排序功能，就按常规简单的方法写吧。然而，如果你的程序对于同样的行为，可能须要在几十个类型上实现，那么能用的算法以及申明式的解决方案会省下很多时间，而且在长时间的运行中也是很有力的。你不应该为WebMethod特性写代全部的代码，你应该把这一技术展开在你自己的算法上。原则42里讨论了一个例子：如何使用特性来建立一个附加命令句柄。其它的例子可能还包括一些在定义附加包建立动态的webUI面页时的其它内容。申明式编程是一个很有力的工具，当你可以使用特性来表明你的意图时，你可以通过使用特性，来减少在大量类似的手写算法中出现逻辑错误的可能。申明式编程创建了更易于阅读，清晰的代码。这也就意味着不管是现在还是将来，都会少出现错误。如果你可以使用.Net框架里定义的特性，那就直接使用。如果不能，考虑选择创建你自己的特性，这样你可以在将来使用它来创建同样的行为。
你已经知道，所谓的只读属性就是指调用者无法修改这个属性。不幸运的是，这并不是一直有效的。如果你创建了一个属性，它返回一个引用类型，那么调用者就可以访问这个对象的公共成员，也包括修改这些属性的状态。例如：public class MyBusinessObject { // Read Only property providing access to a // private data member: private DataSet _ds; public DataSet Data { get { return _ds; } } } // Access the dataset: DataSet ds = bizObj.Data; // Not intended, but allowed: ds.Tables.Clear( ); // Deletes all data tables. 任何MyBusinessObject的公共客户都可以修改你的内部dateset。你创建的属性用来隐藏类的内部数据结构，你提供了方法，让知道该方法的客户熟练的操作数据。因此，你的类可以管理内部状态的任何改变。然而，只读属性对于类的封装来说开了一个后门。当你考虑这些问题时，它并不是一个可读可写属性，而是一个只读属性。欢迎来到一个精彩的基于引用的系统，任何返回引用的成员都会返回一个对象的句柄。你给了调用者一个接口的句柄，因此调用者修改这个对象的某个内部引用时，不再需要通过这个对象。很清楚，你想防止这样的事情发生。你为你的类创建了一个接口，同时希望用户使用这个接口。你不希望用户在不明白你的意图时，访问并修改对象的内部状态。你有四个策略来保护你的内部数据结构不被无意的修改：值类型，恒定类型，接口和包装(模式)。值类型在通过属性访问时，是数据的拷贝。客户对类的拷贝数据所做的任何修改，不会影响到对象的内部状态。客户可以根据需求随意的修改拷贝的数据。这对你的内部状态没有任意影响。恒定类型，例如System.String，也是安全的。你可以返回一个字符串，或者其它恒定类型。恒定类型的安全性告诉你，没有客户可以修改字符串。你的内部状态是安全的。第三个选择就是定义接口，从而充许客户访问内部成员的部份功能(参见原则19)。当你创建一个自己的类时，你可以创建一些设置接口，用来支持对类的子对象进行设置。通过这些接口来暴露一些功能函数，你可以尽可能的减少一些对数据的无意修改。客户可以通过你提供的接口访问类的内部对象，而这个接口并不包含这个类的全部的功能。在DataSet上暴露一个IListsource接口就是这种策略，可以阻止一些有想法的程序员来猜测实现这个接口的对象，以及强制转换。这样做和程序员付出更多的工作以及发现更多的BUG都是自找的(译注：这一句理解可能完全不对，读者可以自行参考原文：Butprogrammers who go to that much work to create bugs get what theydeserve.)。System.Dataset类同时也使用了最后一种策略：包装对象。DataViewManager类提供了一种访问DataSet的方法，而且防止变向的方法来访问DataSeto类：public class MyBusinessObject { // Read Only property providing access to a // private data member: private DataSet _ds; public DataView this[ string tableName ] { get { return _ds.DefaultViewManager. CreateDataView( _ds.Tables[ tableName ] ); } } } // Access the dataset: DataView list = bizObj[ "customers" ]; foreach ( DataRowView r in list ) Console.WriteLine( r[ "name" ] ); DataViewManager创建DataView来访问DataSet里的个别数据表。DataViewManager没有提供任何方法来修改DataSet里的数据表。每一个DataView可以被配置为许可修改个别数据元素，但客户不能修改数据表，或者数据表的列。读/写是默认的，因此客户还是可以添加，修改，或者删除个别的数据条目。在我们开始讨论如何创建一个完全只读的数据视图时以前，让我先简单的了解一下你应该如何响应公共用户的修改。这是很重要的，因为你可能经常要暴露一个DataView给UI控件，这样用户就可以编辑数据(参见原则38)。确信你已经使用过Windows表单的数据绑定，用来给用户提供对象私有数据编辑。DataSet里的DataTable引发一些事件，这样就可以很容易的实现观查者模式：你的类可以响应其它客户的任何修改。DataSet里的DataTable对象会在数据表的任何列以及行发生改变时引发事件。ColumnChanging和RowChanging事件会在编辑的数据提交到DataSet前被引发。而ColumnChanged和RowChanged事件则是在修改提交后引发。任何时候，当你期望给公共客户提供修改内部数据的方法时，都可以扩展这样的技术，但你要验证而且响应这些改变。你的类应该对内部数据结构产生的事件做一些描述。事件句柄通过更新这些内部的状态来验证和响应改变。回到原来的问题上，你想让客户查看你的数据，但不许做任何的修改。当你的数据存储在一个DataSet里时，你可以通过强制在DataTable上创建一个DataView来防止任何的修改。DataView类包含一些属性，通过定义这些属性，可以让DataView支持在实际的表上添加，删除，修改甚至是排序。你可以在被请求的DataTable上使用索引器，通过创建一个索引器来返回一个自定义的DataView：public class MyBusinessObject { // Read Only property providing access to a // private data member: private DataSet _ds; public IList this[ string tableName ] { get { DataView view = _ds.DefaultViewManager.CreateDataView ( _ds.Tables[ tableName ] ); view.AllowNew = false; view.AllowDelete = false; view.AllowEdit = false; return view; } } } // Access the dataset: IList dv = bizOjb[ "customers" ]; foreach ( DataRowView r in dv ) Console.WriteLine( r[ "name" ] ); 这个类的最后一点摘录(的代码)通过访问IList接口引用，返回这个实际数据表上的视图。你可以在任何的集合上使用IList接口，并不仅限于DataSet。你不应该只是简单的返回DataView对象。用户可以再次简单的取得编辑，添加/删除的能力。你返回的视图已经是自定义的，它不许可在列表的对象上做任何的修改。返回的IList指针确保客户没有像DataView对象里赋于的修改权利。从公共接口上暴露给用户的引用类型，可以让用户修改对象内部成员，而不用访问该对象。这看上去不可思议，也会产生一些错误。你须要修改类的接口，重新考虑你所暴露的是引用而不是值类型。如果你只是简单的返回内部数据，你就给了别人机会去访问内部成员。你的客户可以调用成员上任何可用的方法。你可以通过暴露接口来限制一些内部私有数据访问，或者包装对象。当你希望你的客户可以修改你的内部数据时，你应该实现你自己的观察者模式，这样你的对象可以验证修改或者响应它们。
可以用事件给你的类型定义一些外部接口。事件是基于委托的，因为委托可以提供类型安全的函数签名到事件句柄上。加上大多数委托的例子都是使用事件来说明的，以至于开发人员一开始都认为委托与事件是一回事。在原则21里，我已经展示了一些不在事件上使用委托的例子。在你的类型与其它多个客户进行通信时，为了完成它们的行为，你必须引发事件。一个简单的例子，你正在做一个日志类，就像一个信息发布机一样在应用程序里发布所有的消息。它接受所有从程序源发布的消息，并且把这些消息发布到感兴趣的听众那里。这些听众可以是控制台，数据库，系统日志，或者是其它的机制。就可以定义一个像下面这样的类，当消息到达时来引发事件：public class LoggerEventArgs : EventArgs { public readonly string Message; public readonly int Priority; public LoggerEventArgs ( int p, string m ) { Priority = p; Message = m; } } // Define the signature for the event handler: public delegate void AddMessageEventHandler(object sender, LoggerEventArgs msg); public class Logger { static Logger( ) { _theOnly = new Logger( ); } private Logger( ) { } private static Logger _theOnly = null; public Logger Singleton { get { return _theOnly; } } // Define the event: public event AddMessageEventHandler Log; // add a message, and log it. public void AddMsg ( int priority, string msg ) { // This idiom discussed below. AddMessageEventHandler l = Log; if ( l != null ) l ( null, new LoggerEventArgs( priority, msg ) ); } }AddMsg方法演示了一个恰当的方法来引发事件。临时的日志句柄变量是很重要的，它可以确保在各种多线程的情况下，日志句柄也是安全的。如果没有这个引用的COPY，用户就有可能在if检测语句和正式执行事件句柄之间移除事件句柄。有了引用COPY，这样的事情就不会发生了。我还定义了一个LoggerEventArgs来保存事件和消息的优先级。委托定义了事件句柄的签名。而在Logger类的内部，事件字段定义了事件的句柄。编译器会认为事件是公共的字段，而且会为你添加Add和Remove两个操作。生成的代码与你这样手写的是一样的：public class Logger { private AddMessageEventHandler _Log; public event AddMessageEventHandler Log { add { _Log = _Log + value; } remove { _Log = _Log - value; } } public void AddMsg (int priority, string msg) { AddMessageEventHandler l = _Log; if (l != null) l (null, new LoggerEventArgs (priority, msg)); } } }C\#编译器创建Add和Remove操作来访问事件。看到了吗，公共的事件定义语言很简洁，易于阅读和维护，而且更准确。当你在类中添加一个事件时，你就让编译器可以创建添加和移除属性。你可以，而且也应该，在有原则要强制添加时自己手动的写这些句柄。事件不必知道可能成为监听者的任何资料，下面这个类自动把所有的消息发送到标准的错误设备(控制台)上：class ConsoleLogger { static ConsoleLogger() { logger.Log += new AddMessageEventHandler( Logger_Log ); } private static void Logger_Log( object sender, LoggerEventArgs msg ) { Console.Error.WriteLine( "{0}:\t{1}", msg.Priority.ToString(), msg.Message ); } }另一个类可以直接输出到系统事件日志：class EventLogger { private static string eventSource; private static EventLog logDest; static EventLogger() { logger.Log +=new AddMessageEventHandler( Event_Log ); } public static string EventSource { get { return eventSource; } set { eventSource = value; if ( ! EventLog.SourceExists( eventSource ) ) EventLog.CreateEventSource( eventSource, "ApplicationEventLogger" ); if ( logDest != null ) logDest.Dispose( ); logDest = new EventLog( ); logDest.Source = eventSource; } } private static void Event_Log( object sender, LoggerEventArgs msg ) { if ( logDest != null ) logDest.WriteEntry( msg.Message, EventLogEntryType.Information, msg.Priority ); } }事件会在发生一些事情时，通知任意多个对消息感兴趣的客户。Logger类不必预先知道任何对消息感兴趣的对象。Logger类只包含一个事件。大多数windows控件有很多事件，在这种情况下，为每一个事件添加一个字段并不是一个可以接受的方法。在某些情况下，一个程序中只实际上只定义了少量的事件。当你遇到这种情况时，你可以修改设计，只有在运行时须要事件时在创建它。(译注：作者的一个明显相思就是，当他想说什么好时，就决不会，或者很少说这个事情的负面影响。其实事件对性能的影响是很大的，应该尽量少用。事件给我们带来的好处是很多的，但不要海滥用事件。作者在这里没有明说事件的负面影响。)扩展的Logger类有一个System.ComponentModel.EventHandlerList容器，它存储了在给定系统中应该引发的事件对象。更新的AddMsg()方法现在带一个参数，它可以详细的指示子系统日志的消息。如果子系统有任何的监听者，事件就被引发。同样，如果事件的监听者在所有感兴趣的消息上监听，它同样会被引发：public class Logger { private static System.ComponentModel.EventHandlerList Handlers = new System.ComponentModel.EventHandlerList(); static public void AddLogger( string system, AddMessageEventHandler ev ) { Handlers[ system ] = ev; } static public void RemoveLogger( string system ) { Handlers[ system ] = null; } static public void AddMsg ( string system, int priority, string msg ) { if ( ( system != null ) && ( system.Length > 0 ) ) { AddMessageEventHandler l = Handlers[ system ] as AddMessageEventHandler; LoggerEventArgs args = new LoggerEventArgs( priority, msg ); if ( l != null ) l ( null, args ); // The empty string means receive all messages: l = Handlers[ "" ] as AddMessageEventHandler; if ( l != null ) l( null, args ); } } }这个新的例子在EventHandlerList集合中存储了个别的事件句柄，客户代码添加到特殊的子系统中，而且新的事件对象被创建。然后同样的子系统需要时，取回同样的事件对象。如果你开发一个类包含有大量的事件实例，你应该考虑使用事件句柄集合。当客户附加事件句柄时，你可以选择创建事件成员。在.Net框架内部，System.Windows.Forms.Control类对事件使用了一个复杂且变向的实现，从而隐藏了复杂的事件成员字段。每一个事件字段在内部是通过访问集合来添加和移除实际的句柄。关于C\#语言的这一特殊习惯，你可以在原则49中发现更多的信息。你用事件在类上定义了一个外接的接口：任意数量的客户可以添加句柄到事件上，而且处理它们。这些对象在编译时不必知道是谁。事件系统也不必知道详细就可以合理的使用它们。在C\#中事件可以减弱消息的发送者和可能的消息接受者之间的关系，发送者可以设计成与接受者无关。事件是类型把动作信息发布出去的标准方法。
我：“儿子，到院子里除草去，我要看会书。” 斯科特：“爸，我已经打扫过院子了。” 斯科特：“爸，我已经把草放在除草机上了。” 斯科特：“爸，除草机不能启动了。” 我：“让我来启动它。” 斯科特：“爸，我做好了。”这个简单的交互展示了回调。我给了我儿子一个任务，并且他可以报告状态来(重复的)打断我。而当我在等待他完成任务的每一个部份时，我不用阻塞我自己的进程。他可以在有重要(或者事件)状态报告时，可以定时的打断我，或者向我询求帮助。回调就是用于异步的提供服务器与客户之间的信息反馈。它们可能在多线程中，或者可能是简单的提供一个同步更新点。在C\#里是用委托来表示回调的。委托提供了一个类型安全的回调定义。尽管委托大多数是为事件使用的，但这不应该是C\#语言中唯一使用这一功能的地方。任何时候，如果你想在两个类之间进行通信，而你又期望比使用接口有更少的偶合性，那么委托是你正确的选择。委托可以让你在运行确定(回调)目标并且通知用户。委托就是包含了某些方法的引用。这些方法可以是静态方法，也可以是实例方法。使用委托，你可以在运行时确定与一个或者多个客户对象进行交互。多播委托包含了添加在这个委托上的所有单个函数调用。有两点要注意的：它不是异常安全的，并且返回值总是委托上最后一个函数调用后返回的值。在多播委托调用的内部，每一个目标都会成功的调用。委托不会捕获任何的异常，也就是说，在委托链中抛出的任何异常都会终止委托链的继续调用。在返回值上也存在一个简单的问题。你可以定义委托有返回值或者是void。你可能会写一个回调函数来检测用户的异常中断：public delegate bool ContinueProcessing(); public void LengthyOperation( ContinueProcessing pred ) { foreach( ComplicatedClass cl in _container ) { cl.DoLengthyOperation(); // Check for user abort: if (false == pred()) return; } } 在单委托上这是工作的，但在多播委托上却是有问题的：ContinueProcessing cp = new ContinueProcessing (CheckWithUser); cp += new ContinueProcessing(CheckWithSystem); c.LengthyOperation( cp ); 从委托的调用上返回的值，其实是它的最后一个函数的调用上返回的值。其它所有的的返回值都被忽略。即，从CheckWithUser()返回的断言被忽略。你可以自己手动的设置两个委托来调用两个函数。你所创建的每一个委托都包含有一个委托链。直接检测这个委托链，并自己调用每一个委托：public delegate bool ContinueProcessing(); public void LengthyOperation( ContinueProcessing pred ) { bool bContinue = true; foreach( ComplicatedClass cl in _container ) { cl.DoLengthyOperation(); foreach( ContinueProcessing pr in pred.GetInvocationList( )) bContinue &= pr(); if (false == bContinue) return; } } 这时，我已经定义好了程序的语义，因此委托链上的每个委托必须返回真以后，才能继续调用。委托为运行时回调提供了最好的方法，用户简单的实现用户对类的需求。你可以在运行时确定委托的目标。你可以支持多个用户目标，这样，用户的回调就可以用.Net里的委托实现了。
Effective C# 原则20：明辨接口实现和虚函数重载的区别
粗略的看一下，感觉实现接口和虚函数重载是一样的。你定义了一些对象，但是这些对象是在另一个类型里申明的。你被第一感觉骗了，实现接口与虚函数重载是完全不同的。在接口里定义的成员默认情况下，是根本不存在实际内容的。派生类不能重载基类中的接口成员。接口可以隐式的实现，就是把它们从类的公共接口中隐藏。它们的概念是不同的而且使用也是不同的。但你可以这样的实现接口：让你的派生类可以修改你的实现。你只用对派生类做一个Hook就行了。(译注：相信写过C++程序的人就知道hook是什么意思，而且我也实在想不到把hook译成什么比较好，所以就直接用hook这个原词了，就像bug一样。)为了展示它们的不同之处，试着做一个简单的接口以及在一个类中实现它：interface IMsg { void Message(); } public class MyClass : IMsg { public void Message() { Console.WriteLine( "MyClass" ); } } Message()方法是MyClass的公共接口，Message同样可以用一个接口指针IMsg来访问。现在让我们来一点繁杂的，添加一个派生类：public class MyDerivedClass : MyClass { public new void Message() { Console.WriteLine( "MyDerivedClass" ); } } 注意到，我添加了一个关键字new在Message方法上，用于区别前面的一个Message(参见原则29)。MyClass.Message()不是虚函数，派生类可以不提供重载版本。MyDerived类创建了一个新的Message方法，但这个方法并不是重载MyClass.Message:它隐藏了原来的方法。而且，MyClass.Message还是可以通过IMsg的引用来访问：MyDerivedClass d = new MyDerivedClass( ); d.Message( ); // prints "MyDerivedClass". IMsg m = d as IMsg; m.Message( ); // prints "MyClass" 接口方法不是虚的，当你实现一个接口时，你就要在详细的相关类型中申明具体的实现内容。但你可能想要创建接口，在基类中实现这些接口而且在派生类中修改它们的行为。这是可以办法到的。你有两个选择，如果不访问基类，你可以在派生类中重新实现这个接口：public class MyDerivedClass : MyClass, IMsg { public new void Message() { Console.WriteLine( "MyDerivedClass" ); } } 添加的IMsg让你的派生类的行为发生了改变，以至IMsg.Message现在是在派生类上使用的：MyDerivedClass d = new MyDerivedClass( ); d.Message( ); // prints "MyDerivedClass". IMsg m = d as IMsg; m.Message( ); // prints "MyDerivedClass" 派生类上还是须要在MyDerivedClass.Message()方法上添加关键字new，这还是有一点隐患(参见原则29)。基类还是可以通过接口引用来访问：MyDerivedClass d = new MyDerivedClass( ); d.Message( ); // prints "MyDerivedClass". IMsg m = d as IMsg; m.Message( ); // prints "MyDerivedClass" MyClass b = d; b.Message( ); // prints "MyClass" 唯一可以修正这个问题的方法是修改基类，把接口的申明修改为虚函数：public class MyClass : IMsg { public virtual void Message() { Console.WriteLine( "MyClass" ); } } public class MyDerivedClass : MyClass { public override void Message() { Console.WriteLine( "MyDerivedClass" ); } } MyDerivedClass以及其它所有从MyClass派生的类可以申明它们自己的Message()方法。这个重载的版本每次都会调用：通过MyDerivedClass的引用，通过IMsg接口的引用，或者直接通过MyClass的引用。如果你不喜欢混杂的虚函数概念，那就对MyClass的定义做一个小的修改：public abstract class MyClass, IMsg { public abstract void Message(); } 是的，你可以用一个抽象方法来实现一个接口。通过申明一个接口内的抽象的方法，你可以让你的所有派生都必须实现这个接口。现在，IMsg接口成为了MyClass的一个组成部份，你的每一个派生类都必须实现它。隐式接口实现，可以让你在一个类上隐藏公共的接口成员方法，而且也实现了这个接口。它在实现接口和虚函数重载上绕了几个圈。当有多个合适的函数版本时，你可以利用隐式接口的实现来限制用户的编码。在原则26讲到的IComparable习惯会详细的讨论这一点。实现接口让我们有更多的选择，用于创建和重载虚函数。你可以创建隐秘的实现，虚的实现，或者抽象关联到派生类。你可以精确的决定，你的派生类如何以及何时，修改接口的默认实现。接口方法不是虚方法，而是一个独立的约定!
抽象类在类的继承中提供了一个常规的“祖先”。一个接口描述了一个可以被其它类型实现的原子级泛型功能。各有千秋，却也不尽相同。接口是一种合约式设计：一个类型实现了某个接口的类型，就必须实现某些期望的方法。抽象类则是为一个相关类的集合提供常规的抽象方法。这些都是老套的东西了：它是这样的，继承就是说它是某物(isa,)，而接口就是说它有某个功能(behaves like.)!这些陈词滥调已经说了好久了，因为它们提供了说明，同时在两个结构上描述它们的不同：基类是描述对象是什么，接口描述对象有某种行为。接口描述了一组功能集合，或者是一个合约。你可以在接口里创建任何的占位元素(placeholder，译注：就是指先定义，后面再实现的一些内容)：方法，属性，索引器以及事件。任何实现类型这个接口的类型必须为接口里的每个元素提供具体的内容。你必须实现所有的方法，提供全部属性访问器，索引器，以及定义接口里的所有事件。你在接口里标记并且构造了可重用的行为。你可以把接口当成参数或者返回值，你也可以有更多的机会重用代码，因为不同的类型可以实现相同的接口。更多的是，比起从你创建的基类派生，开发人员可以更容易的实现接口。(译注：不见得!)你不能在接口里提供任何成员的具体实现，无论是什么，接口里面都不能实现。并且接口也不能包含任何具体的数据成员。你是在定义一个合约，所有实现接口的类型都应该实现的合约。抽象的基类可以为派生类提供一些具体的实现，另外也描述了一些公共的行为。你可以更详细的说明数据成员，具体方法，实现虚函数，属性，事件以及索引器。一个基类可以只提供部份方法的实现，从而只提供一些公共的可重用的具体实现。抽象类的元素可以是虚的，抽象的，或者是非虚的。一个抽象类可以为具体的行为提供一个可行的实现，而接口则不行。重用这些实现还有另一个好处：如果你在基类中添加一个方法，所有派生类会自动隐式的增加了这个方法。这就是说，基类提供了一个有效的方法，可以随时扩展几个(派生)类型的行为：就是向基类添加并实现方法，所有派生类会立即具有这些行为。而向一个接口添加一个方法，所会破坏所有原先实现了这个接口的类。这些类不会包含新的方法，而且再也通不过编译。所有的实现者都必须更新，要添加新的方法。这两个模式可以混合并重用一些实现代码，同时还可以实现多个接口。System.Collections.CollectionBase就是这样的一个例子，它个类提供了一个基类。你可以用这个基类你的客户提供一些.Net缺少的安全集合。例如，它已经为你实现了几个接口：IList,ICollection,和IEnumerable。另外，它提供了一个受保护的方法，你可以重载它，从而为不同的使用情况提供自己定义的行为。IList接口包含向集合中添加新对象的Insert()方法。想自己更好的提供一个Insert方法的实现，你可以通过重载CollectionBase类的OnInsert()或OnInsertCcomplete()虚方法来处理这些事件：public class IntList : System.Collections.CollectionBase { protected override void OnInsert( int index, object value ) { try { int newValue = System.Convert.ToInt32( value ); Console.WriteLine( "Inserting {0} at position {1}", index.ToString(), value.ToString()); Console.WriteLine( "List Contains {0} items", this.List.Count.ToString()); } catch( FormatException e ) { throw new ArgumentException( "Argument Type not an integer", "value", e ); } } protected override void OnInsertComplete( int index, object value ) { Console.WriteLine( "Inserted {0} at position {1}", index.ToString( ), value.ToString( )); Console.WriteLine( "List Contains {0} items", this.List.Count.ToString( ) ); } } public class MainProgram { public static void Main() { IntList l = new IntList(); IList il = l as IList; il.Insert( 0,3 ); il.Insert( 0, "This is bad" ); } } 前面的代码创建了一个整型的数组链表，而且使用IList接口指针添加了两个不同的值到集合中。通过重载OnInsert()方法，IntList类在添加类型时会检测类型，如果不是一个整数时，会抛出一个异常。基类给你实现了默认的方法，而且给我们提供了机会在我们自己的类中实现详细的行为。CollectionBase这个基类提供的一些实现可以直接在你的类中使用。你几乎不用写太多的代码，因为你可以利用它提供的公共实现。但IntList的公共API是通过CollectionBase实现接口而来的：IEnumerable,ICollection和IList接口。CollectionBase实现了你可以直接使用的接口。现在我们来讨论用接口来做参数和返回值。一个接口可以被任意多个不相关的类型实现。比起在基类中编码，实现接口的编码可以在开发人员中提供更强的伸缩性。因为.Net环境中强制使用单继承的，这使得实现接口这一方法显得很重要。下面两个方法完成了同样的任务：public void PrintCollection( IEnumerable collection ) { foreach( object o in collection ) Console.WriteLine( "Collection contains {0}", o.ToString( ) ); } public void PrintCollection( CollectionBase collection ) { foreach( object o in collection ) Console.WriteLine( "Collection contains {0}", o.ToString( ) ); } 第二个方法的重用性远不及第一个。Array，ArrayList，DataTable，HashTable，ImageList或者很多其它的集合类无法使用第二个方法。让方法的参数使用接口，可以让程序具有通用性，而且更容易重用。用接口为类定义API函数同样可以取得很好的伸缩性。例如，很多应用程序使用DataSet与你的应用程序进行数据交换。假设这一交流方法是不变的，那这太容易实现了：public DataSet TheCollection { get { return _dataSetCollection; } } 然而这让你在将来很容易遇到问题。某些情况下，你可能从使用DataSet改为暴露一个DataTable，或者是使用DataView，甚至是使用你自己定义的对象。任何的改变都会破坏这些代码。当然，你可能会改变参数类型，但这会改变你的类的公共接口。修改一个类的公共接口意味着你要在一个大的系统中修改更多的内容；你须要修改所有访问这个公共接口的地方。紧接着的第二个问题麻烦问题就是：DataSet类提供了许多方法来修改它所包含的数据。类的用户可能会删除表，修改列，甚至是取代DataSet中的所有对象。几乎可以肯定这不是你想要的。幸运的是，你可以对用户限制类的使用功能。不返回一个DataSet的引用，你就须要返回一个期望用户使用的接口。DataSet支持IListSource接口，它用于数据绑定：using System.ComponentModel; public IListSource TheCollection { get { return _dataSetCollection as IListSource; } } IListSource让用户通过GetList()方法来访问内容，它同时还有ContainsListCollection属性，因此用户可以修改全部的集合结构。使用IListSource接口，在DataSet里的个别对象可以被访问，但DataSet的所有结构不能被修改。同样，调用者不能使用DataSet的方法来修改可用的行为，从而在数据上移动约束或者添加功能。当你的类型以类的方式暴露一些属性时，它就暴露了这个类的全部接口。使用接口，你可以选择只暴露一部分你想提供给用户使用的方法和属性。以前在类上实现接口的详细内容，在后来是可以修改的(参见原则23)。另外，不相关的类型可以实现同样的接口。假设你在创建一个应用程序。用于管理雇员，客户和卖主。他们都不相关，至少不存在继承关系。但他们却共享着某些功能。他们都有名字，而且很有可能要在一些Windows控件中显示他们的名字：public class Employee { public string Name { get { return string.Format( "{0}, {1}", _last, _first ); } } // other details elided. } public class Customer { public string Name { get { return _customerName; } } // other details elided } public class Vendor { public string Name { get { return _vendorName; } } } Eyployee,Customer和Vendor类不应该共享一个基类。但它们共享一些属性：姓名(正如前面显示的那样)，地址，以及联系电话。你应该在一个接口中创建这些属性：public interface IContactInfo { string Name { get; } PhoneNumber PrimaryContact { get; } PhoneNumber Fax { get; } Address PrimaryAddress { get; } } public class Employee : IContactInfo { // implementation deleted. } 对于不的类型使用一些通用的功能，接口可以简化你的编程任务。Customer,Employee,和Vendor使用一些相同的功能，但这只是因为你把它们放在了接口上。使用接口同样意味着在一些意外情况下，你可以减少结构类型拆箱的损失。当你把一个结构放到一个箱中时，这个箱可以实现结构上的所有接口。当你用接口指针来访问这个结构时，你不用结构进行拆箱就可以直接访问它。这有一个例子，假设这个结构定义了一个链接和一些说明：public struct URLInfo : IComparable { private string URL; private string description; public int CompareTo( object o ) { if (o is URLInfo) { URLInfo other = ( URLInfo ) o; return CompareTo( other ); } else throw new ArgumentException( "Compared object is not URLInfo" ); } public int CompareTo( URLInfo other ) { return URL.CompareTo( other.URL ); } } 你可以为URLInfo的对象创建一个有序表，因为URLInfo实现了IComparable接口。URLInfo结构会在添加到链表中时被装箱，但Sort()方法不须要拆箱就可以调用对象的CompareTo()方法。你还须要对参数(other)进行拆箱，但你在调用IComparable.CompareTo()方法时不必对左边的对象进行拆箱。基类可以用来描述和实现一些具体的相关类型的行为。接口则是描述一些原子级别的功能块，不相关的具体类型都可以实现它。接口以功能块的方法来描述这些对象的行为。如果你明白它们的不同之处，你就可以创建出表达力更强的设计，并且它们面对修改是有很加强的伸缩性的。类的继承可以用来定义一些相关类型。通过实现一些接口来暴露部份功能来访问这些类型。
我们已经讨论过，处理一个占用了非托管资源对象是很重要的。现在是时候来讨论如何写代码来管理这些类占用的非内存资源了。一个标准的模式就是利用.Net框架提供的方法处理非内存资源。你的用户也希望你遵守这个标准的模式。也就是通过实现IDisposable接口来释放非托管的资源，当然是在用户记得调用它的时候，但如果用户忘记了，析构函数也会被动的执行。它是和垃圾回收器一起工作的，确保在一些必要时候，你的对象只会受到因析构函数而造成的性能损失。这正是管理非托管资源的好方法，因此有必要彻底的弄明白它。处在类继承关系中顶层的基类应该实现IDisposable接口来释放资源。这个类型也应该添加一个析构函数，做为最后的被动机制。这两个方法都应该是用虚方法来释放资源，这样可以让它的派生类重载这个函数来释放它们自己的资源。派生类只有在它自己须要释放资源时才重载这个函数，并且一定要记得调用基类的方法。开始时，如果你的类使用了非内存资源，则一定得有一个析构函数。你不能指望你的用户总是记得调用Dispose方法，否则当他们忘记时，你会丢失一些资源。这或许是因为他们没有调用Dispose的错误，但你也有责任。唯一可以确保非内存资源可以恰当释放的方法就是创建一个析构函数。所以，添加一个析构函数吧!当垃圾回收器运行时，它会直接从内存中移除不用析构的垃圾对象。而其它有析构函数的对象还保留在内存中。这些对象被添加到一个析构队列中，垃圾回收器会起动一个线程专门来析构这些对象。当析构线程完成它的工作后，这些垃圾对象就可以从内存中移除了。就是说，须要析构的对象比不须要析构的对象在内存中待的时间要长。但你没得选择。如果你是采用的这种被动模式，当你的类型占用非托管资源时，你就必须写一个析构函数。但目前你还不用担心性能问题，下一步就保证你的用户使用更加简单，而且可以避免因为析构函数而造成的性能损失。实现IDisposable接口是一个标准的模式来告诉用户和进行时系统：你的对象占有资源而且必须及时的释放。IDisposable接口只有一个方法：public interface IDisposable { void Dispose(); } 实现IDisposable.Dispose()方法有责任完成下面的任务：1、感知所有的非托管资源。 2、感知所有的托管资源(包括卸载一些事件)。3、设置一个安全的标记来标识对象已经被处理。如果在已经处理过的对象上调用任何方法时，你可以检验这个标记并且抛出一个ObjectDisposed的异常。4、阻止析构。你要调用GC.SuppressFinalize(this)来完成最后的工作。通过实现IDisposable接口，你写成了两件事：第一就是提供了一个机制来及时的释放所有占用的托管资源(译注：这里就是指托管资源，当实现了这个接口后，可以通过调用Dispose来立即释放托管资源)，另一个就是你提供了一个标准的模式让用户来释放非托管资源。这是十分重要的，当你在你的类型上实现了IDisposable接口以后，用户就可以避免析构时的损失。你的类就成了.Net社区中表现相当良好的成员。但在你创建的机制中还是存在一些漏洞。如何让一个派生类清理自己的资源，同时还可以让基类很好的再做资源清理呢？(译注：因为调用Dispose方法时，必须调用基类的Dispose，当然是在基类有这个方法时。但前面说过，我们只有一个标记来标识对象是否处理过，不管先调用那个，总得有一个方法不能处理这个标记，而这就存在隐患)如果基类重载了析构函数，或者自己添加实现了IDisposable接口，而这些方法又都是必须调用基类的方法的；否则，基类无法恰当的释放资源。同样，析构和处理共享了一些相同的职责：几乎可以肯定你是复制了析构方法和处理方法之间的代码。正如你会在原则26中学到的，重载接口的方法根本没有如你所期望的那样工作。Dispose标准模式中的第三个方法，通过一个受保护的辅助性虚函数，制造出它们的常规任务并且挂接到派生类来释放资源。基类包含接口的核心代码，派生类提供的Dispose()虚函数或者析构函数来负责清理资源：protected virtual void Dispose( bool isDisposing ); 重载的方法同时完成析构和处理必须提供的任务，又因为它是虚函数，它为所有的派生类提供函数入口点。派生类可以重载这个函数，提供恰当的实现来释放它自己的资源，并且调用基类的函数。当isDisposing为true时你可能同时清理托管资源和非托管资源，当isDisposing为false时你只能清理非托管资源。两种情况下，都可以调用基类的Dispose(bool)方法让它去清理它自己的资源。当你实现这样的模式时，这里有一个简单的例子。MyResourceHog类展示了IDisposable的实现，一个析构函数，并且创建了一个虚的Dispose方法：public class MyResourceHog : IDisposable { // Flag for already disposed private bool _alreadyDisposed = false; // finalizer: // Call the virtual Dispose method. ~MyResourceHog() { Dispose( false ); } // Implementation of IDisposable. // Call the virtual Dispose method. // Suppress Finalization. public void Dispose() { Dispose( true ); GC.SuppressFinalize( true ); } // Virtual Dispose method protected virtual void Dispose( bool isDisposing ) { // Don't dispose more than once. if ( _alreadyDisposed ) return; if ( isDisposing ) { // TODO: free managed resources here. } // TODO: free unmanaged resources here. // Set disposed flag: _alreadyDisposed = true; } } 如果派生类有另外的清理任务，就让它实现Dispose方法：public class DerivedResourceHog : MyResourceHog { // Have its own disposed flag. private bool _disposed = false; protected override void Dispose( bool isDisposing ) { // Don't dispose more than once. if ( _disposed ) return; if ( isDisposing ) { // TODO: free managed resources here. } // TODO: free unmanaged resources here. // Let the base class free its resources. // Base class is responsible for calling // GC.SuppressFinalize( ) base.Dispose( isDisposing ); // Set derived class disposed flag: _disposed = true; } } 注和意，派生类和基类都有一个处理状态的标记，这完全是被动的。重制的标记掩盖了在处理时任何可能发生的错误，而且是单一的类型处理，而不是处理构成这个对象的所有类型。(译注：就是基类与子类各自标记一个，互不影响。)你应该被动的写处理方法和析构函数，处理对象可能以任何顺序发生，你可能会遇到这种情况：你的类中某个成员在你调用Dispose方法以前已经被处理过了。你没有看到这种情况是因为Dispose()方法是可以多次调用的。如果在一个已经被处理过的对象上调用该方法，就什么也不发生。析构函数也有同样的规则。任何对象的引用存在于内存中时，你不用检测null引用。然而，你引用的对象可能已经处理掉了，或者它已经析构了。这就引入用了一个非常重要的忠告：对于任何与处理和资源清理相关的方法，你必须只释放资源!不要在处理过程中添加其它任何的任务。你在处理和清理中添加其它任务时，可能会在对象的生存期中遇到一些严重而繁杂的问题。对象在你创建它时出生，在垃圾回收器认领它时死亡。你可以认为当你的程序不能再访问它们时，它们是睡眠的。你无法访问对象，无法调用对象的方法。种种迹象表明，它们就像是死的。但对象在宣布死亡前，析构函数还有最后一气。析构函数什么也不应该做，就是清理非托管资源。如果析构函数通过某些方法让对象又变得可访问，那么它就复活了。(译注：析构函数不是用户调用的，也不由.Net系统调用，而是在由GC产生的额外线程上运行的)它又活了，但这并不好。即使是它是从睡眼中唤醒的。这里有一个明显的例子：public class BadClass { // Store a reference to a global object: private readonly ArrayList _finalizedList; private string _msg; public BadClass( ArrayList badList, string msg ) { // cache the reference: _finalizedList = badList; _msg = (string)msg.Clone(); } ~BadClass() { // Add this object to the list. // This object is reachable, no // longer garbage. It's Back! _finalizedList.Add( this ); } } 当一个BadClass对象的析构函数执行时，它把自己的一个引用添加到了全局的链表中。这使得它自己又是可达的，它就又活了。前面向你介绍的这个方法会遇到一些让人畏缩的难题。对象已经被析构了，所以垃圾回收器从此相信再也不用调用它的析构函数了。如果你实际要析构一个可达对象，这将不会成功。其次，你的一些资源可能不再有用。GC不再从内存上移除那些只被析构队列引用的对象，但它们可能已经析构了。如果是这样，它们很可能已经不能使用了。(译注：也就是说利用上面的那个方法让对象复活后，很有可能对象是不可用的。)尽管BadClass所拥有的成员还在内存里，它们像是可以被析构或者处理，但C\#语言没有一个方法可以让你控制析构的次序，你不能让这样的结构可靠的运行。不要尝试。我还没有看到这样的代码：用这样明显的方式来复活一个对象，除非是学术上的练习。但我看过这样的代码，析构函数试图完成一些实质的工作，最后还通过析构函数的调用把引用放到对象中，从而把自己复活。析构函数里面的代码看上去是精心设计的，另外还有处理函数里的。再检查一遍，这些代码是做了其它事情，而不是释放资源!这些行为会为你的应用程序在后期的运行中产生很多BUG。删除这些方法，确保析构函数和Dispose()方法除了清理资源外，什么也不做。在托管环境里，你不用为每一个创建的类写析构函数；只有须要释放一些使用的非托管资源时才添加，或者你的类所包含的成员有实现了IDisposable接口的时候也要添加。即使如此，你也只用实现IDisposable接口完成所有的功能就行了，不用析构函数。否则，你会限制你的派生类实现实现标准的Dispose习惯。遵守这个我所讲叙的标准的Dispose习惯。这会让你的程序生活变得轻松，也为你的用户，也为那些从你的类创建派生类的人。
发布于：2006-9-6 15:04:261因为尹崇民，她想起这段惊航。这夜她赤脚坐在冰冷的木质地板上，窗外有雷鸣电闪，崇民正安好地睡在她身边。她听到了崇民的呼吸，安定平稳。她闭上眼睛，就有层叠影像纷至沓来。像是一场深海远航，记忆的末梢总会隐隐触及那样一些暗礁。是时，她听到时间的航船破浪前行的声音，听到暗礁硌到船底，发出了让人心碎的咔咔声。于是她爬起来，回到崇民身边，抱住了他暖而有力的臂膀。在那里，勇敢的大副带她离开，奔赴一段面向朝阳的旅途。她看到一朵暗礁深处的浪花在身后绽放，白浪滚滚，逐渐隐没。2十五岁的时候，衣砚疯狂地想穿高跟鞋。那时她与外婆住在城南小镇上，一座老旧的木质房子，楼下是一顺的干杂店，卖五毛钱的头花和一块钱的香烟，房子侧面有一道回旋楼梯，木质扶手木质踏板，人踩上去噔噔响。她喜欢买上一串蘸过甜酱的香干，站在楼梯下慢慢咬。街景热闹，有骑破烂脚踏车下班买菜的，有推着小推车卖烙饼水果的，还有东家的猫西家的狗，间或有一场乱糟糟的撕咬打斗，一条小街沸成一锅粥。这些热闹她并不留心，惟一能记得的，是临街小吃店的老板娘。每天傍晚，老板娘会踩着红彤彤的高跟鞋从干杂店前面过，藕臂上偎着闪亮的小拎包，买二两话梅或一袋瓜子，顾盼生姿地走出小街去。有一次她在楼上看到老板娘，穿胭脂色连衣裙，似一团红云从路口飘来。她赶紧下楼，鞋也没顾上穿好。老板娘买好瓜子，看她站在一旁瞧，以为她想吃，走过来笑眯眯地掏一捧往她手里放。这时候外婆在楼上好大声叫她的名字：衣砚，还不快上来写作业。衣砚没敢接，转身往楼上跑。噔噔噔，脚步重得震落了楼板上的灰尘。等她转头看的时候，老板娘还站在原地，笑眯眯地望着她，然后两只手指捻一颗瓜子放到嘴里，噗，瓜子皮落到台阶上。之后她听外婆说，老板娘，风骚。风骚这两个字在十五岁的衣砚看来，不具备任何贬义。尽管外婆一边剪线头一边用不屑的语气说，这回不晓得又和哪个男人勾搭上了。不过勾搭这两个字衣砚倒是懂得。那对卖干杂的小夫妻以及贩菜的老两口，都不算是勾搭。年少的衣砚想，勾搭这种事，必须是老板娘这样漂亮的女人才能做到。只是外婆骂老板娘风骚，有时又会叹气，说那女人可怜。衣砚就总爱追问，怎么可怜了？外婆说，她大概新婚守寡，儿子生下来也寄养别处，因为招了些骂名，这么多年来，也始终没有哪个正经男人肯同她好。摇摇头，外婆又说，一个女人，很快就老了。这是衣砚关于十五岁最深刻的记忆。十五岁，她做梦都想像老板娘那样，穿着红色高跟鞋，款摆着柳腰走过小街。二两话梅或一包瓜子拎在她手里，十指上的那点红一闪一闪，唇角有痣隐现，笑一笑，醉落一片桃花。后来有一天，妈妈回小镇看她和外婆。那夜她睡在木楼小间的钢丝床上，听见妈妈在和外婆说什么重点什么转学。她并不很在意。亮白的月光从木楼走廊外流淌过来，翻越黝黑的廊柱和雕花的栏杆，在木质地板上展平。那里躺着妈妈宝蓝色的缎面高跟鞋，一颗小小的水钻发出了冷硬的光。她盯着那点亮光，一直熬到妈妈和外婆的声音低下去，终于听不见。然后她爬起来，光着脚丫蹑手蹑脚走过去，偷偷抱起那双鞋。深秋的小街万籁俱寂，没有人，也没有灯。她小小的白瓷般的双脚放在空荡荡的高跟鞋里，像幽蓝花瓶里栽种着两朵莲花。一步，两步，笃笃的敲击声响起，她是高傲的公主，在月光下接受着空巷来风的注视。那种瞬间而至的成长，在月色中，在空巷里，仓猝而忧伤。3衣砚回小镇是在十七岁，考上大学以后。那一年的小镇对她来说，是所有悲伤的发源。外婆在春天过世了。因为怕影响她高考，妈妈甚至不肯把这个消息告诉她。衣砚很后悔。她还记得外婆坐在书桌边借着台灯的光线挑脚边的样子——微微张着嘴，针凑到眼前，两只手指捏着线头抖抖嗦嗦地试探，啊一声，穿过去了，脸上露出如释重负的表情，然后抬头看看她的课本说：专心。衣砚想，如果能早点回来，大约还能在月圆的夜晚，见到舍不得走掉的外婆。老人们都说，如果一个人心有留恋，是能在死后一段时间里，回来完成心愿的。外婆最大的心愿就是能再看她一眼吧，那么多年，她们相依为命，外婆走的时候，一定有很多很多话想要对她说。可她的归来毕竟是晚了。那张菲薄的通知书上有她的名字，不过外婆走得太远，已经看不见了。这时候的外婆，想是已经喝了孟婆汤，过了奈何桥，忘记有她这个小人儿还等在这里。她想起能为外婆做的好多事，不过等她明白子欲养而亲不待的时候，外婆已经不见了。她去给外婆的坟上烧纸钱，在田边听人讲起老板娘的事。老板娘死了，在镇郊的一大片葵花地里。盛夏的葵花很灿烂，金黄里带着点点青，每天都是笑脸。只是老板娘的红色高跟鞋抛落在地里，大片大片伏倒的葵花盖住了她赤裸的身体。静寂的小镇投了一颗弹，黑压压的人群包围了那片金黄得刺眼的农田。人群散开的时候警犬来了，在那具不再鲜活的肉体上嗅嗅，发出呜呜的声音。后来有人说老板娘是自杀，约略是为什么人动了真情，对方却是个有妇之夫，不肯给她好的名分，不肯娶她；不过也有很多人不相信，说像老板娘这样风骚的女人，断不会为一个男人送了性命。这些人怀疑老板娘是被她某个情人的老婆给害死，为了让她到下辈子都记得勾引别人丈夫是可耻的行为，所以扒光她的衣服，让她一路羞到阎王殿去。当然，说这些话的人多半都是些姿色平庸的半老女人。老板娘的死在衣砚心中种下一颗魔豆，一路茁壮生长到云端里去。如何死的呢？那个风骚妖娆的独居女人。她曾经，在男人们火热的目光中骄傲地从小街走过，红色高跟鞋踩在石板路上发出清脆的声响。只是，她为什么赤身裸体躺在葵花地里，像一个初生的，纯洁无邪的婴儿那样死去？有人指着葵花地对衣砚说，她在那里。衣砚看到那座孤伶伶的坟冢立在葵花地边，没有墓碑也没人烧纸。只有一只被雨淋坏的白幡悬在坟头，像是吊死的白鹤。她走过去翻开结在一起的纸幡，那上面有一行隐隐约约的小字。同来的人说大概是某天晚上她的某个旧情人挂上去的，因为当晚就有大雨，因此也不能知道，那些淋湿的字迹到底是什么。衣砚想，老板娘，她大概是这个小镇上惟一有故事的女人吧。4二十岁的时候，衣砚想恋爱。那时候她有了一双白色高跟鞋。小尖头的高跟鞋是为一场舞会，她花去几乎一个月的生活费买来的。羊皮的鞋面上镶一颗小小的水钻，有八寸细跟。如果穿着它和某个人跳舞，衣砚想，不知道会是怎样的美。舞会那天樱花却凋了，白蝴蝶死在丛生的杂草里。衣砚穿着白色高跟鞋站在炫目的灯光下看他，进退失据。有一个人，年轻男孩儿，过来请她跳舞。她摇头，把手藏到身后去。男孩儿以为她不会，歪着头轻轻问，我教你好吗？她咬咬唇再看他一眼，终于一言不发地离开。她收起那双白鞋子的时候很心酸。她只是想和他跳个舞，手心对着手心，脚步随着脚步，用没有人知道的心情，就连他，也不知道。杜远方是她老师。当她能理直气壮地穿着高跟鞋在人前走过的时候，她遇到的这样一个人。他站在青春的尾巴上，用残存的激情以及正急速涌现的衰老落寞撼动了她的心。如同她十五岁时疯狂渴望的红色高跟鞋，那是一种代表着她将要奔赴的方向的气息。因为是他，因此她渴望有一场凶狠炽烈刻骨铭心的爱，就像荒野之中被雷电击中的孤树一样，去燃烧，去牺牲，直至耗尽心力，尸骨无存。她想过一些结局，比如隐秘地爱过一生，然后去他坟上献一只纸幡。在此之前，他不知道任何与她有关，与她爱他有关的事情，因为他们之间不光隔着漫长的十八年的距离，还隔着一个平凡安好的婚姻，隔着世间太多蜚短流长。她喜欢在远处看着他，一个微微瘦削却显得刚劲的背影。他讲课的时候，他抽烟的时候，他走路的时候，他微笑的时候。每一个片段，她都习惯于拿来温习很久，就像参加某场考试那样，怕记得不牢，怕转眼忘掉。事实上她怎可能忘掉？像对待一双美丽的高跟鞋，时时刻刻，心心念念。只是，当她孤独地穿着高跟鞋走在空巷中，却猝不及防，被他迎头撞见，一眼洞穿。她始终都不明白，他是如何知道并笃定她对他的微妙感情。而如果那封信不是杜撰，那么，到底又是谁在窥探着她的生活她的感情？深秋的下午，杜远方打电话给她。她穿过夹道的银杏去学院大楼找他。厚厚的墨印信纸上，都是她因他做过的种种傻事。她在大风口等他骑车经过，叫声老师好，然后一路咳嗽回去；他阅卷时她站在办公室门口，一遍又一遍挡掉询问的同学，只怕他不能天黑前吃饭。她跟踪过他妻子，学她穿衣打扮说话语气；她去小学门口拦过他的儿子，用一支波板糖换他叫一声阿姨。她记得他的生日他的电话以及他的住址门牌身份证号码。她在左侧肩胛上，纹上了这个十八位的数字，她想有他独一无二不能为别人所取代的标志。可是这一切，他怎么可能知道？杜远方看着她笑，衣砚，对我来说，你太年轻。他的眼神坦荡荡，嘴角笑微微，额头眼角有了一些细碎的皱纹，慈爱得像对待一个恶作剧的孩子。他的神情让她感到羞耻。就像偷穿了妈妈高跟鞋的孩子，自以为很美，却怎么看都是笑话。羞耻感让她哭起来，更多的是心痛。二十岁，她穿过白色高跟鞋，想和他跳一支舞，像樱花一般开过，燃一夜也足够。可是他不需要，他看她只似一个笑话。因此她急急掩藏受伤，开始讲十五岁时对高跟鞋的渴望，讲起老板娘。她说老师其实没什么的，它的意义只是这样。杜远方讶然，看她的眼神变得闪烁。她离开时杜远方点燃了一支烟，无声地，把自己罩进一片烟雾里去。5因为这封信，衣砚感到了恐惧。就像死去的老板娘，在杜远方以及某个不知名的人面前被扒光了衣服，赤身裸体无处遁形。很长一段时间，她都有些精神恍惚，夜里抚着背上的刺青，陷入一片虚空里去。爱一个人让她瞬间枯槁，而恐惧又把她折磨得不成人形。即使她一整个冬天都穿着厚厚的棉袄大衣，却仍时刻警惕着周围的每一个人，怀疑必定有这样一个鬼魅，藏在他们中间，窥伺、刺探、狞笑，剥开层层遮羞布，让她站在寒风中周身赤裸。她突然就想谈一场恋爱，属于二十岁的，平和而温暖的恋爱。透明，直白，不需要有卑微怯懦的姿态。而等她这样想着并睁开眼的时候，就发现周潮阳已经在那里了。周潮阳是她同学，关系一直友好。十二月，他们被调到同一个课题组跟项目，于是相约去喝酒。那天晚上城市里有一场小雪，周潮阳抱住她的时候她瑟瑟发抖。她想若不是这个男人，谁来陪她抵挡让人发疯的夜的寂寞寒凉。他的指尖修长，她的香烟微凉。他们拥抱着从操场的大风中走过，周潮阳说，我看过你哭泣时的表情，是你从杜远方的办公室出来以后。我不知道是什么令你如此悲伤，但就是这样一瞬间，你打动了我。那之前他还没有爱过谁也不肯被谁爱过，他的感情单纯而又直接。她指间的香烟一段一段燃成灰烬，烟灰落在他肩上，如霜，如雪。此后她不肯再见杜远方，所有他的课，都和周潮阳逃去校外。有时候她独自搭长途客车回小镇，去看望外婆，看望老板娘。她发现老板娘的坟头，开始添了新的白幡。从前那上面写满了细密却被淋得模糊不清的字，而这一次，是一片洁白。过了那年冬天，天气变暖的时候，她和周潮阳正式恋爱了。她又肯去听杜远方讲课，在课上，他落寞的眼神扫过她，那一席话让她始终心有戚戚。他说人的一生如同一张白纸，只有最初画上的那一笔，才会有锥心入骨的记忆，而以后的种种经历，不过是层叠其上，无止境的重复而已，即使深刻，也不会刻入你的脏器骨髓里去。后来衣砚不断问自己，你能忘得掉吗？如果人生果真只如一张白纸上的不断重复，那画下的第一笔，你要怎么忘掉？因为这样，她对周潮阳，始终没有对等的温度。夏天快到的时候周潮阳对她说，带我去看看你外婆吧。回小镇的路上有金黄的葵花地，客车驶过的时候会听到风吹葵杆的声响。进了镇已经是傍晚，她带周潮阳去看临街的小吃店。那里成了一家茶馆，支着七八桌竹椅，有人在堂上讲书，有人在底下喝盖碗茶。他们在附近吃过晚饭，出来的时候天边正烧晚霞。周潮阳买了一支手电，牵着她的手往外婆家走。外婆的木楼大部分已经租给干杂店的小夫妻，三间铺面连起来，挂上了“副食批发中心”的招牌。楼底下的台阶上有人坐着扣花牌，一群孩子围着看，不时发出嗷嗷的叫声。几个熟识的街坊在门口支了麻将桌，看见她点点头说回来啦。他们上楼。二楼走廊上晾着汗衫和粗布裤子，只有最尽头外婆的那间屋子落了锁。抹完屋子里沉积的灰尘，周潮阳去楼下买水果。衣砚推开窗，窗外是一望无际的稻田和远山。外婆的坟墓还有那片金黄的向日葵地正对着窗口，远远的，暮色中看过去像油画。而突然间，她的眼睛就被什么点亮了。在那里，外婆的身边，老板娘的坟前，一个熟悉的背影，正朝向镇外的方向一步一步离开。他的发，背，以及走路的姿势都是属于杜远方的，都是在衣砚的心里温习过无数次，决不会错认的。她跑出去，撞上了楼梯上的周潮阳。他在她身后叫她的名字，她没有停留。那只白幡，在田野的暮色里，在孤寂的坟头上高高飘扬。洁白的，一字不着的，像极了一个初生儿。杜远方。她站在那里喊，对着已经看不见的小路尽头，对着已经弥漫开来的夜雾。周潮阳站在她身边了，沉默地抱住她了，可是她停不下来。停不下来喊杜远方的名字，停不下来哭泣，停不下来向魔豆顶端的攀爬。她一直喊到声嘶力竭，眼泪打湿了周潮阳的肩。杜远方爱老板娘，那个在他心里画下第一笔的人，原来是她。6周潮阳问，为什么是杜远方？她说，那是十五岁的红色高跟鞋，是不肯熄灭自以为是的美，是第一笔，是一种渴望。那么我是什么呢？是风。是路过空巷的风。欣赏过一个偷穿高跟鞋的女孩，但是要吹去别的地方。那天晚上周潮阳走了。她从二楼的走廊上看他穿过小镇的街道，像一只流浪的猫。她跟着他走过一段路，他留下的手电光在夜里显得微弱。然后她折返去了葵花地，在虫鸣声里，一步一步，沿着杜远方的足迹，走到老板娘的坟前。下雨了，那只白幡渐渐被淋湿，像是一只吊死的白鹤。她呆呆地坐了一会儿，然后站起身来往回走。这时候大风穿过葵花地，发出呜呜的哀号。她的手电一下子摔碎在田埂外，一双滚烫有力的大手从背后抱住了她。恐惧如潮水一般顿时将她灭顶。她像溺水的人那样，不顾一切地挣扎，呼喊，声音却被堵在一只宽厚的手掌中，而她的身体迅速被压倒在了大片葵花丛里。她听见葵杆断裂时发出的噼啪声，绿色的浆水喷礴而出，同冰冷的雨水一起，染满了她的身体。滚雷在喧嚣，棉布在撕裂，黑暗是一只密不透风的大盖子，在她仰面的那一瞬间，铺天盖地地拍过来。她有过短暂昏迷，然后渐渐清醒。她停止了一切挣扎和哭喊，她听到了他的喘息。碎裂的、痛苦的，像是一种不痛快的哭泣。他的鬓角有碎碎的扎人的发茬，唇上似乎有某种果香。而他的身体，却是一种年轻的激情的残暴。他像在惩罚，而后却变得温柔。她再一次陷入了无边的黑暗之中。雨在天明以前停了。有人从老板娘坟前离开，那坟头上，插着一只写满了字又被淋湿的白幡。7衣砚醒来的时候是在外婆床上，那只摔坏的手电放在她枕边。她乘午间的车回学校找杜远方。那一刻他变成最让她感到安全和信任的人。杜远方燃着烟，厚重的烟雾把他的脸变得缥缈虚浮。衣砚，他说，这个故事很简短，我与她，几乎只用短短的几句话就可以说完。年轻的时候我们深爱，后来她突然悄无声息地离开。很多年时间，我娶妻，她嫁人，甚至没有再见过对方，直到三年前的偶遇。后来我对婚姻有过动摇，决定去找她，可是等我找到她的时候，她已经变成了一座荒坟。杜远方说完这个故事好像陷入了沉思，衣砚走出去的时候他唇边的烟火还在明明灭灭地闪烁。周潮阳不再来找衣砚。衣砚鄙薄地想，欠你的，我已经清偿。只是两个月之后，衣砚不得不去找杜远方帮忙。她讲起葵花地里的那个夜晚，复杂的心理描述像是小说。她没有提到周潮阳，她只是觉得，那种感觉，好像不是强暴，倒是一次意外的野合，经历了，甚至是解脱。只是她怀孕了，除了杜远方无人可说。在杜远方的肩头，他抚着她的发说，不用担心，有老师在你身边。她拿掉了那个孩子，在他或她还未及三个月的时候。她坐在另一个城市的医院走廊里等待杜远方拿药回来，走廊上有人来去，之后她看到一张轮廓分明并有两道法令纹的脸。这是她第一次见到的尹崇民。面相陌生，表情严肃。只是尹崇民的声音那样温暖干净。你好，你还记得我吗？那个舞会，邀请你跳舞的男生。我是这里的实习医生，这个月刚刚进来。她一直觉得长着法令纹的人总是残酷无情，可是尹崇民的笑，就像初夏时节金黄泛青的葵花。8周潮阳到美国做访学的公示贴出来，签证也很快下来。他走后不久，杜远方陪衣砚去医院的事，开始被传得沸沸扬扬。那时衣砚已大四，工作安顿在崇民所在的城市。横竖是要离开，只是心里担忧着杜远方。她打过两次电话给他。他说，你别回来，也不要管。她想，他到底还是用心呵护着她。这段所谓的师生畸恋后来闹得满城风雨，杜远方的妻子来工作地找她。一个已经三十五岁，需要用厚厚粉底来盖住青黑眼袋的女人。她很瘦，瘦就显得有些刻薄凌厉。首先她给了衣砚左脸一巴掌说是为自己，后来又给了她右脸一巴掌说是为儿子，然后她开始哭哭啼啼痛陈杜远方对她和她儿子的重要性，一面质问她女人何苦为难女人，一面劝告她年纪轻轻应该顾及前程名誉。第一次，衣砚意识到婚姻对一个女人来说是多么重要。它可以不是红色高跟鞋，不是白纸上的第一笔。它只需要是一种权力，一种包含凛冽耳光，对同性宣告所有的权力。要知道，这种所有是理直气壮无可辩驳的，也是绝版竞拍先来先得的。她是杜远方的妻子，因此有权让衣砚灰头土脸地承受响亮的耳光，还要在众目睽睽之下听她对她晓以大义。对杜远方，衣砚感动愧疚的同时，还有深切的同情。幸亏之后崇民下班过来请她吃饭，撞上了这一幕。于是他揽住衣砚的肩说，请尊重我女朋友。那天崇民请杜远方的妻子一起吃饭。他从容地解释说，其实我和衣砚在一起快两年了，双方父母都同意，也确实有过同居生活并来同一个城市工作。因为是学生，衣砚不想让父母知道孩子的事，我不巧又在那家医院实习，所以不得已才央求了杜老师。随后他掏一个锦盒出来，是一双戒指。他说师母您看，我和衣砚戒指都挑好了，准备年底就结婚。他捏着衣砚的手，好像真是缱绻情深的样子。过后崇民对衣砚说，本来正是打算向你求婚的。我不开玩笑，不是同情，更不为应付那些蜚短流长。衣砚，你不知道我有多爱你。周潮阳之后，尹崇民之前，衣砚将旧有生活一一盘点，才发现杜远方对她来说，永远都像是十五岁对红色高跟鞋的想像，那么可望不可及。因此当崇民第二次求婚的时候，衣砚笑着说，到明年初夏，我们结婚吧。后来有一天，她遇上当年留校的同学。同学带点嘲讽鄙夷的语气，故意说起杜远方。她毕业以后杜远方在学校也呆不下去，匆匆离职下海，自己做起了生意。后来听说他妻子赴美继承了叔父一笔遗产，杜远方便结束掉手头经营惨淡的生意，跑到美国一所三流大学任教。不久他妻子和同校一个教授闹出了轰动一时的绯闻，与他离了婚，他只好一个人灰溜溜地跑了回来。衣砚的心仍是为他隐隐痛了一下，是第一笔画下的裂痕在发出咔咔的声响。她把这些事转述给崇民听，崇民搂着她淡淡地说，杜远方的事，以后别再打听。9若是生命的航程里没有周潮阳，衣砚想，她能否自一段忧伤的自迷中返航？春天的时候，周潮阳在美国病逝，他的信辗转到了衣砚手里。同学说，周潮阳虽然已经不在了，不过他的遗物里，有很多是写给你却未及发出的信。这些虽是死者的私隐，不过交给你，能让你记得他那份深情，也算是个纪念。衣砚想，死者已矣，说原谅与否，都无用了。可是她展开信，就如同开始了一次惊险的远航。她在滔天的巨浪里颠簸，每听到一声碎裂的轻响，都能感到巨大的疼。晚春的空气里到处都有罪恶的甜蜜。她读完信，像一只为郁香麻醉的蝴蝶，虚软无力地留下眼泪。崇民下班回家，她说，我们去看看外婆吧。回镇的客车还是在傍晚到达，她领着崇民沿她与周潮阳走过的路线一一看过去。茶馆又倒了，翻修后成了美容美发厅；木楼太旧，年后就要拆迁。干杂店早就关闭，楼上楼下都租住着外来客。她踩着呀呀作响的木梯上楼，外婆的房间充斥着潮湿发霉的味道。她推开窗，金黄的葵花地以及孤伶伶的坟头，像油画一样留在那里。那天晚上她对崇民说有雨。崇民在走廊上观望的时候她说睡吧，屋外就有风声起来。夜像浓墨一样黑，崇民的呼吸渐渐均匀。她起身，去打开崇民随身携带的那个小行李袋。脆的，冷的，层层叠叠。一个闪雷，她无声惊叫，黑色提袋里，是一只写满了字的白幡。她呆坐在地板上，暖意和安然一起涌出来，像粘稠的棉花糖，对她做着温柔的挤压。她记得，周潮阳的信里分明写着：“衣砚，在这些永远不会发出的信里，有我所有怯懦的爱和迟到的忏悔。那个负气离开的夜晚，其实我曾折返找过你。在坟地，你身边是惊恐慌乱的杜远方。那夜他承诺给我访学名额并保我签证顺利，于是我懦弱无耻地逃掉。后来我知道你误会那夜是我，而杜远方并没像他说的那样给你婚姻的承诺。于是我打电话给他妻子，想为你讨回些什么。我回来找过你，可看到崇民，我知道，不会有人比他更爱你。衣砚，如果你相信爱有奇迹，那就是老板娘把儿子崇民带给了你。而我会带着这忏悔，只在天国，为你亮一段航程……”她重新爬上床躺在崇民身边，抱住了他暖而有力的臂膀。她想，人生真是一场惊险离奇的深海远航，总会有无数暗礁潜藏在你不可预想的地方。她乘着时间的航船自密布的暗礁群中险险掠过，只分毫的差距便能葬身海底。可是很幸运，她有一个始终沉默却勇敢坚定的大副，能带着她奔赴一段迎向朝阳而去的旅途。当她在这段惊险的航程后回望，她看见，所有暗礁深处的浪花都化成滚滚白沫，终究逐渐隐没。天明的时候崇民叫醒她，他们手牵手去外婆坟上烧纸。那只白幡已经挂在了老板娘的坟头，她知道当她安睡的时候，她的大副曾穿越风雨中的稻田和大片葵花。在远处，外婆的木楼那样老旧；在近处，晨光流泻，一地葵花金黄。而她不知道的是，十五岁的那个夜晚，年少崇民在空巷，看到一个偷穿高跟鞋的女孩。十年来她一直那样单纯地对爱充满了幻想，就像崇民的母亲一样。只是老板娘的故事并不复杂，她只是一个需要靠种种手段勉力谋生的普通女人。而在老板娘的生命中，从来没有一个叫杜远方的男子，他只借用了她构想的故事，留给她一道暗伤。她也不会知道，崇民从葵花地里抱她回家的时候，有多么害怕她会像母亲一样。但是这都没有关系了，因为她知道，那只白幡上写着：——妈妈，我能肯定，她会幸福。
发布于：2006-9-2 14:36:17生命原是那样的忧伤和残酷，我们在呼啸而过的记忆中开始奔跑与追逐。大风中亲爱的你们有没有听到，那女孩挥动着翅膀在轻声说——其实，每一位天使都终将离去。12003年2月初，我再次见到这个叫流岚的女孩。当她异常温柔地对我说，请把你的病历卡拿给我，我知道，她已经不记得我。她拿着我的病历卡，目光在我的名字上停顿了几秒钟，然后她问：你叫严戈？是的，我说。我的心里还有着极大的期待，我迫不及待地肯定，然后用热切的眼光看着她。哦。她什么也没说，只是点点头，用黑色签字笔在那下面的空白处飞快地写了两行字，然后递给我，严戈，你进去吧。在那一瞬间，我觉得身体内有什么东西被彻底绷裂。我感觉不到自己还是完整的一体，好像所有的器官都在同一时间停止了运转，分崩离析，只剩胸膛以左的某处还在异常剧烈地跳动。我相信她听到了这不寻常的声音，我转回头时，分明看见她抬头看了我一眼。这一眼，便是我一厢情愿，也愿意相信它是意味深长的。只是她的嘴唇始终紧抿着，吝于再说出一个字。我就在她沉默的注视中，仓皇而逃。2流岚的出现和消失，就像一场梦的始末。开始，我和女友路枚是众人口中的郎才女貌，天作之合。新千年伊始，我还在外贸公司做部门主管，路枚的事业也刚起步，非常辛苦。所幸工作顺利，恋爱甜蜜，年中在三环买了房子。我同路枚商量，等年底房子完工装修好就结婚。于是路枚每天下班总是拉着我往工地跑，穿着套装高跟鞋一扭一扭地往钢筋水泥缝里钻。好几次被守工地的大爷恶狠狠地骂，她也不生气，只是笑嘻嘻地望着我，跟孩子一般聒噪地问来问去，你看卧室涂什么颜色呢？你看地板用那种牌子呢？2001年春天，温暖的三月，我和路枚拿到房子钥匙，第一次去看我们的家。空荡荡的客厅和雪白的墙壁，但一切很美妙。她站在二十一层楼高的露台上，衬着微蓝天空洁白流云，把身子斜斜地向外倾，张开双臂用尽全力大声喊——这是严戈路枚的家。以后的日子里，我们总是在下班后一起牵手去屈臣氏看家具。我一直以为最迟九月，我和路枚的婚礼就能定下来。可到五月底房子布置得差不多时，路枚突然跟我说要到另一个城市去拓展市场。为这事我们起了前所未有的争执，路枚的父母还专门北上，但她去意已决。临别那晚，我们躺在房间的床上听音乐时她说，严戈，我只是想让自己足够独立与坚强，在你累了的时候，能够撑起这个家的重量。3路枚离开我，留下满满的寂寞。每天下班后我常常一个人躺在客厅冰冷的沙发上，开着电视放着音乐毫无节制地喝酒，胃病就在这个时候复发。我第一次上医院，是在2001年路枚离开后的十月里。北方的天气已经有微微的凉意，医院回廊上的风很大。下午，阳光把走廊尽头窗棂的影子投在光滑的地板上，拉得很长。我从医院出来，遇上父亲在医院工作时的同事常青。很长时间不见面，常青忍不住埋怨我，来看病也不事先打声招呼，我好帮你安排呀。正说着，门里突然冲出一个女孩，冲着常青嚷，常主任，快，快去看看严歌。严戈？我当场愣在那里。常青一边匆匆往大门里走一边甩给我一句，她男朋友也叫严歌。这是我第一次见到流岚时的情形。她的五官异常白皙精致，仿佛易碎的玻璃，眼睛红肿，显然刚刚哭过。她的身体在北方的十月里看起来瘦得不像话，毛衣领口露出的锁骨冷冷地扎人。她的声音颤抖但是神情坚强，跟常青进门的时候，她特意转身问我，你也叫严歌吗？我说对，严肃的严，戈壁的戈。然后她深深地看我一眼，扯出一个有点凄凉意味的微笑，转身匆匆离去。4十一月底，我为公司签成一份为数颇巨的合同。周末客户过来，老总做东设宴。那日喝了很多酒，心情非常愉快，给路枚打电话，被吹捧上了天，一时间情绪高涨，便乘夜到四环一个僻静的小酒吧里坐坐。那地方叫危城。路枚最初离开的日子，我经常一个人开车过来呆坐半日，听听蓝调，或者只是静坐。那夜人尤其少。入夜开始下第一场雪，不很大，但是大风乍起且气势汹汹。我在吧台同酒保胡侃的时候，有人推门进来。我没想到还会见到流岚，也没想到她会在这种天气这种环境里出现。她穿着驼色长大衣和同色的小羊皮靴，围着厚厚围巾，两只手揣在大衣口袋里，还是很瘦，不胜风寒的样子，进门的时候，微微喘口气，带来午夜十二点的雪意。她看见我，愣了愣，似乎也很意外，然后走过来打招呼说你好严戈，没想到会在这里遇见你。她在我身边坐下来，姿势娴熟地点上香烟，和当日我初见的她判若两人。我忍不住倾身过去，轻叩两下桌子说，流岚，这样抽烟，会被尼古丁杀死的。她狠狠吸一口，转过头对我笑。她的气息萦绕在我鼻尖，我知道在来这里之前，她已经喝过不少酒。我终于还是问了。你男朋友，他的病情好些了么？她并不看我，低下头把手边大杯透蓝液体一饮而尽。然后她把脸深深埋进胳膊里，很久很久之后，有哭声断断续续传了出来。最后她醉了。她手里还夹着那支青烟袅袅的烟。酒保小五过来，充满怜惜地拿开她手中香烟。他说，已经没有任何希望了，她为他奔走两年多，还是没有办法，即使她本身，就被病人称为白衣天使。那夜雪纷纷扬扬直到凌晨，危城为了这个叫流岚的女子，第一次营业通宵，顾客只有我和她。也许她已经很久没这样无牵无绊甚至无梦境的好好睡过一觉，所以居然整夜未醒。我把她抱到靠近壁炉的沙发上，小五拿来毛毯和厚棉衣。危城的老板阿陈烤来榛仁松饼，和着温热的啤酒，我们围坐在流岚身边天南海北闲聊着看夜色从窗外渐渐褪去，其实心里都只是想让她睡得更沉些更久些更安心些。酒精把流岚的面色晕染得如一朵盛开的暗夜之花，充满幽淡的香气。她的脸在炉火的映照下美得不可思议，睫毛的暗影，微翕的鼻翼，丰润的嘴唇，尖翘的小下巴，如同镀上金光的天使，或者甫出花蕊的精灵。漫长的一夜里她仅微微翻过一次身，但同时惊吓了三个男人。这让我想起小时候看过的童话书里说的，在森林里走失的白雪公主躺在小矮人的床上沉沉睡去。想必，那七个小矮人和我们一样，也是怀着同样的心情，守候着公主的醒来。天亮的时候，我们喝光了第三次温出的酒。阿陈说，我们得休息了，要不晚上没办法照常营业，严戈，你送流岚回医院吧。那天流岚醒来的时候没有显示出任何惊讶和不安。她好像只是刚刚从自己家里的床上睡醒一样，呆呆发愣半分钟，然后托阿陈要了一次性洗漱用具做了简单梳洗，就乖乖地坐上我的车。就在那天，我第一次也是最后一次见到严歌。他与流岚一样年轻，眼睛大大，鼻子很挺，笑起来的时候脸颊上的梨涡格外明显。除了太瘦以外，他是一个让任何女孩子看了都会喜欢的英俊男孩。送我离开时流岚问我，为什么白衣天使，却救不了最爱的人？5十二月最后一天，我在危城度过了第一个没有路枚的旧岁夜晚。那日依旧大雪，从清晨到黄昏到深夜。是夜，我果真喝了超出平日太多的酒。离开危城的时候，夜已经很深。我在被积雪覆盖的马路上顶着刺骨的寒风步行。新年的灯火依然通明，霓虹把这城市上空照耀得五彩斑斓。在距离酒吧不远的地方，突然有人从背后紧紧抱住我。是流岚。夜色里她的眼神流彩碎金，仿佛随时会滴下钻石的星芒。这个新年的夜晚，她像个讨好贪欢的小孩，突然之间袭击了我全部的脆弱和孤独。她在我怀里哭了，她哭的时候我的手机在响。路枚打电话来同我说新年快乐，原来十二点的钟声就这样在我们头顶敲响。路枚从新年的整点开始陪我默数十秒许下最虔诚的愿望，然后愉快地说再见。我在突然涌出的欢呼的人潮里紧紧抱住流岚，在掐断手机的同一时间吻住了她。温暖与孤独，幸福与无望。雪依然无止尽地下着，狂欢的人群在我们身边如呼啸而过的风景，全世界都不重要了，当我吻她的时候，我知道我并没有想起路枚的样子。我明白这已是我能给她的最好。她要的，也就只是这么多。2002新年的第一天，我从清晨的阳光中醒来，流岚在我怀中沉睡。我不自觉回想起她的手臂缠绕在我颈间的感觉，似乎还能感受到她温热的眼泪，熨烫在我胸膛的最深处。常青这时候打来电话，他声音急切且疲惫，严戈，帮我找找流岚，她最近都只和你来往。你知道吗，昨夜她男友死了。流岚在这个时候醒过来，她看着我，然后慢慢将手臂缠上来，严歌，太好了，你还留在我身边。我无法遏止地紧紧抱住她，就是这一瞬间，我知道自己是在爱了。然而我也知道，她将我当成了严歌，她或许将带着这残破幻象，终生无法复原。严歌的葬礼很简单，一抔骨灰，两个人凭吊，仅此而已。流岚自始至终没有掉一滴眼泪。当我放下最后一捧鲜花时，她过来牵我的手，并笑嘻嘻地催促我回家。她走的时候，甚至没有再看一眼那照片上的人。她是决心要把他的离开，只当作一个游戏的散场。后来常青忧心忡忡地说，流岚现在这种情况，最好还是接受住院治疗。然而，当流岚在我身边，我便忍不住想要让这个幻觉更牢固。当她重复着叫我的名字时，我便忆起在危城见到她的那晚，她在毕毕剥剥燃烧着的壁炉边，睡得犹如童话故事里的公主。6整整一个冬天，我们都在危城度过。天气转暖的时候，我和流岚常常在黄昏，一起推开危城的那扇木门。二月里我们更频繁地出入危城，最后的一次我们统统喝醉，在温暖的壁炉边，睡倒在一起。夜里我似乎闻到榛仁松饼的味道，还有初时流岚身上的那种如暗夜之花的幽香。我忽然间有过一种隐约的预感，流岚会像这花香一般，渐渐消散在危城的夜色中。当我觉得我快要忘记路枚的时候，路枚打电话给我说，严戈，我要回来看你。路枚归来的那天是一个灰蒙蒙的阴天，傍晚的时候大风卷起飞扬的尘土。从机场回家的路上开始起雾，手机的讯号不是很好。我一直想着如何在路枚面前开口解释流岚的事。我想我是个自私的男人，我甚至希望路枚能够原谅我。当车驶上立交桥的时候路枚的情绪已经平静下来，她说她尊重并理解我的任何决定。那天，我忐忑地推开房门的时候，流岚从厨房里跳出来，腰间还系着我煮拉面时围过的红格子围裙，松松垮垮的样子，让她看起来像一只套着大布袋的松鼠，而她的手里还高高举着汤勺，脸上堆满了笑。路枚突然冲到她面前，紧紧握住她的双肩开始哭泣。流岚，你是好女孩，我知道你并不爱严戈！其实你一直都知道，真的严歌已经死了！我求求你，把我的幸福还给我吧！流岚在那时有过短暂的困惑，像一个无辜的小孩子一样，把漂亮的大眼睛转向我，带着祈求和期望，似乎是在寻找一个解释。但是没有等到我开口，她突然狂暴地丢掉手里的汤勺，一阵风似的冲出门去。等我回过神来追出去的时候，她已经坐上出租车，消失在我眼前。我跑回来抓起车钥匙，路枚扶着门框，给我一个哀婉的笑容。严戈，你怎么忍心救了流岚，却让我病入膏肓？你去找流岚吧，我会一直在这里，直到你回来。7我还是离开了，离开的时候天已经完全黑下来。我开着车在马路上发疯一般寻找流岚的身影。我不知道她会去哪里，她身上有没有带钱，她伤心绝望的样子会不会让坏人欺负。我的眼前都是她哭她笑的影子，车厢里还有她留下的味道，我答应过要好好照顾的女孩，就这样从我眼前消失，让我的心脏在冬末的马路上，发出了衰竭的声响。我去了危城，那里大门紧闭。我打电话给小五，手机里有狂风呼啸的声音，然后信号突然断掉。天开始下起很大的雨，浓雾已经笼罩了整个城市。我在雨中像一个无家可归的弃儿，一遍又一遍无助地拨打小五已经关掉的手机。天空又开始电闪雷鸣，我脑海中突然闪过一个念头，调转车头，向郊外的墓园驶去。瓢泼大雨不知疲倦地冲击着车身，我用几近疯狂的速度飙车，雨水在挡风玻璃上变成从下至上的逆向流动。在第九个路牌处，我拐下公路，把车停在墓园的空地前。那是我2002年最后一次见到流岚，我的格子围裙还穿在她身上，被雨淋湿，变成暗红。她站在那里，全身上下都透出寒意，面孔苍白没有一丝血色。雨水顺着她的头发向下奔跑，跑过她的面颊她尖翘的小下巴，小溪一样奔涌进她薄薄的黑毛衣里。她转身看着我说，严戈，他已经死了。我在那一刻知道，她已然醒来。只是这清醒多么残酷，连一片好的梦境都不再留给她。她握我的手像终年不化的寒冰，一直冷到我心里，在冬末的大雨里，宣告了一个梦境的终结。那夜我抱她离开墓园，我最后一次回望严歌的墓碑，那张黑白相片，已经变得模糊起来。流岚在车里打电话给常青，她说，来带我走吧。我2002年的记忆，便随着流岚的离开像突然抽成真空的器皿，变得纯澈干净起来。路枚不眠不休地照顾了我两个月，其间对流岚的事情只字不提，只是在写给我父母的信中说，请你们放心，我已经足够坚强，能够撑起这个家的重量。82003年2月，我的胃病愈发严重。到南方出差的途中疼痛难忍，决定去医院做一次彻底检查。我就这样再次见到流岚。但她显然已经忘记了我，只是漠然地看我一眼说，你进去吧。她确乎是已不再记得，在北方的某个城市里，在那个醉酒危城的冬夜，她在一个男人面前，曾经睡得像童话故事里的公主。2003年5月，报纸上说非典病例已经达到多少多少。全国很多医院已经有医护人员自愿组织医疗队伍，奔赴抗击非典的最前线。2003年10月，非典布下的阴云渐渐散去。12月，常青说，很多被感染的医护人员已经脱离危险。我仅听他提起过一次流岚，他说她到南方一家医院重新开始她的医护工作，并参加了抗击非典医护人员志愿队伍，像个长着翅膀的天使，把那些恐慌的人们纳入她的羽翼之下。2004年初春，我再次去了南方那家医院。但是流岚已经不在那里，她的同事小马私下里对我说，常老师介绍来的那个女孩，在去年8月被病人传染，感染上非典且永远地睡去。他在医院的天台上陷入了回忆，非常动情地说，你不知道，流岚是一个多么惹人喜爱的女孩。你没有见过她像天使一样的表情，你不会知道，当她穿着白大褂守候在你身边的时候，你会以为，全世界的幸福和安宁都在这里。2004年10月，路枚辞掉工作，回到我身边。我幸未碳化的戒指终于套在她指间，而我们的卧室是宁静的蓝，婴儿房是漂亮的粉。我的胃病渐渐好起来，路枚说要我好好爱惜身体。我所有关于流岚的记忆，只剩下日记本里一张有些模糊的照片而已。那上面，长发的流岚偎在我胸前，笑得如冬阳一般温暖。我常常在深夜醒来，再也无法安然入睡。窗外有月光投射进来，这时候，我就会想起一个叫流岚的女孩。这种想念会让我的心脏紧紧纠结，让我在逐渐朦胧的视线里，看见她收起翅膀，低头微笑的样子。夜将大白，我终于知道，童话完结，梦境荒芜。而我们的天使，已经离去。
值类型是数据的容器，它们不具备多太性。另一方面就是说，.Net框架被设计成单一继承的引用类型，System.Object，在整个继承关系中做为根对象存在。设计这两种类型的目的是截然不同的，.Net框架使用了装箱与拆箱来链接两种不同类型的数据。装箱是把一个值类型数据放置在一个无类型的引用对象上，从而使一个值类型在须要时可以当成引用类型来使用。拆箱则是额外的从“箱”上拷贝一份值类型数据。装箱和拆箱可以让你在须要使用System.Object对象的地方使用值类型数据。但装箱与拆箱操作却是性能的强盗，在些时候装箱与拆箱会产生一些临时对象，它会导致程序存在一些隐藏的BUG。应该尽可能的避免使用装箱与拆箱。装箱可以把一个值类型数据转化也一个引用类型，一个新的引用对象在堆上创建，它就是这个“箱子”，值类型的数据就在这个引用类型中存储了一份拷贝。参见图2.3，演示了装箱的对象是如何访问和存储的。箱子中包含一份这个值类型对象的拷贝，并且复制实现了已经装箱对象的接口。当你想从这个箱子中取回任何内容时，一个值类型数据的拷贝会被创建并返回。这就是装箱与拆箱的关键性概念：对象的一个拷贝存放到箱子中，而不管何时你再访问这个箱子时，另一个拷贝又会被创建。![EffectiveC\#](http://cdn.guqiankun.com/img/201309/20130923220358265625.jpg)\图2.3，值类型数据在箱子中。把一个值类型数据转化成一个System.Object的引用，一个无名的引用类型会被创建。值类型的数据就存储在这个无名的引用对象中，所有的访问方法都要通过这个箱子才能到达值类型数据存储的地方。最阴险的地方是这个装箱与拆箱很多时候是自动完成的!当你在任何一个期望类型是System.Object的地方使用值类型数据时，编译器会生成装箱与拆箱的语句。另外，当你通过一个接口指针来访问值类型数据时，装箱与拆箱也会发生。当你装箱时不会得到任何警告，即使是最简单的语句也一样。例如下面这个：Console.WriteLine("A few numbers:{0}, {1}, {2}", 25, 32, 50); 使用重载的Console.WriteLine函数须要一个System.Object类型的数组引用，整型是值类型，所以必须装箱后才能传给重载的WriteLine方法。唯一可以强制这三个整数成为System.Object对象的方法就是把它们装箱。另外，在WriteLine内部，通过调用箱子对象上的ToString()方法来到达箱子内部。某种意义上讲，你生成了这样的结构：int i =25; object o = i; // box Console.WriteLine(o.ToString()); 在WriteLine内部，下面的执行了下面的代码：object o; int i = ( int )o; // unbox string output = i.ToString( ); 你可能自己从来不会写这样的代码，但是，却让编译器自动从一个指定的类型转化为System.Object，这确实是你做的。编译器只是想试着帮助你，它想让你成功(调用函数)，它也很乐意在必要时候为你生成装箱和拆箱语句，从而把一个值类型数据转化成System.Object的实例。为了避免这么挑剔的惩罚，在使用它们来调用WriteLine之前，你自己应该把你的类型转化成字符串的实例。Console.WriteLine("A few numbers:{0}, {1}, {2}", 25.ToString(), 32.ToString(), 50.ToString()); (译注：注意，在自己调用ToString方法时，还是会在堆上创建一个引用实例，但它的好处是不用拆箱，因为对象已经是一个引用类型了。)这段代码使用已知的整数类型，而且值类型再也不会隐式的转化为System.Object类型。这个常见的例子展示了避免装箱的第一个规则：注意隐式的转化为System.Object，如果可以避免，值类型不应该被System.Object代替。另一个常见情况就是，在使用.Net1.x的集合时，你可能无意的把一个值类型转化成System.Object类型。任何时候，当你添加一个值类型数据到集合时中，你就创建了一个箱子。任何时候从集合中移出一个对象时，你得到的是箱子里的一个拷贝。从箱子里取一个对象时，你总是要创建一个拷贝。这会在应用程序中产生一些隐藏的BUG。编译器是不会帮你查找这些BUG的。这都是装箱惹的祸。让我们开始创建一个简单的结构，可以修改其中一个字段，并且把它的一些实例对象放到一个集合中：public struct Person { private string _Name; public string Name { get { return _Name; } set { _Name = value; } } public override string ToString( ) { Return _Name; } } // Using the Person in a collection: ArrayList attendees = new ArrayList( ); Person p = new Person( "Old Name" ); attendees.Add( p ); // Try to change the name: // Would work if Person was a reference type. Person p2 = (( Person )attendees[ 0 ] ); p2.Name = "New Name"; // Writes "Old Name": Console.WriteLine( attendees[ 0 ].ToString( )); Person是一个值类型数据，在存储到ArrayList之前它被装箱。这会产生一个拷贝。而在移出的Persone对象上通过访问属性做一些修改时，另一个拷贝被创建。而你所做的修改只是针对的拷贝，而实际上还有第三个拷贝通过ToString()方法来访问attendees[0]中的对象。正因为这以及其它一些原因，你应该创建一些恒定的值类型(参见原则7)。如果你非要在集合中使用可变的值类型，那就使用System.Array类，它是类型安全的。如果一个数组不是一个合理的集合，以C\#1.x中你可以通过使用接口来修正这个错误。尽量选择一些接口而不是公共的方法，来访问箱子的内部去修改数据：public interface IPersonName { string Name { get; set; } } struct Person : IPersonName { private string _Name; public string Name { get { return _Name; } set { _Name = value; } } public override string ToString( ) { return _Name; } } // Using the Person in a collection: ArrayList attendees = new ArrayList( ); Person p = new Person( "Old Name" ); attendees.Add( p ); // box // Try to change the name: // Use the interface, not the type. // No Unbox needed (( IPersonName )attendees[ 0 ] ).Name = "New Name"; // Writes "New Name": Console.WriteLine( attendees[ 0 ].ToString( )); // unbox 装箱后的引用类型会实现原数据类型上所有已经实现的接口。这就是说，不用做拷贝，你可以通过调用箱子上的IPersonaName.Name方法来直接访问请求到箱子内部的值类型数据。在值类型上创建的接口可以让你访问集合里的箱子的内部，从而直接修改它的值。在值类型上实现的接口并没有让值类型成为多态的，这又会引入装箱的惩罚(参见原则20)。在C\#2.0中对泛型简介中，很多限制已经做了修改(参见原则49)。泛型接口和泛型集合会时同处理好集合与接口的困境。在那之前，我们还是要避免装箱。是的，值类型可以转化为System.Object或者其它任何的接口引用。这些转化是隐式的，使得发现它们成为繁杂的工作。这些也就是环境和语言的规则，装箱与拆箱操作会在你不经意时做一些对象的拷贝，这会产生一些BUG。同样，把值类型多样化处理会对性能有所损失。时刻注意那些把值类型转化成System.Object或者接口类型的地方：把值类型放到集合里，调用定义参数为System.Object类型的方法，或者强制转化为System.Object。能够避免就尽量避免!