zoukankan      html  css  js  c++  java
  • Effective C# 原则41:选择DataSet而不是自定义的数据结构

    因为两个原则,把DataSet的名声搞的不好。首先就是使用XML序列化的DataSet与其它的非.Net代码进行交互时不方便。如果在Web服务的API中使用DataSet时,在与其它没有使用.Net框架的系统进行交互时会相当困难。其次,它是一个很一般的容器。你可以通过欺骗.Net框架里的一些安全类型来错误DataSet。但在现代软件系统中,DataSet还可以解决很多常规的问题。如果你明白它的优势,避免它的缺点,你就可以扩展这个类型了。

    DataSet类设计出来是为了离线使用一些存储在相关数据库里的数据。你已经知道它是用来存储DataTable的,而DataTable就是一个与数据库里的结构在行和列上进行匹配的内存表。或许你已经看到过一些关于DataSet支持在内部的表中建立关系的例子。甚至还有可能,你已经见过在DataSet里验证它所包含的数据,进行数据约束的例子。

    但不仅仅是这些,DataSet还支持AcceptChanges 和RejectChanges 方法来进行事务处理,而且它们可以做为DiffGrams存储,也就是包含曾经修改过的数据。多个DataSet还可以通过合并成为一个常规的存储库。DataSet还支持视图,这就是说你可以通过标准的查询来检测数据里的部份内容。而且视图是可以建立在多个表上的。

    然而,有些人想开发自己的存储结构,而不用DataSet。因DataSet是一个太一般的容器,这会在性能上有所损失。一个DataSet并不是一个强类型的存储容器,其实存储在里面的对象是一个字典。而且在里的表中的列也是字典。存储在里的元素都是以System.Object的引用形式存在。这使得我们要这样写代码:


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

    以C#强类型的观点来看,这样的结构是很麻烦的。如果你错误使用table1 或者total的类型,你就会得到一个运行时错误。访问里面的数据元素要进行强制转化。而这样的麻烦事情是与你访问里面的元素的次数成正比的,与其这样,我们还真想要一个类型化的解决方法。那就让我们来试着写一个DataSet吧,基于这一点,我们想要的是:

    int val = MyDataSet.table1.Rows[ 0 ].total;

    当你看明白了类型化的DataSet内部的C#实现时,就会知道这是完美的。它封装了已经存在的DataSet,而且在弱类型的访问基础上添加了强类型访问。你的用户还是可以用弱类型API。但这并不是最好的。

    与它同时存在的,我会告诉你我们放弃了多少东西。我会告诉你DataSet类里面的一些功能是如何实现的,也就是在我们自己创建的自定义集合中要使用的。你可能会觉得这很困难,或者你觉得我们根本用上不同DataSet的所有功能,所以,代码并不会很长。OK,很好,我会写很长的代码。

    假设你要创建一个集合,用于存储地址。每一个独立的元素必须支持数据绑定,所以你我创建一个具有下面公共属性的结构:


    public struct AddressRecord
    {
      private string _street;
      public string Street
      {
        get { return _street; }
        set { _street = value; }
      }

      private string _city;
      public string City
      {
        get { return _city; }
        set { _city = value; }
      }

      private string _state;
      public string State
      {
        get { return _state; }
        set { _state = value; }
      }

      private string _zip;
      public string Zip
      {
        get { return _zip; }
        set { _zip = value; }
      }
    }

    下面,你要创建这个集合。因为我们要类型安全的集合,所以我们要从CollectionsBase派生:


    public class AddressList : CollectionBase
    {
    }

    CollectionBase 支持IList 接口,所以你可以使用它来进行数据绑定。现在,你就发现了你的第一个问题:如果地址为空,你的所有数据绑定行就失败了。而这在DataSet里是不会发生的。数据绑定是由基于反射的迟后绑定代码组成的。控件使用反射来加载列表里的第一个元素,然后使用反射来决定它的类型以及这个类型上的所有成员属性。这就是为什么DataGrid可以知道什么列要添加。它会在集合中的第一个元素上发现所有的公共属性,然后显示他们。当集合为空时,这就不能工作了。你有两种可能来解决这个问题。第一个方法有点丑,但是一个简单的方法:那就是不充许有空列表存在。第二个好一些,但要花点时间:那就是实现ITypedList 接口。ITypedList 接口提供了两个方法来描述集合中的类型。GetListName 返回一个可读的字符串来描述这个列表。GetItemProperties 则返回PropertyDescriptors 列表,这是用于描述每个属性的,它要格式化在表格里的:

    public class AddressList : CollectionBase
    {
      public string GetListName( PropertyDescriptor[ ] listAccessors )
      {
        return "AddressList";
      }

      public PropertyDescriptorCollection   GetItemProperties(PropertyDescriptor[ ] listAccessors)
      {
        Type t = typeof( AddressRecord );
        return TypeDescriptor.GetProperties( t );
      }
    }

    这稍微好一点了,现在你你已经有一个集合可以支持简单的数据绑定了。尽管,你失去了很多功能。下一步就是要实现数据对事务的支持。如果你使用过DataSet,你的用户可以通过按Esc键来取消DataGrid中一行上所有的修改。例如,一个用户可能输入了错误的城市,按了Esc,这时就要原来的值恢复过来。DataGrid同样还支持错误提示。你可以添加一个ColumnChanged 事件来处理实际列上的验证原则。例如,州的区号必须是两个字母的缩写。使用框架里的DataSet,可以这样写代码:


    ds.Tables[ "Addresses" ].ColumnChanged +=new  DataColumnChangeEventHandler( ds_ColumnChanged );

    private void ds_ColumnChanged( object sender,  DataColumnChangeEventArgs e )
    {
      if ( e.Column.ColumnName == "State" )
      {
        string newVal = e.ProposedValue.ToString( );
        if ( newVal.Length != 2 )
        {
          e.Row.SetColumnError( e.Column,
            "State abbreviation must be two letters" );
          e.Row.RowError = "Error on State";
        }
        else
        {
          e.Row.SetColumnError( e.Column,"" );
          e.Row.RowError = "";
        }
      }
    }

    为了在我们自己定义的集合上也实现这样的概念,我们很要做点工作。你要修改你的AddressRecord 结构来支持两个新的接口,IEditableObject 和IDataErrorInfo。IEditableObject 为你的类型提供了对事务的支持。IDataErrorInfo 提供了常规的错误处理。为了支持事务,你必须修改你的数据存储来提供你自己的回滚功能。你可能在多个列上有错误,因此你的存储必须包含一个包含了每个列的错误集合。这是一个为AddressRecord做的更新的列表:

    public class AddressRecord : IEditableObject, IDataErrorInfo
    {
        private struct AddressRecordData
        {
          public string street;
          public string city;
          public string state;
          public string zip;
        }

        private AddressRecordData permanentRecord;
        private AddressRecordData tempRecord;

        private bool _inEdit = false;
        private IList _container;

        private Hashtable errors = new Hashtable();

        public AddressRecord( AddressList container )
        {
          _container = container;
        }

        public string Street
        {
          get
          {
            return ( _inEdit ) ? tempRecord.street :
              permanentRecord.street;
          }
          set
          {
            if ( value.Length == 0 )
              errors[ "Street" ] = "Street cannot be empty";
            else
            {
              errors.Remove( "Street" );
            }
            if ( _inEdit )
              tempRecord.street = value;
            else
            {
              permanentRecord.street = value;
              int index = _container.IndexOf( this );
              _container[ index ] = this;
            }
          }
        }

        public string City
        {
          get
          {
            return ( _inEdit ) ? tempRecord.city :
              permanentRecord.city;
          }
          set
          {
            if ( value.Length == 0 )
              errors[ "City" ] = "City cannot be empty";
            else
            {
              errors.Remove( "City" );
            }
            if ( _inEdit )
              tempRecord.city = value;
            else
            {
              permanentRecord.city = value;
              int index = _container.IndexOf( this );
              _container[ index ] = this;
            }
          }
        }

        public string State
        {
          get
          {
            return ( _inEdit ) ? tempRecord.state :
              permanentRecord.state;
          }
          set
          {
            if ( value.Length == 0 )
              errors[ "State" ] = "City cannot be empty";
            else
            {
              errors.Remove( "State" );
            }
            if ( _inEdit )
              tempRecord.state = value;
            else
            {
              permanentRecord.state = value;
              int index = _container.IndexOf( this );
              _container[ index ] = this;
            }
          }
        }

        public string Zip
        {
          get
          {
            return ( _inEdit ) ? tempRecord.zip :
              permanentRecord.zip;
          }
          set
          {
            if ( value.Length == 0 )
              errors["Zip"] = "Zip cannot be empty";
            else
            {
              errors.Remove ( "Zip" );
            }
            if ( _inEdit )
              tempRecord.zip = value;
            else
            {
              permanentRecord.zip = value;
              int index = _container.IndexOf( this );
              _container[ index ] = this;
            }
          }
        }
        public void BeginEdit( )
        {
          if ( ( ! _inEdit ) && ( errors.Count == 0 ) )
            tempRecord = permanentRecord;
          _inEdit = true;
        }

        public void EndEdit( )
        {
          // Can't end editing if there are errors:
          if ( errors.Count > 0 )
            return;

          if ( _inEdit )
            permanentRecord = tempRecord;
          _inEdit = false;
        }

        public void CancelEdit( )
        {
          errors.Clear( );
          _inEdit = false;
        }

        public string this[string columnName]
        {
          get
          {
            string val = errors[ columnName ] as string;
            if ( val != null )
              return val;
            else
              return null;
          }
        }

        public string Error
        {
          get
          {
            if ( errors.Count > 0 )
            {
              System.Text.StringBuilder errString = new
                System.Text.StringBuilder();
              foreach ( string s in errors.Keys )
              {
                errString.Append( s );
                errString.Append( ", " );
              }
              errString.Append( "Have errors" );
              return errString.ToString( );
            }
            else
              return "";
          }
        }
      }

    花了几页的代码来支持一些已经在DataSet里实现的了的功能。实际上,这还不能像DataSet那样恰当的工作。例如,交互式的添加一个新记录到集合中,以及支持事务所要求的BeginEdit, CancelEdit, 和EndEdit等。 你要在CancelEdit 调用时检测一个新的对象而不是一个已经修改了的对象。CancelEdit 必须从集合上移除这个新的对象,该对象应该是上次调用BeginEdit时创建的。对于AddressRecord 来说,还有很多修改要完成,而且一对事件还要添加到AddressList 类上。

    最后,就是这个IBindingList接口。这个接口至少包含了20个方法和属性,用于控件查询列表上的功能描述。你必须为只读列表实现IBindingList 或者交互排序,或者支持搜索。在你取得内容之前就陷于层次关系和导航关系中了。我也不准备为上面所有的代码添加任何例子了。
    几页过后,再问问你自己,还准备创建你自己的特殊集合吗?或者你想使用一个DataSet吗?除非你的集合是一个基于某些算法,对性能要求严格的集合,或者必须有轻便的格式,就要使用自己的DataSet,特别是类型化的DataSet。这将花去你大量的时间,是的,你可以争辩说DataSet并不是一个基于面向对象设计的最好的例子。类型化的DataSet甚至会破坏更多的规则。但,使用它所产生的代码开发效率,比起自己手写更优美的代码所花的时间,这只是其中一小部份

  • 相关阅读:
    python错误信息 object is not subscriptable 的原因
    python join函数
    string的部分总结
    第k个非立方数(忘记哪里的题了)
    pandas 学习
    数学建模中 时间序列典型分解模型 matlab实现
    matlab三维画图学习 三次插值
    原型
    JavaScript
    js数组去重(多种写法)
  • 原文地址:https://www.cnblogs.com/hongyin163/p/1562136.html
Copyright © 2011-2022 走看看