Effective C#之Item 41:Prefer DataSets to Custom Structures

  rel="File-List" href="file:///C:%5CDOCUME%7E1%5CHelios%5CLOCALS%7E1%5CTemp%5Cmsohtmlclip1%5C01%5Cclip_filelist.xml"> rel="themeData" href="file:///C:%5CDOCUME%7E1%5CHelios%5CLOCALS%7E1%5CTemp%5Cmsohtmlclip1%5C01%5Cclip_themedata.thmx"> rel="colorSchemeMapping" href="file:///C:%5CDOCUME%7E1%5CHelios%5CLOCALS%7E1%5CTemp%5Cmsohtmlclip1%5C01%5Cclip_colorschememapping.xml">

Item 41: Prefer DataSets to Custom Structures

优先选择DataSet而不是自定义结构

DataSets have gotten a bad reputation for two reasons. First, XML serialized DataSets do not interact well with non-.NET code. Using DataSets as part of a web service API makes it more difficult to interact with systems that don't use the .NET Framework. Second, they are a very generic container. You can misuse a DataSet by circumventing some of the .NET Framework's type safety. But the DataSet still solves a large number of common requirements for modern systems. If you understand its strengths and avoid its weaknesses, you can make extensive use of the type.

DataSet得了个臭名声,因为2个原因。首先,XML序列化过的DataSet不能和非.Net代码很好的交互。将DataSet作为web服务API的一部分,那么和没有使用.Net框架的系统交互起来更困难。第二,它们是非常通用的容器。被.Net框架的类型安全性所欺骗,你可能会误用DataSet。但是DataSet仍然解决了现代系统很多常用的需求。如果你理解了它的长处,避免它的弱点,那么就可以很好的利用该类型了。

The DataSet class is designed to be an offline cache of data stored in a relational database. You already know that it stores DataTables, which store rows and columns of data that can match the layout of a database. You know that the DataSet and its members support data binding. You might even have seen examples of how the DataSet supports relations between the DataTables it contains. It's even possible that you've seen examples of constraints that validate the data being placed in a DataSet.

DataSet类被设计为存储相关数据库的离线数据缓存。你已经知道它是用来存储DataTable的,DataTable则用来存储数据的行和列,这些数据能够和数据库的布局相匹配。你知道DataSet和它的成员支持数据绑定。你可能见过这样的例子:DataSet如何支持它包含的DataTable的关系,甚至可能见过进行约束的例子:验证置于DataSet里面的数据。

But there's even more than that. Datasets also support transactions through the AcceptChanges and RejectChanges methods, and they can be stored as DiffGrams that contain the history of changes to the data. Multiple Datasets can be merged to provide a common storage repository. DataSets support views, which enable you to examine portions of your data that satisfy search criteria. You can create views that cross several tables.

但是还有更多。DataSet也支持通过AcceptChangesRejectChanges方法进行事务处理,它们也可以作为DiffGrams来存储,包含对数据所做的修改的历史。多个DataSet可以被合并来提供通用的存储仓库。DataSet支持视图,使你可以检查满足查询要求的数据的一部分,你可以跨越多个表格创建视图。

Yet, some of us want to develop our own storage structures rather than use the DataSet. The DataSet is a general container. Performance suffers a little to support that generality. A DataSet is not a strongly typed container. The collection of DataTables is a dictionary. The collection of columns in a table is also a dictionary. Items are stored as System.Object references. That leads us to write these kinds of constructs:

是的,我们中的一些人想开发自己的存储结构,而不是使用DataSetDataSet是通用的容器。为了支持通用性,多少牺牲了一些性能。DataSet不是强类型容器。DataTable的集合是一个字典。表格中的列的集合同样也是字典。元素都以System.Object引用存储。这就让我们编写出下面这样的结构:

  1. int val = ( int )MyDataSet.Tables[ "table1" ].
  2.   Rows[ 0 ][ "total" ];

