程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> Effective C#原則41:選擇DataSet而不是自定義的數據結構

Effective C#原則41:選擇DataSet而不是自定義的數據結構

編輯:關於C#

因為兩個原則,把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甚至會破壞更多的規則。但,使用它所 產生的代碼開發效率,比起自己手寫更優美的代碼所花的時間,這只是其中一小 部份。

返回教程目錄

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved