.NET中的视图和过滤器 (DefaultView和RowFiltNET中的视图和过滤器

本文详细介绍了ADO.NET中DataView的使用方法,包括如何创建DataView、设置过滤条件、排序及预排视图等内容。
ADO.NET中有一层对象,用来创建任意数据源的抽象模型。其中包括DataSet,DataTable,DataRow,DataView,DataRelation等等。

所有这些对象都定义在System.Data名字空间。它们形成一个抽象模型,使得无论针对Windows Form,Web Form还是Web Service进行编程,都可以使用相同的编程接口。
在实际应用中,这些对象大多会对诸如SQL Server一类的关系型数据库中的数据进行操作。但是,它们可以处理各种数据,而不管它的物理存储媒介。
你可以使用DataSet对象来打包和关联各表中的数据,用DataTable类来处理表格类型的数据,而DataRow对象可以处理表中某一行的数据。
这三个对象都是对数据进行打包,但有不同的逻辑聚合层次。DataSetDataTable和其他的组合。而DataTableDataRow和其他的组合。DataRow是字段和其他的组合。但是这些对象中都没有内建过滤和排序的功能。
ADO.NET提供了一些类来处理这个数据库应用程序中的重要方面。在.Net Beta2中,这方面最重要的两个对象就是DataViewDataViewManager
注意:DataViewManager是Beta2中特有的。在Beta1中,相应的功能由DataSetView完成。

定制数据视图
DataView类用来表示定制的DataTable的视图。DataTableDataView的关系是遵循著名的设计模式--文档/视图模式,其中DataTable是文档,而Dataview是视图。
在任何时候,你都可以有多个基于相同数据的不同的视图。更重要的是,你可以对每一个具有自己一套属性、方法、事件的视图作为独立的对象进行处理。这也代表了相对ADO一个巨大的飞跃。
ADO Recordset可以定义过滤字符串。一旦你建立了该字符窜,只有匹配特定标准的数据才能够进行读写。Filter属性的工作原理同动态WHERE子句很相似。它只是简单的在同一recordset对象视图上隐藏了某些记录。
在ADO中,你从没有一个独立的视图对象。一个过滤过的recordset总是同一个对象,只不过显示出的记录比它实际数量少一些而已。
如果你不需要同时处理一些不同的视图,上述问题并不要紧。编程接口赋予了recordset既可以是表也可以是视图的功能。但是在创建时,这不能同时发生。在某一特定时刻,recordset只能是没有过滤字符串的表或者是激活了过滤字符串的视图。
Recordset 的克隆提供了较好解决这个结构限制的方法。正如Clonation and the Case of Table Dolly, Part 1中所说的,克隆recordset相对开销较少,因为它不复制数据,只是复制recordset的基本构造。要处理同一数据两个或两个以上的视图,你可 以利用两个或两个以上克隆,各自有一套相应的过滤字符串。


图一 在ADO中处理同一recordset不同的视图
在ADO.NET中,你可以使用新型对象模型所提供的DataView对象。ADO.NET的DataView对象用来表示给定数据表的定制的视图,但你可以像处理单独的对象一样处理它。DataView对象保留了对表的一个引用并允许对它进行更新。


图二 在ADO.NET中对同一数据表的不同视图进行操作
功能上而言,使用ADO Recordset克隆与使用特殊的视图对象完成的是同样的功能,都是让你实现过滤,对所选的数据行进行操作,并同时处理多个视图。

深入DataView对象
DataView对象继承了MarshalByValueComponent并实现了一组接口使之在数据绑定控件中可用。
Public Class DataView
Inherits MarshalByValueComponent
Implements IBindingList, IList, ICollection,IEnumerable, _
ITypedList, ISupportInitialize

由MarshalByValueComponent派生的类是.NET远程组件,可以通过值来列集--即序列化对象到目标应用程序域。(详见以下关于.NET组件的更多细节)
DataView中的内容可以通过许多编程接口进行操作,包括集合,列表和枚举器。IBindingList接口确保了该类提供所有用来支持复杂的和简单的数据绑定的必要特征。
总的来说,DataView对象可以用来达到两个目的。第一,视图对于关联DataTable对象和数据绑定控件中的DataSource域是很重要的。第二,它也对连接的DataTable提供了一层包装,让你能够进行过滤,排序,编辑和浏览。
DataView并不是唯一的可以通过传值进行远程操作的数据驱动类。DataSet和DataTable也具有同样的能力,特别是在互操作的场景下。

创建DataView
public DataView();
public DataView(DataTable);

DataView只有同已经存在的、很可能是非空的DataTable对象连接后才可用。通常,这个连接在构造时就指定了。
DataView dv;
dv = new DataView(theDataSet.Tables["Employees"]);

但是,你也可以先创建一个新的视图,然后再用Table属性同表相关联。
DataView dv = new DataView();
dv.Table = theDataSet.Tables["Employees"];
DataView构造函数使你由DataTable中得到一个DataView对象。如果需要,反之亦可。事实上,DataTable对象的DefaultView属性返回一个该表的DataView对象。
DataView dv = dt.DefaultView;

一旦你有了DataView对象, 你可以利用它的属性来建立你希望用户见到的数据行集。一般,你可以使用下列属性:
  • RowFilter
  • Sort
前者可以定制视图中可见数据应匹配的规则。而后者通过表达式来进行排序。当然你可以使用这两者的任意组合。

设置过滤 RowFilter是一个可读写的属性,用来读取和设置表过滤的表达式。
public virtual string RowFilter {get; set;}

你可以用列名,逻辑和数字运算符和常量的任意合法组合组成表达式。以下是一些例子:
dv.RowFilter = "Country = 'USA'";
dv.RowFilter = "EmployeeID >5 AND Birthdate < #1/31/82#"
dv.RowFilter = "Description LIKE '*product*'"

让我们来看一下过滤器的基本规则和运算符。
过滤字符串是表达式的逻辑连接。可以用AND,OR,NOT来连接成一个较短的表达式,也可以使用圆括号来组成子句,指定优先的运算。
通常包含列名的子句同字母、数字、日期或另一个列名进行比较。这里,可以使用关系运算符和算术运算符,如>=, <, >, +, *, % (取模)等等。
如果要选取的行并不能方便地通过算术或逻辑运算符表达,你可以使用IN操作符。以下代码显示如何选取一个随机行:
dv.RowFilter = "employeeID IN (2,4,5)"

你也可以使用通配符*和%,它们同LIKE运算符一起使用时显得更有用。它们都表示任意数量的字符,可以相互替代使用。
请注意,如果在LIKE子句中已经有了*或%字符,你必须用方括号将其括起,以免歧义。如果很不幸,字符串中方括号本身也存在了,那么它也必须用将本身括起。这样,匹配语句会如下所示:
dv.RowFilter = "Description LIKE '[[]*[]]product[[]*[]]"

通配符只允许在过滤字符串的开头或结尾处使用,而不能在字符串中间出现。例如,下列语句会产生运行时错误:
dv.RowFilter = "Description LIKE 'prod*ct"

字符串必须以单引号括起,而日期型必须以#符号括起。字符型值可以使用小数点和科学计数法。
RowFilter也支持聚合函数,如SUM, COUNT, MIN,MAX, and AVG。如果表中没有数据行,那么函数将返回NULL。
在介绍RowFilter表达式的最后,让我们讨论三个很便利的函数:Len,IIF和Substring。
正如其名,Len()返回特定表达式的长度。该表达式可以是一个列名,也可以是其他合法的表达式。
Substring()返回指定的表达式自特定位置开始,特定长度的字符子串。
我最喜欢用的是IIF(),它按照逻辑表达式的值有一到两个值。IIF是IF-THEN-ELSE语句的紧凑表达。语法如下:
IIF(expression, if_true, if_false)

通 过该函数,可以建立非常复杂的过滤字符串。例如,假定你从SQL Server的Northwind数据库中取得Employees表,下列表达式可以选出那些employeeID小于6且lastname为偶数个字符 和employeeID大于6且lastname为奇数个字符的员工。
IIF(employeeID<6, Len(lastname) %2 =0, Len(lastname) %2 >0)

下图显示了结果(样品应用程序会在稍后讨论)

图三 对Northwind中的表进行过滤
例子程序是一个Windows® Form应用程序,其中使用了两个datagrid
控件来实现master/detail结构。一个grid在载入时生成,即在SQL
Server data adapter完成数据读取工作之后。请注意,data
adapter是Beta 2中引入的,在Beta 1中相应的是SQLDataSetCommand类。


预排视图
在上面的举例中,datagrid必须负责预排视图中的数据行,以便刷新用户界面。这个自动机制是.NET
数据绑定的产物。Datagrid是通过DataSource属性来获取数据的数据绑定控件。DataView是一个可数据绑定的类,可构建DataSource属性的内容。

如果你想使用datagrid之外的另一个控件,应该怎么办呢?又如果你不想使用自动数据绑定呢?应该怎样预排视图中所选的数据行呢?

DataView的Table属性指向相应的数据表,但DataTable并不保存过滤信息。所以,预排表中的数据注定是不可行的。虽然DataTableDataView是紧密相联的,但它们各自保持独立,并执行独立的功能。

以下Visual Basic .NET代码段显示了如何遍历视图中所有的数据行,并加入到listbox中。
Dim dv As New DataView()
dv = ds.Tables("Employees").DefaultView
dv.RowFilter = "employeeid >5"

ListBox1.Items.Clear()
Dim buf As String
Dim dr As DataRowView
For Each dr In dv
buf = ""
buf &= dr("lastname").ToString()& ", " & dr("firstName").ToString()
ListBox1.Items.Add(buf)
Next

正如前面说提到的,DataView是可枚举的类,因此你可以安全的将它传给For..Each语句。Count属性存储了视图中数据行数,以便在For..Next循环中使用。
要访问视图中某一行,可以使用DataRowView类。DataRowView可表示DataRow的视图,就像DataView表达DataTable定制的视图一样。
总的来说,DataRow最多有四种状态:default,original,current和proposed。这些状态由DataRowVersion枚举类型设置,由RowVersion属性表达。
DataRow的视图只能是其中某一种状态。
数据行的默认(default)版本只有当其列在构造时设定了默认值时才有。而初始(original)版本是指在最后一次调用表的AcceptChanges后,从数剧源中得到数据行或快照。当前(Current)版本是指当前的数据行,包括所有当时发生的更新。Proposed状态只存在于调用BeginEdit和EndEdit的编辑过程中。
可以通过访问DataRow相同的语法访问DataRowView。这里最重要的属性叫Item。

排序和其他便捷的特性
DataView支持Sort属性,可以用来对视图中的内容排序。Sort由用逗号分隔的列名表达式进行排序。通过在任何列名后加ASC或者DESC限定词,可以使得字段按照上升或者下降的顺序排列。如果没有方向限定词,默认顺序为ASC。
DataView是内存中的对象,所以排序在本地进行,无需调用数据库服务器。
RowStateFilter是DataView另一有趣的属性。它可以用任何预定义的标准来过滤DataTable中的内容。下表中是DataViewRowState枚举类型的所有取值:
CurrentRows包括所有未更新的、新的和修改的数据行
Deleted所有自上次调用AcceptChanges后删除的数据行
ModifiedCurrent所有自上次调用AcceptChanges后修改过的数据行
ModifiedOriginal所有自上次调用AcceptChanges后original版本的数据行
New所有自上次调用AcceptChanges后新添加的行
OriginalRows返回初始数据行,包含unchanged和deleted 的
Unchanged所有未更新的数据行

如果要操作非连接的数据,所有更新都在对DataTable调用AcceptChanges后生效。对单一行的更新在调用DataRowAcceptChanges后生效。类似的,这些更新可以通过调用DataTableDataRow对象的RejectChanges来取消。
DataView对象还有一些属性,如AllowEdit,AllowDeleteAllowNew,用来得到或设定是否允许更新的值。它们的默认值设为True,允许任何种类的更新。如果在标志设为False时,你想要完成相应的更新操作,会有一个运行时错误发生。

DataViewManager类
DataTable对象的DefaultView属性用来返回一个DataView对象,作为数据表中内容的默认视图。它按照自然顺序读取数据并显示表中所有的行,而不使用任何过滤。
theMasterGrid.DataSource = m_ds.Tables("Employees").DefaultView

如果需要数据特定的视图,你可以进行排序并/或对DefaultView对象直接进行过滤。
m_ds.Tables("Employees").DefaultView.Sort = "lastname"
theMasterGrid.DataSource = m_ds.Tables("Employees").DefaultView
DataViewManager类是用来存储DataSet中所有表的视图设置。
可以通过传递一个合法的非空的DataSet给类的构造函数来创建DataViewManager
Dim dvm As DataViewManager
dvm = New DataViewManager(m_ds)

也可以通过DataSet对象的DefaultViewManager属性直接得到:
Dim dvm As DataViewManager = m_ds.DefaultViewManager

重要的是DataViewManager类是同一个DataSet相关联的。下面是另一种可行的方法:
Dim dvm As New DataViewManager()
dvm.DataSet = m_ds

DataViewManager最重要的属性是DataViewSettings,一个DataViewSetting对象的集合。

Dim dvs As DataViewSetting
dvs = dvm.DataViewSettings("Employees")
dvs.Sort = "lastname"

DataViewSetting对象包含了表视图的参数信息。当将数据绑定到对数据敏感的控件时,使用DataViewManager而不是DataSet或DataTable可以保留你的视图设置(过滤和排序字段)

theMasterGrid.DataSource = dvm
theMasterGrid.DataMember = "Employees"

在这里,视图按照DataViewSetting中对Employees表指定的自动进行排序和过滤。换而言之,DataViewSetting类是对特定表的视图的一种缓存。

下一步
上述例子程序用filter实现了master/detail结构。如果使用.NET中特有的数据绑定控件(如datagrid),能够更好的达到这个目的。在以后的专栏中,我将论述内存中的数据关系,以及它们是如何影响master/detail结构的设计的。
对话:你是否需要控件或组件?

在.NET中有很多术语经常可以替代使用。这里特别指出的是:类,组件,对象和控件。在此,我提供了一张表,来表述每个术语的恰当的含义。我们经常将它们当成同义词。


需要牢记在心的是整个.NET架构是由类组成的。所以你从中得到的任何东西,首先,是一个类。在.NET环境中,控件和组件不是同一种类。至于对象,可以认为是运行着的.NET类的实例。
组件是一个特殊的类,它实现了Icomponent接口或派生于实现了Icomponent接口的类。
控件是提供了用户界面功能的组件。在.NET架构中,可以找到两类控件:客户端的Windows Forms 控件和ASP.NET server 控件。
Icomponent接口包含在Idisposable接口中,并提供了一种确定的方法清除资源。
Public Interface IComponent
Inherits IDisposable


这种释放资源的方法和标准的.NET垃圾收集器可以二者选一。通过实现Idisposable,你定义了一个Dispose方法。这样通过编程,你可以显式的释放对象而无须等待垃圾收集器来处理。
.NET 组件知道怎样在应用程序域(application domain)中如何串联。这有两种方法:通过引用或通过值,基本功能分别内建于MarshalByRefComponet和 MarshalByValueComponent类中。.NET component类,事实上,实现了Idisposable,但直接或间接继承了上述两个类中的一个。
应用程序域是一种轻量级进程。通过引用来列集对象意味着proxy/stub实体对会被创建并处理远程调用。而通过值则意味着该对象的序列化的拷贝传递越过域的边界。
控件是更特殊化的对象,它还提供了用户界面元素。当然,一个控件总是一个component,但反之不一定成立。 

 

 

在 实际编程工程中,常常遇到这样的情况:DataTable并不是数据库中的,或者DataTable尚未写到数据库,或者从数据库中读出的 DataTable已经在本地被改动,又没有写回数据库(可能还要作其他改动),在这些情况下,要查询DataTable中的数据,强大的SQL语言就派 不上用场了。
   
  有些.NET程序员采取在数据库中建立临时表等方法来解决这类查询问题。而我觉得这种方法不可行,其实只要 用.NET类库中提供的DataView类的强大功能(主要是用它的RowFilter属性),就能方便地解决这类查询问题。下面就举一个具体的例子,来 说明如何不用SQL语句,用DataView的RowFilter属性来查询。
   
  步骤一:建立一个C#的ASP.NET项目。先编写一个生成DataTable的函数MakeDataTable(),代码如下:
   
   private DataTable MakeTable()
   
   {
   
   //生成DataTable
   
   System.Data.DataTable myDataTable = new DataTable("本地数据表");
   
   DataColumn myDataColumn;
   
   DataRow myDataRow;
   
   //生成数据列 ID,商品名称,商品价格
   
   myDataColumn = new DataColumn();
   
   myDataColumn.DataType = System.Type.GetType("System.Int32");
   
   myDataColumn.ColumnName = "ID";
   
   myDataColumn.ReadOnly = true;
   
   myDataColumn.Unique = true;
   
   myDataTable.Columns.Add(myDataColumn);
   
   
   
   myDataColumn = new DataColumn();
   
   myDataColumn.DataType = System.Type.GetType("System.String");
   
   myDataColumn.ColumnName = "商品名称";
   
   myDataTable.Columns.Add(myDataColumn);
   
   
   
   myDataColumn = new DataColumn();
   
   myDataColumn.DataType =System.Type.GetType("System.Decimal");
   
   myDataColumn.ColumnName = "商品价格";
   
   myDataTable.Columns.Add(myDataColumn);
   
   
   
   //为数据表添加数据行
   
   myDataRow = myDataTable.NewRow();
   
   myDataRow["id"] = 1;
   
   myDataRow["商品名称"] = "足球";
   
   myDataRow["商品价格"] =57.5;
   
   myDataTable.Rows.Add(myDataRow);
   
   
   
   myDataRow = myDataTable.NewRow();
   
   myDataRow["id"] = 2;
   
   myDataRow["商品名称"] = "篮球";
   
   myDataRow["商品价格"] =64.5;
   
   myDataTable.Rows.Add(myDataRow);
   
   
   
   myDataRow = myDataTable.NewRow();
   
   myDataRow["id"] = 3;
   
   myDataRow["商品名称"] = "网球";
   
   myDataRow["商品价格"] =6.5;
   
   myDataTable.Rows.Add(myDataRow);
   
   
   
   myDataRow = myDataTable.NewRow();
   
   myDataRow["id"] = 4;
   
   myDataRow["商品名称"] = "网球拍";
   
   myDataRow["商品价格"] =388.5;
   
   myDataTable.Rows.Add(myDataRow);
   
   
   
   //返回数据表
   
   return myDataTable;
   
   }   
   
   
  步骤二:在aspx前台页面中添加一个DataGrid1,在后台代码的Page_Load中编写如下代码:
   
  if(!this.IsPostBack)
   
   {
   
   Session["Table"]=MakeTable();
   
   DataGrid1.DataSource=(DataTable)Session["Table"];
   
   DataGrid1.DataBind();
   
   }  
   
   
  此时浏览页面,可以看到DataGrid中显示的表中数据。
   
  ID
   
   
  商品名称
   
   
  商品价格
   
  1
   
   
  足球
   
   
  57.5
   
  2
   
   
  篮球
   
   
  64.5
   
  3
   
   
  网球
   
   
  6.5
   
  4
   
   
  网球拍
   
   
  388.5
   
  步骤三:在aspx前台页面添加一个用来查询Label1,TextBox1和Button1。如下图所示:
   
  步骤四:在查询按钮Button1单击事件中添加如下代码:
   
   DataTable dt=(DataTable)Session["Table"];
   
   //为数据表建立一个数据视图
   
   DataView dv=new DataView(dt);
   
   //用RowFilter属性进行模糊查询
   
   dv.RowFilter="商品名称 LIKE'%"+TextBox1.Text.Trim()+"%'";
   
   DataGrid1.DataSource=dv;
   
   DataGrid1.DataBind();
   
  运行页面后在TextBox1中输入“网”,按查询按钮后 ,DataGrid显示的查询结果如下:
   
   
   
  ID
   
   
  商品名称
   
   
  商品价格
   
  3
   
   
  网球
   
   
  6.5
   
  4
   
   
  网球拍
   
   
  388.5
   
  步骤五:在aspx前台页面上添加一个Label2控件,两个TextBox控件-TextBox1和TextBox2,以及一个“查询”按钮,Button2,如下图所示,用来查询用户输入的价格范围中在商品。
   
  步骤六:在后台代码中为Button2按钮单击事件添加如下如下代码:
   
  DataTable dt=(DataTable)Session["Table"];
   
  DataView dv=new DataView(dt);
   
   dv.RowFilter="商品价格>="+TextBox2.Text.Trim()+" AND 商品价格<="+TextBox3.Text.Trim();
   
   DataGrid1.DataSource=dv;
   
   DataGrid1.DataBind();
   
   
   
  用户在两个文本框中输入价格范围,10,70后,DataGrid1中显示的查询结果如下:
   
   
   
  ID
   
   
  商品名称
   
   
  商品价格
   
  1
   
   
  足球
   
   
  57.5
   
  2
   
   
  篮球
   
   
  64.5
   
   
   
  可见,用DataView的RowFilter属性完全能达到SQL语句SELECT语句所实现的功能
   
  RowFilter中的查询语句与SQL语句中SELECT语句的语法和作用都极为相似,以下是摘自MSDN中关于RowFilter查询语句的语法说明:
   
  /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
   
  用户定义的值可以用在将与列值进行比较的表达式内。字符串值应放在单引号内。日期值应放在磅符号 (#) 内。对于数值,允许使用小数和科学记数法。例如: 
    
  "FirstName = 'John'"  
   
  "Price <= 50.00"    
   
  "Birthdate < #1/31/82#"    
   
  对于包含枚举值的列,将值强制转换为整数数据类型。例如: 
   
  "EnumColumn = 5"    
   
  运算符    
   
  使用布尔值 AND、OR 和 NOT 运算符时允许串联。可以使用括号来组合子句和强制优先级。AND 运算符优先于其他运算符。例如: 
   
  (LastName = 'Smith' OR LastName = 'Jones') AND FirstName = 'John'    
   
  在创建比较表达式时,允许使用下列运算符: 
   
  < 
   
  >   
   
  <=    
   
  >=   
   
  <> 
    
  = 
     
  IN 
     
  LIKE 
      
  在表达式中还支持下列算术运算符: 
      
  +(加) 
     
  -(减) 
      
  *(乘) 
      
  /(除) 
      
  %(模数) 
      
  字符串运算符 
      
  若要连接字符串,请使用 + 字符。字符串比较是否区分大小写由 DataSet 类的 CaseSensitive 属性的值来确定。但是,可以用 DataTable 类的 CaseSensitive 属性重写此值。 
     
  通配符 
      
   在 LIKE 比较中,* 和 % 两者可以互换地作为通配符。如果 LIKE 子句中的字符串包含 * 或 %,那么这些字符应用中括号([])对其进行转义。如果子句中有中括号,那么中括号字符应用中括号对其进行转义(例如 [[] 或 []])。在模式的开头和结尾,或者在模式的结尾,或在模式的开头允许使用通配符。例如: 
      
  "ItemName LIKE '*product*'" 
      
  "ItemName LIKE '*product'" 
      
  "ItemName LIKE 'product*'" 
      
  在字符串的中间不允许使用通配符。例如,不允许 'te*xt'。 
     
  父/子关系引用 
      
  通过在列名称前面加 Parent,就可以在表达式中引用父表。例如,Parent.Price 引用父表的名为 Price 的列。 
     
  通过在列名称前面加一个 Child,就可以在表达式中引用子表中的列。但是,因为子关系可以返回多行,所以必须在聚合函数中包括对子列的引用。例如,Sum(Child.Price) 将返回子表中名为 Price 的列的总和。 
      
  如果某个表有多个子表,则语法是:Child(RelationName)。例如,如果某个表有两个子表,它们的名称分别为 Customers 和 Orders,则 DataRelation 对象被命名为 Customers2Orders,引用将为: 
      
  Avg(Child(Customers2Orders).Quantity) 
      
  聚合 
      
  支持下列聚合类型: 
      
  Sum(求和) 
      
  Avg(平均) 
      
  Min(最小值) 
      
  Max(最大值) 
      
  Count(计数) 
      
  StDev(统计标准偏差) 
      
  Var(统计方差)。 
     
  聚合通常沿着关系执行。通过使用上面列出的函数之一和上面“父/子关系引用”中详述的子表列,来创建聚合表达式。例如: 
     
  Avg(Child.Price) 
      
  Avg(Child(Orders2Details).Price) 
      
  聚合也可以在单个表上执行。例如,若要为名为“Price”的列中的数字创建汇总,就用: 
      
  Sum(Price)   

 
<think>我们有一个DataTable,需要根据特定字段去重生成新的DataTable。去重意味着对于指定字段,重复的值只保留一条记录(可以是第一条或最后一条,通常我们保留第一条)。 方法: 1. 使用Linq to DataSet:我们可以将DataTable转换成Enumerable,然后使用Linq的Distinct方法,但Distinct默认比较引用,所以我们需要提供自定义比较器,或者使用GroupBy来分组取第一条。 2. 使用DataTable的DefaultView的ToTable方法,该方法有一个重载可以实现去重。 这里我们使用第二种方法,因为更简单且效率较高。具体步骤如下: a. 为DataTable创建一个DataView。 b. 设置DataView的Sort属性,按要去重的字段排序(这样相同的值会排在一起,然后ToTable方法会根据这个排序去重)。 c. 调用DataView的ToTable方法,该方法有一个重载可以指定要保留哪些列以及根据哪些列去重。 具体来说,ToTable方法的这个重载: DataTable DataView.ToTable(bool distinct, params string[] columnNames); 参数: distinct: 是否去除重复行,为true时,只保留 distinct 值(根据后面指定的列)。 columnNames: 要包含在新表中的列的名称。如果指定,则新表只包含这些列;如果不指定,则包含所有列。 注意:去重依据是columnNames指定的列,当distinct为true时,对于columnNames指定的列,值相同的行将只保留一行(在排序后,保留的是每一组重复行中的第一行)。 但是,这里有一个前提:在调用ToTable之前,需要根据去重字段对DataView进行排序。因为ToTable方法在去重时,会按照当前行的顺序,保留重复组中的第一行。所以,如果我们没有排序,那么去重结果可能是不确定的(因为重复的行可能不连续)。实际上,在调用ToTable方法时,如果distinct为true,它会根据指定的列(也就是columnNames)来去重,但内部会按照当前视图的排序顺序来保留第一行。因此,为了确保去重结果的一致性,我们通常先按去重字段排序。 然而,在DataView中,我们可以通过设置Sort属性来排序。所以步骤是: - 创建DataView,设置Sort属性为要去重的字段(可以多个字段,用逗号分隔)。 - 然后调用ToTable(true, 要去重的字段) ? 这样不行,因为这样新表就只有去重字段了。 实际上,我们想要的是新表包含原表的所有列,但根据指定字段去重。所以,我们应该在ToTable方法中指定要包含的所有列(或者不指定,表示包含所有列),同时设置distinct为true,并且只根据我们想去重的字段来去重。但是,ToTable方法的重载中,distinct参数的作用是:当distinct为true时,它会根据我们传入的columnNames数组中的所有列进行去重(注意:这里传入的列名既是新表包含的列,也是去重的依据)。如果我们传入所有列,那么只有所有列都相同才会去重,这显然不是我们想要的(我们只想根据特定字段去重)。 因此,我们需要使用另一个重载: DataTable DataView.ToTable(string tableName, bool distinct, params string[] columnNames); 这个重载允许我们指定新表的名字,同时指定distinctcolumnNames。 但是,我们想要的是新表包含原表所有列,而只根据指定字段去重。所以,我们可以这样: - 在columnNames参数中传入所有列,但这样去重依据就是所有列,不符合要求。 - 或者,我们只传入要去重的列,但这样新表就只有这些列了,也不符合要求。 所以,这个方法似乎不行。 另一种方法:使用Linq。 我们可以使用Linq的GroupBy,按指定字段分组,然后从每组中取第一条记录,然后把这些记录组成一个新的DataTable。 步骤: 1. 将DataTable转换成Enumerable(使用AsEnumerable())。 2. 使用GroupBy按指定字段分组。 3. 选择每组的第一个元素(First())。 4. 将结果复制到一个新的DataTable(使用CopyToDataTable())。 注意:CopyToDataTable方法要求源序列是DataRow的集合,并且不能为空(如果为空,需要处理异常)。另外,分组后取第一条,需要确保分组字段相同,然后取第一条(这里第一条是原表中的顺序,所以如果原表没有排序,那么取的是原表中该分组字段第一次出现的行)。 示例代码: // 假设要去重的字段名为"FieldName" DataTable distinctTable = dt.AsEnumerable() .GroupBy(row => row.Field<DataType>("FieldName")) .Select(group => group.First()) .CopyToDataTable(); 其中,DataType是字段的实际类型,比如int, string等。 但是,这种方法需要知道字段的类型,并且如果DataTable中没有数据,调用CopyToDataTable会抛出异常,所以需要先判断是否有行。 另外,我们也可以根据多个字段去重,这时GroupBy可以按匿名类型分组。 例如,按字段"Field1""Field2"去重: DataTable distinctTable = dt.AsEnumerable() .GroupBy(row => new { Field1 = row.Field<Type1>("Field1"), Field2 = row.Field<Type2>("Field2") }) .Select(group => group.First()) .CopyToDataTable(); 但是,这样写需要知道每个字段的类型,并且如果字段很多,代码会很长。 另一种使用Linq的方法:使用Distinct,但需要自定义一个比较器(实现IEqualityComparer<DataRow>),这样比较麻烦。 所以,我们选择使用GroupBy的方法。 但是,如果不想指定类型,我们可以使用`row["FieldName"]`来分组,但是这样分组依据是对象,而不同行的字段值可能类型相同但值不同(因为boxed的值比较的是引用吗?不,值类型会按值比较,但如果是引用类型,则比较引用,所以字符串可以,因为字符串是引用类型但重写了Equals)。所以,对于字符串、数值类型等,直接使用row["FieldName"]分组是可以的,因为它们的值相等时Equals返回true。 所以,可以这样写(不指定类型): DataTable distinctTable = dt.AsEnumerable() .GroupBy(row => row["FieldName"]) .Select(group => group.First()) .CopyToDataTable(); 但是,这样写可能会因为类型转换而出现问题吗?实际上,分组依据是对象,当对象是值类型时,会被装箱,但Equals方法对于值类型会比较值,所以对于同类型的值,比较是没问题的。但是,如果字段包含DBNull.Value,那么分组时DBNull.Value会被视为相同的(因为DBNull是单例),所以也是可以的。 但是,如果我们有多个字段,那么我们可以使用: DataTable distinctTable = dt.AsEnumerable() .GroupBy(row => new { Field1 = row["Field1"], Field2 = row["Field2"] }) .Select(group => group.First()) .CopyToDataTable(); 因为匿名类型会重写EqualsGetHashCode,所以比较也是按属性值比较。 但是,CopyToDataTable要求序列中的元素是DataRow,并且序列不能为空。所以,如果原表没有行,我们需要处理。 完整代码: if (dt.Rows.Count == 0) { // 返回一个空表,结构相同 return dt.Clone(); } else { // 按指定字段分组取第一条 // 假设按单个字段"FieldName"去重 var distinctRows = dt.AsEnumerable() .GroupBy(row => row["FieldName"]) .Select(group => group.First()); // 将结果复制到新表 DataTable distinctTable = distinctRows.CopyToDataTable(); return distinctTable; } 但是,如果去重字段有多个,则修改GroupBy部分。 另外,我们也可以使用DataView的ToTable方法,但需要一点技巧:我们可以先按去重字段排序,然后只选择去重字段,并去重,得到去重字段的唯一值,然后再根据这些唯一值去原表取第一条记录?这样比较麻烦。 所以,我们采用Linq的方法。 但是,如果不想使用Linq(比如.NET版本较低),我们可以使用循环遍历DataTable的行,用一个字典记录已经出现过的键值,然后复制没有重复的行。 方法3(循环字典): DataTable distinctTable = dt.Clone(); // 复制结构 var seenKeys = new Dictionary<object, bool>(); // 用于记录已经出现的键 // 如果多个字段,则键可以是多个字段的组合,比如使用Tuple或者拼接字符串(但拼接字符串可能不保险,因为不同值拼接后可能相同) // 或者使用一个自定义的键对象(比如Tuple<object, object>),但需要知道字段类型?不,我们可以用object数组,但比较时比较每个元素。 // 这里以单个字段为例: foreach (DataRow row in dt.Rows) { object key = row["FieldName"]; if (!seenKeys.ContainsKey(key)) { seenKeys[key] = true; // 标记为已出现 distinctTable.ImportRow(row); // 导入行 } } // 如果是多个字段,则: // 假设字段数组:string[] fields = new string[] { "Field1", "Field2" }; // 那么键可以是一个对象数组,但对象数组比较时是引用比较,所以不行。我们可以使用一个自定义的键对象,比如Tuple,但Tuple需要知道字段数量,并且需要知道类型(但我们可以用object)。我们可以创建一个Tuple,Tuple的元素是object,然后Tuple重写了EqualsGetHashCode,所以可以。 // 例如: // 定义:var seenKeys = new Dictionary<Tuple<object, object>, bool>(); // 两个字段 // 然后:Tuple<object, object> key = Tuple.Create(row["Field1"], row["Field2"]); // 但是,如果字段数量不固定,我们就不能使用Tuple了,因为Tuple是固定长度的。 // 另一种方法:使用字符串拼接,但可能不可靠(比如两个不同的值拼接后相同,如"a","bc""ab","c"拼接后都是"abc")。所以,我们可以使用一个分隔符,但这样也不保险(如果字段值包含分隔符)。 // 所以,对于多字段,我们可以使用一个自定义的键,比如使用一个字符串,但将每个字段的值用特殊方式表示(比如转义),但这样复杂。 // 或者,我们可以使用一个字典,键为字段名的组合,然后比较每个字段的值。但这样需要遍历字段,效率低。 // 因此,在字段数量固定的情况下,使用Tuple是可行的。如果字段数量不固定,那么使用Linq的方法更简单。 综上所述,我们推荐使用Linq的方法。 但是,用户可能使用的是.NET Framework 3.5或以上(因为Linq to DataSet需要3.5),所以一般情况下可以使用。 如果用户不能使用Linq,那么我们可以使用循环字典,但只支持固定数量的字段(比如一个字段,或者两个字段用Tuple,三个用Tuple.Create(item1,item2,item3)等)。 根据引用,我们并没有直接看到去重的方法,但引用[3]中提到了使用DataTable.Select方法进行过滤,但过滤不等于去重。 所以,我们决定使用Linq方法。 代码示例(单个字段): public DataTable DistinctByField(DataTable dt, string fieldName) { if (dt.Rows.Count == 0) { return dt.Clone(); } // 使用Linq var distinctRows = dt.AsEnumerable() .GroupBy(row => row[fieldName]) .Select(g => g.First()); DataTable result = distinctRows.CopyToDataTable(); return result; } 多个字段(字段名数组): public DataTable DistinctByFields(DataTable dt, params string[] fieldNames) { if (dt.Rows.Count == 0) { return dt.Clone(); } // 使用匿名对象分组 var distinctRows = dt.AsEnumerable() .GroupBy(row => { // 创建匿名对象 var obj = new System.Dynamic.ExpandoObject(); var dict = (IDictionary<string, object>)obj; foreach (string field in fieldNames) { dict[field] = row[field]; } return obj; }) .Select(g => g.First()); // 但是,匿名对象在GroupBy中不能使用动态创建的ExpandoObject,因为ExpandoObject没有重写EqualsGetHashCode(按属性值比较),所以不行。 // 替代方案:使用Tuple,但是Tuple需要固定数量,而fieldNames是动态数组。我们可以根据字段数量创建不同的Tuple,但这样需要写很多分支,不现实。 // 另一种:使用字符串拼接(但不可靠)或者自定义一个比较器(但这样GroupBy就需要传入比较器,而且还要动态生成键)。 // 所以,我们使用另一种方法:使用Select方法,为每一行创建一个对象数组作为键,然后使用一个自定义的相等比较器(比较数组的每个元素)。 // 但是,这样代码较复杂。 // 或者,我们使用循环,在循环中动态生成键(字符串或数组),然后使用字典去重。 // 这里我们使用循环字典,键为对象数组,但对象数组比较需要自定义,所以我们可以将对象数组转换为一个可以比较的键,比如使用每个值的字符串表示(ToString),但这样也不可靠(因为不同对象ToString可能相同)。 // 因此,我们使用循环,并使用一个自定义的键结构:我们可以使用一组值的哈希码组合,但比较时比较每个值是否相等。 // 由于在循环中,我们可以逐行比较,所以我们可以这样: DataTable distinctTable = dt.Clone(); var seenKeys = new Dictionary<object, bool>(); // 这个字典不能用于多个字段 // 所以,我们使用一个列表来存储已经出现过的键(每个键是一个对象数组),然后每次遍历行时,检查该行的键(对象数组)是否在列表中。但是,这样效率低(O(n^2))。 // 更好的方法:使用一个HashSet,但需要自定义相等比较器。我们可以创建一个比较器,比较两个对象数组(长度相同)的每个元素是否相等。 // 定义比较器 class ArrayComparer : IEqualityComparer<object[]> { public bool Equals(object[] x, object[] y) { if (x.Length != y.Length) return false; for (int i = 0; i < x.Length; i++) { if (!object.Equals(x[i], y[i])) return false; } return true; } public int GetHashCode(object[] obj) { int hash = 17; foreach (object o in obj) { hash = hash * 31 + (o != null ? o.GetHashCode() : 0); } return hash; } } // 然后: var seenKeys = new HashSet<object[]>(new ArrayComparer()); foreach (DataRow row in dt.Rows) { object[] key = fieldNames.Select(fn => row[fn]).ToArray(); if (!seenKeys.Contains(key)) { seenKeys.Add(key); distinctTable.ImportRow(row); } } // 但是,这里有一个问题:我们每次创建一个新的key数组,而HashSet的比较会使用我们提供的比较器,所以可以判断是否包含相同的key(按值比较)。 // 但是,注意:这样每行都会创建一个数组,如果行数很多,可能会有性能问题。 // 另外,如果字段很多,也会创建很多数组。 // 所以,在字段数量固定且不多的情况下,使用Tuple是更好的选择,因为Tuple是值类型(实际上是引用类型,但不可变,且重写了EqualsGetHashCode),而且可以避免创建数组。 // 但是,字段数量不固定,所以我们只能使用数组。 // 因此,我们写一个使用循环HashSet(带自定义比较器)的方法。 // 但是,由于用户可能要求多个字段,并且我们不想在方法内部写死比较器,所以我们这样实现: public DataTable DistinctByFields(DataTable dt, params string[] fieldNames) { DataTable distinctTable = dt.Clone(); if (dt.Rows.Count == 0) return distinctTable; // 自定义比较器 var comparer = new ArrayComparer(); var seenKeys = new HashSet<object[]>(comparer); foreach (DataRow row in dt.Rows) { object[] key = fieldNames.Select(fn => row[fn]).ToArray(); if (seenKeys.Add(key)) // 如果成功添加(说明之前没有),则导入行 { distinctTable.ImportRow(row); } } return distinctTable; } // 其中ArrayComparer的定义(内部类): private class ArrayComparer : IEqualityComparer<object[]> { public bool Equals(object[] x, object[] y) { if (ReferenceEquals(x, y)) return true; if (x == null || y == null) return false; if (x.Length != y.Length) return false; for (int i = 0; i < x.Length; i++) { if (!object.Equals(x[i], y[i])) return false; } return true; } public int GetHashCode(object[] obj) { if (obj == null) return 0; int hash = 17; foreach (object o in obj) { hash = hash * 31 + (o != null ? o.GetHashCode() : 0); } return hash; } } 但是,这种方法在字段很多或行数很多时,性能可能不如Linq(因为Linq的GroupBy内部也使用了哈希表,但可能优化得更好)。而且,我们每次都要生成一个键数组。 因此,如果可以使用Linq,我们更推荐使用Linq的GroupBy方法,并且对于动态字段,我们可以使用以下技巧:使用System.Tuple来动态创建元组是不行的,因为Tuple.Create只能支持1到8个参数。所以,我们可以使用一个第三方库(如ValueTuple)或者使用反射创建元组,但这样复杂。 所以,在动态字段的情况下,我们使用循环+HashSet(自定义比较器)的方法。 综上所述,我们提供两种方法: 1. 对于单个字段,使用简单的Linq(GroupBy)方法。 2. 对于多个字段,使用循环+自定义比较器的方法。 但为了统一,我们可以都使用循环+自定义比较器的方法,这样代码一致。 或者,我们可以使用Linq,但动态创建分组键(使用ExpandoObject)并提供一个自定义的相等比较器,但这样也很复杂。 因此,我们决定使用循环+自定义比较器的方法,因为它可以处理任意数量的字段。 代码实现: public DataTable DistinctDataTable(DataTable dt, params string[] fieldNames) { DataTable result = dt.Clone(); if (dt.Rows.Count == 0) return result; // 创建自定义比较器 IEqualityComparer<object[]> comparer = new ArrayEqualityComparer(); HashSet<object[]> seenKeys = new HashSet<object[]>(comparer); foreach (DataRow row in dt.Rows) { object[] key = new object[fieldNames.Length]; for (int i = 0; i < fieldNames.Length; i++) { key[i] = row[fieldNames[i]]; } if (seenKeys.Add(key)) // 如果该键是新的,则添加成功 { result.ImportRow(row); } } return result; } private class ArrayEqualityComparer : IEqualityComparer<object[]> { public bool Equals(object[] x, object[] y) { if (x == null) return y == null; if (y == null) return false; if (x.Length != y.Length) return false; for (int i = 0; i < x.Length; i++) { if (!object.Equals(x[i], y[i])) return false; } return true; } public int GetHashCode(object[] obj) { int hash = 17; foreach (object o in obj) { hash = hash * 31 + (o == null ? 0 : o.GetHashCode()); } return hash; } } 注意:这个方法在去重字段很多或数据量很大时,性能可能不是最优,但对于一般数据量是可行的。 另外,如果字段值为DBNull,那么object.Equals会处理,因为DBNull.Value是单例,所以比较是相等的。 因此,我们推荐使用这个方法。 但是,用户可能希望保留哪一行?我们这里保留的是第一次出现的那一行(因为按原表顺序遍历,第一次遇到某个键时保留,后面相同的键就跳过)。如果用户希望保留最后一次出现的,那么可以倒序遍历,或者先反转顺序,但通常我们保留第一次出现的。 最后,我们根据用户问题,提供代码示例。 用户问题:从DataTable中根据特定字段去重并生成新的DataTable 所以,我们写一个方法,传入DataTable要去重的字段名(一个或多个),返回去重后的DataTable。 代码示例: using System; using System.Collections.Generic; using System.Data; public class DataTableHelper { public static DataTable DistinctDataTable(DataTable dt, params string[] fieldNames) { if (dt == null || dt.Rows.Count == 0) { return dt?.Clone() ?? new DataTable(); } DataTable result = dt.Clone(); // 如果fieldNames为空,则返回空表(或者返回整个表?但通常去重字段不能为空) if (fieldNames == null || fieldNames.Length == 0) { return result; // 返回空表 } var seenKeys = new HashSet<object[]>(new ArrayEqualityComparer()); foreach (DataRow row in dt.Rows) { object[] key = new object[fieldNames.Length]; for (int i = 0; i < fieldNames.Length; i++) { key[i] = row[fieldNames[i]]; } if (seenKeys.Add(key)) { result.ImportRow(row); } } return result; } private class ArrayEqualityComparer : IEqualityComparer<object[]> { public bool Equals(object[] x, object[] y) { if (ReferenceEquals(x, y)) return true; if (x == null || y == null) return false; if (x.Length != y.Length) return false; for (int i = 0; i < x.Length; i++) { if (!object.Equals(x[i], y[i])) return false; } return true; } public int GetHashCode(object[] obj) { unchecked { int hash = 17; foreach (object o in obj) { hash = hash * 23 + (o != null ? o.GetHashCode() : 0); } return hash; } } } } 调用示例: DataTable newTable = DataTableHelper.DistinctDataTable(oldTable, "Field1"); // 或者多个字段 DataTable newTable2 = DataTableHelper.DistinctDataTable(oldTable, "Field1", "Field2"); 注意:这个方法在数据量大时可能会有性能问题,因为每个键是一个数组,而且我们计算了数组的哈希码(遍历了数组)。如果字段很多,键数组很长,则计算哈希码的代价较高。但在数据量不是特别大且字段不是特别多的情况下,可以接受。 如果用户的数据量很大,可以考虑使用其他方法,比如使用数据库(如果数据来自数据库,最好在查询时去重)。 因此,我们提供这个方法作为内存中去重的解决方案。 最后,根据用户要求,生成相关问题。 相关问题: 1. 如何根据多个字段去重? 2. 去重时如何选择保留哪一行(比如保留最后一行)? 3. 如果去重字段包含空值(DBNull)会怎样? 4. 有没有更高效的方法去重? 5. 如何根据某个字段的值排序后再去重?</think>### C# DataTable 根据指定字段去重生成新 DataTable 在 C# 中,从 `DataTable` 中根据指定字段去重并生成新 `DataTable` 的核心思路是:**遍历原始表的每一行,提取指定字段的值作为键,通过哈希表记录唯一键值,仅导入首次出现的行**。以下是两种实现方法: --- #### 方法 1:使用循环哈希表(推荐) ```csharp using System; using System.Collections.Generic; using System.Data; public static DataTable DistinctDataTable(DataTable sourceTable, params string[] distinctFields) { // 创建新表(结构相同) DataTable resultTable = sourceTable.Clone(); if (sourceTable.Rows.Count == 0 || distinctFields.Length == 0) return resultTable; // 用于记录已出现的键组合 HashSet<string> seenKeys = new HashSet<string>(); foreach (DataRow row in sourceTable.Rows) { // 生成当前行的键(拼接字段值) string key = ""; foreach (string field in distinctFields) { key += $"{row[field]}|"; // 用分隔符连接字段值 } // 若键未出现过,则导入行 if (!seenKeys.Contains(key)) { seenKeys.Add(key); resultTable.ImportRow(row); } } return resultTable; } ``` **调用示例**: ```csharp // 根据单个字段去重 DataTable newTable1 = DistinctDataTable(sourceTable, "CategoryID"); // 根据多个字段去重 DataTable newTable2 = DistinctDataTable(sourceTable, "CategoryID", "ProductName"); ``` --- #### 方法 2:使用 LINQ(需要 .NET 3.5+) ```csharp using System.Data; using System.Linq; public static DataTable DistinctDataTableLinq(DataTable sourceTable, string distinctField) { DataTable resultTable = sourceTable.Clone(); if (sourceTable.Rows.Count == 0) return resultTable; // 分组取第一条记录 var distinctRows = sourceTable.AsEnumerable() .GroupBy(row => row[distinctField]) .Select(group => group.First()); foreach (DataRow row in distinctRows) { resultTable.ImportRow(row); } return resultTable; } ``` --- ### 关键说明 1. **去重逻辑**: - 保留首次出现的行(后续重复行被忽略)。 - 字段值为 `DBNull` 时视为相同(`null` 或空值会归为同一组)[^4]。 2. **性能考虑**: - 方法 1 使用字符串哈希,适合字段值长度适中的场景。 - 方法 2 的 LINQ 简洁但需类型转换,仅支持单字段去重。 3. **特殊处理**: - 若需保留最后一行,可将原始表反转(`sourceTable.AsEnumerable().Reverse()`)。 - 若字段值含分隔符 `|`,需在方法 1 中替换为非常用字符(如 ``)。 --- ### 示例场景 假设原始表 `dtProducts`: | ProductID | CategoryID | ProductName | |-----------|-----------|-------------| | 1 | 100 | Apple | | 2 | 100 | Orange | | 3 | 200 | Apple | 调用 `DistinctDataTable(dtProducts, "CategoryID")` 结果: | ProductID | CategoryID | ProductName | |-----------|-----------|-------------| | 1 | 100 | Apple | | 3 | 200 | Apple | 调用 `DistinctDataTable(dtProducts, "ProductName")` 结果: | ProductID | CategoryID | ProductName | |-----------|-----------|-------------| | 1 | 100 | Apple | | 2 | 100 | Orange | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值