To the strongly typed C# mind, this construct is troublesome. If you mistype table1 or total, you get a runtime error. An access to the data element requires a cast. If you multiply these problems by the number of times you access the elements of a DataSet, you can really want to find a strongly typed solution. So we try typed DataSets. On the surface, it's what we want:

C#里面的强类型概念看,该结构很麻烦。如果你搞错了table或者total的类型,将会得到运行时错误。对数据元素的访问要求强制转换。如果用该问题乘以访问DataSet中元素的次数,那么你将很想找到一个强类型的解决方法。因此我们尝试类型化的DataSet。从表面上看,这正是我们想要的:

  1. int val = MyDataSet.table1.
  2.   Rows[ 0 ].total;
  3.  
It's perfect until you look inside the generated C# that comprises the typed DataSet. It wraps the existing DataSet and provides strongly typed access in addition to the weakly typed access in the DataSet class. Your clients can still access the weakly typed API. That's less than optimal.

如果你不看内部生成的C#代码(这些代码组成了类型化的DataSet),那么这简直是完美的。它对现存的DataSet进行了包装,除了DataSet类的弱类型访问,它提供了强类型访问。你的客户仍然能访问弱类型的API,所以这还不是最佳的。

Live with it. To illustrate how much you give up, I'll show you how some of the features inside the DataSet class are implemented, in the context of creating your own custom collection. You're thinking that it can't be that hard. You're thinking that you don't need all the features of the DataSet, so it won't take that long. Okay, fine, I'll play along.

与这同时存在的还有其他问题。为了展示你放弃了多少。在创建自定义的集合的上下文中,我将向你展示DataSet类内部的一些特性是如何实现的。你正在想它不可能那么难,你不需要DataSet的所有特性,因此不会花费太多时间。嘿嘿,好吧,其实我会写很长。

Imagine that you need to create a collection that stores addresses. An individual item must support data binding, so you create a struct with public properties:

假设你需要创建一个用来存储地址的集合。单独的元素应该支持数据绑定,因此你创建了具有公共属性的结构体:

  1. public struct AddressRecord
  2. {
  3.   private string _street;
  4.   public string Street
  5.   {
  6.     get { return _street; }
  7.     set { _street = value; }
  8.   }
  9.  
  10.   private string _city;
  11.   public string City
  12.   {
  13.     get { return _city; }
  14.     set { _city = value; }
  15.   }
  16.  
  17.   private string _state;
  18.   public string State
  19.   {
  20.     get { return _state; }
  21.     set { _state = value; }
  22.   }
  23.  
  24.   private string _zip;
  25.   public string Zip
  26.   {
  27.     get { return _zip; }
  28.     set { _zip = value; }
  29.   }
  30. }

 

Next, you need to create the collection. You want a type-safe collection, so you derive one from CollectionsBase:

接下来,你需要创建集合了。你希望它是一个类型安全的集合,因此从CollectionsBase进行派生:

  1. public class AddressList : CollectionBase
  2. {
  3. }

 

CollectionBase supports the IList interface, so you can use it as a data-binding source. Now you discover your first serious problem: All your data-binding actions fail when your list of addresses is empty. That did not happen with the Dataset. Data binding consists of late-binding code built on reflection. The control uses reflection to load the first element in the list, and then uses reflection to determine its type and all the properties that are members of that type. That's how a DataGrid learns what columns to add. It finds all the public properties of the first element in the collection, and those are displayed. When the collection is empty, that won't work. You have two possible solutions to this problem. The first is the ugly but simple solution: Never allow an empty list. The second is the elegant but more time-consuming solution: Implement the ITypedList interface. ITypedList provides two methods that describe the types in the collection. GetListName returns a human-readable string that describes the list. GetItemProperties returns a list of PropertyDescriptors that describe each property that should form a column in the grid:

CollectionBase支持IList接口,因此你可以将它作为数据绑定源来使用。现在你发现自己的第一个严重问题:当你的地址列表为空的时候,所有数据绑定动作都会失败。这在DataSet上不会发生。数据绑定由构建在反射上的晚期绑定所组成。控件使用反射加载列表中的第一个元素,然后使用反射来决定它的类型和所有作为该类型成员的属性。这就是DataGrid如何学会要添加哪个列。它寻找集合里面第一个元素的所有公共属性,对它们进行显示。当集合为空时,就不行了。对于该问题由2个可能的解决方案。第一个方法有点丑陋但是简单:决不允许空列表。第二个方法很优雅但是比较费时间:实现ITypedList接口。ITypedList提供了2个描述集合中类型的方法。GetListName返回一个描述列表的可读的字符串。GetItemProperties返回一个PropertyDescriptors的列表,该列表描述了应该形成网格(grid)中的列的每个属性。

  1. public class AddressList : CollectionBase
  2. {
  3.   public string GetListName( PropertyDescriptor[ ] listAccessors )
  4.   {
  5.     return "AddressList";
  6.   }
  7.  
  8.   public PropertyDescriptorCollection  GetItemProperties( PropertyDescriptor[ ] listAccessors)
  9.   {
  10.     Type t = typeof( AddressRecord );
  11.     return TypeDescriptor.GetProperties( t );
  12.   }
  13. }

 

It's getting a little better. Now you have a collection that supports simple binding. You're missing a lot of features, though. The next requested feature is transaction support. If you had used a DataSet, your users would be able to cancel all changes to a single row in the DataGrid by pressing the Esc key. For example, a user could type the wrong city, press Esc, and have the original value restored. The DataGrid also supports error notification. You could attach a ColumnChanged event handler to perform any validation rules you need on a particular column For instance, the state code must be a two-letter abbreviation. Using the DataSet framework, that's coded like this:

这比前面好了一些了。。尽管你漏掉了很多特性,但是现在你有了一个支持简单绑定的集合。接下来需要的特性是事务支持。如果你使用过DataSet,那么就知道:用户将能够在一个DataGrid里面通过Esc键来取消一个单独行里面所有的修改。例如,用户可能输入了错误的城市,按下Esc,将会恢复到原来的值。DataGrid同时也支持错误通知。你可以添加一个ColumnChanged事件,在特定的列上面执行自己需要的任何验证规则。例如,州代码应该是2个字符的缩写。使用DataSet框架的话,它的代码将会像下面这样:

  1. ds.Tables[ "Addresses" ].ColumnChanged +=new
  2.   DataColumnChangeEventHandler( ds_ColumnChanged );
  3.  
  4. private void ds_ColumnChanged( object sender, DataColumnChangeEventArgs e )
  5. {
  6.   if ( e.Column.ColumnName == "State" )
  7.   {
  8.     string newVal = e.ProposedValue.ToString( );
  9.     if ( newVal.Length != 2 )
  10.     {
  11.       e.Row.SetColumnError( e.Column, "State abbreviation must be two letters" );
  12.       e.Row.RowError = "Error on State";
  13.     }
  14.     else
  15.     {
  16.       e.Row.SetColumnError( e.Column,"" );
  17.       e.Row.RowError = "";
  18.     }
  19.   }
  20. }
  21.  

To support both concepts on your custom collection, you have quite a bit more work ahead of you. You need to modify your AddressRecord structure to support two new interfaces, IEditableObject and IDataErrorInfo. IEditableObject provides transaction support for your object. IDataErrorInfo provides the error-handling routines. To support the transactions, you must modify your data storage to provide your own rollback capability. You might have errors on multiple columns, so your storage must also include a collection of errors for each column. Here's the updated listing for the AddressRecord:

为了在自定义集合上同时支持这2种概念,还有很多工作在等着你。你需要修改AddressRecord结构体来支持这两个新接口:IEditableObjectIDataErrorInfoIEditableObject为你的对象提供事务支持。IDataErrorInfo提供错误处理子程序。为了支持事务处理,你应该修改数据存储,来提供自己的回滚能力。你可能会在多列上出现错误,因此,你的存储也应该是包含每个列的错误的集合。下面是AddressRecord升级后的代码清单:

  1. public class AddressRecord : IEditableObject, IDataErrorInfo
  2. {
  3.     private struct AddressRecordData
  4.     {
  5.       public string street;
  6.       public string city;
  7.       public string state;
  8.       public string zip;
  9.     }
  10.  
  11.     private AddressRecordData permanentRecord;
  12.     private AddressRecordData tempRecord;
  13.  
  14.     private bool _inEdit = false;
  15.     private IList _container;
  16.  
  17.     private Hashtable errors = new Hashtable();
  18.  
  19.     public AddressRecord( AddressList container )
  20.     {
  21.       _container = container;
  22.     }
  23.  
  24.     public string Street
  25.     {
  26.       get
  27.       {
  28.         return ( _inEdit ) ? tempRecord.street : permanentRecord.street;
  29.       }
  30.       set
  31.       {
  32.         if ( value.Length == 0 )
  33.           errors[ "Street" ] = "Street cannot be empty";
  34.         else
  35.         {
  36.           errors.Remove( "Street" );
  37.         }
  38.         if ( _inEdit )
  39.           tempRecord.street = value;
  40.         else
  41.         {
  42.           permanentRecord.street = value;
  43.           int index = _container.IndexOf( this );
  44.           _container[ index ] = this;
  45.         }
  46.       }
  47.     }
  48.  
  49.     public string City
  50.     {
  51.       get
  52.       {
  53.         return ( _inEdit ) ? tempRecord.city : permanentRecord.city;
  54.       }
  55.       set
  56.       {
  57.         if ( value.Length == 0 )
  58.           errors[ "City" ] = "City cannot be empty";
  59.         else
  60.         {
  61.           errors.Remove( "City" );
  62.         }
  63.         if ( _inEdit )
  64.           tempRecord.city = value;
  65.         else
  66.         {
  67.           permanentRecord.city = value;
  68.           int index = _container.IndexOf( this );
  69.           _container[ index ] = this;
  70.         }
  71.       }
  72.     }
  73.  
  74.     public string State
  75.     {
  76.       get
  77.       {
  78.         return ( _inEdit ) ? tempRecord.state : permanentRecord.state;
  79.       }
  80.       set
  81.       {
  82.         if ( value.Length == 0 )
  83.           errors[ "State" ] = "City cannot be empty";
  84.         else
  85.         {
  86.           errors.Remove( "State" );
  87.         }
  88.         if ( _inEdit )
  89.           tempRecord.state = value;
  90.         else
  91.         {
  92.           permanentRecord.state = value;
  93.           int index = _container.IndexOf( this );
  94.           _container[ index ] = this;
  95.         }
  96.       }
  97.     }
  98.  
  99.     public string Zip
  100.     {
  101.       get
  102.       {
  103.         return ( _inEdit ) ? tempRecord.zip : permanentRecord.zip;
  104.       }
  105.       set
  106.       {
  107.         if ( value.Length == 0 )
  108.           errors["Zip"] = "Zip cannot be empty";
  109.         else
  110.         {
  111.           errors.Remove ( "Zip" );
  112.         }
  113.         if ( _inEdit )
  114.           tempRecord.zip = value;
  115.         else
  116.         {
  117.           permanentRecord.zip = value;
  118.           int index = _container.IndexOf( this );
  119.           _container[ index ] = this;
  120.         }
  121.       }
  122.     }
  123.     public void BeginEdit( )
  124.     {
  125.       if ( ( ! _inEdit ) && ( errors.Count == 0 ) )
  126.         tempRecord = permanentRecord;
  127.       _inEdit = true;
  128.     }
  129.  
  130.     public void EndEdit( )
  131.     {
  132.       // Can't end editing if there are errors:
  133.       if ( errors.Count > 0 )
  134.         return;
  135.  
  136.       if ( _inEdit )
  137.         permanentRecord = tempRecord;
  138.       _inEdit = false;
  139.     }
  140.  
  141.     public void CancelEdit( )
  142.     {
  143.       errors.Clear( );
  144.       _inEdit = false;
  145.     }
  146.  
  147.     public string this[string columnName]
  148.     {
  149.       get
  150.       {
  151.         string val = errors[ columnName ] as string;
  152.         if ( val != null )
  153.           return val;
  154.         else
  155.           return null;
  156.       }
  157.     }
  158.  
  159.     public string Error
  160.     {
  161.       get
  162.       {
  163.         if ( errors.Count > 0 )
  164.         {
  165.           System.Text.StringBuilder errString = new System.Text.StringBuilder();
  166.           foreach ( string s in errors.Keys )
  167.           {
  168.             errString.Append( s );
  169.             errString.Append( ", " );
  170.           }
  171.           errString.Append( "Have errors" );
  172.           return errString.ToString( );
  173.         }
  174.         else
  175.           return "";
  176.       }
  177.     }
  178.   }

 

That's several pages of code all to support features already implemented in the DataSet. In fact, this still doesn't have all the DataSet features working properly. Interactively adding new records to the collection and supporting transactions require some more hoops for BeginEdit, CancelEdit, and EndEdit. You need to detect when CancelEdit is called on a new object rather than a modified object. CancelEdit must remove the new object from the container if the object was created after that last BeginEdit. It requires more modification to the AddressRecord and a couple event handlers added to the AddressList class.

这是几页的代码,用来支持在DataSet里面已经实现的特性。实际上,这仍然还不能像DataSet的所有特性那样很好的工作。交互性的向集合添加一个记录,同时要支持事务处理,对于BeginEdiCancelEditEndEdit还要有更多的循环。当CancelEdit在一个新对象上被调用,而不是在一个修改过的对象上被调用时,你需要进行检测。如果一个对象在BeginEdit 之后被创建,那么CancelEdit应该移除该新对象。这需要对AddressRecord进行更多的修改,还需要向AddressList类添加一对事件处理句柄。

Finally, there's the IBindingList interface. This interface contains more than 20 methods and properties that controls query to describe the capabilities of the list. You must implement IBindingList for read-only lists or interactive sorting, or to support searching. That's before you get to anything involving navigation and hierarchies. I'm not even going to add an example of all that code.

最后,还有IBindingList接口。该接口包含多于20个方法和属性,它们控制对列表功能描述的查询。你应该为只读列表、交互式排序或者搜索支持实现IBindingList(为实现该接口)在你取得内容之前就陷于层次关系和导航关系中了。我也不准备为上面所有的代码添加例子了。

Several pages later, ask yourself, do you still want to create your own specialized collection? Or do you want to use a DataSet? Unless your collection is part of a performance-critical set of algorithms or must have a portable format, use the DataSet especially the typed DataSet. It will save you tremendous amounts of time. Yes, you can argue that the DataSet is not the best example of object-oriented design. Typed DataSets break even more rules. But this is one of those times when your productivity far outweighs what might be a more elegant hand-coded design.

几页过后,问问自己,还准备创建你自己的特定集合吗?或者你想使用DataSet吗?除非你的集合是算法中对性能要求严格的一部分,或者必须有轻便的格式,否则就使用DataSet,特别是类型化的DataSet。这将节省你大量的时间。是的,你可以争辩说DataSet并不是面向对象设计的最好的例子。类型化的DataSet甚至破坏更多的规则。当你的生产效率远远重于手写的优美硬编码时,使用DataSet这就是那样的情况之一。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值