DataTable的基本结构与创建
DataTable 是 C# 中 System.Data 命名空间提供的内存数据表对象,常用于保存查询结果、临时数据以及需要进行筛选、排序和修改的数据集合。它不依赖数据库连接,数据加载到内存后即可直接操作,因此在桌面应用、Web 项目、数据转换以及报表处理中都比较常见。
使用 DataTable 前通常需要引入:
C#using System.Data;
最简单的创建方式如下:
C#DataTable table = new DataTable("Users"); table.Columns.Add("Id", typeof(int)); table.Columns.Add("Name", typeof(string)); table.Columns.Add("Age", typeof(int)); table.Rows.Add(1, "张三", 25); table.Rows.Add(2, "李四", 30); table.Rows.Add(3, "王五", 28);
此时表结构可以理解为:
| Id | Name | Age |
|---|---|---|
| 1 | 张三 | 25 |
| 2 | 李四 | 30 |
| 3 | 王五 | 28 |
Columns 用于管理列,Rows 用于管理数据行。实际开发中,查询和行操作基本都围绕这两个集合展开。
使用 Select 查询DataTable中的数据
DataTable.Select() 是进行条件查询最直接的方法,可以按照类似 SQL WHERE 子句的表达式筛选数据。
例如查询年龄大于 25 岁的数据:
C#DataRow[] rows = table.Select("Age > 25"); foreach (DataRow row in rows) { Console.WriteLine($"{row["Id"]} - {row["Name"]} - {row["Age"]}"); }
也可以组合多个条件:
C#DataRow[] rows = table.Select("Age >= 25 AND Name <> '张三'");
字符串条件需要使用单引号:
C#DataRow[] rows = table.Select("Name = '李四'");
如果条件中的字符串来自用户输入,应避免直接拼接表达式,否则可能出现特殊字符导致查询表达式异常。对于复杂业务条件,更适合先使用 LINQ 查询。
DataTable按条件查询单行
如果明确知道条件最多只会匹配一条记录,可以获取数组后判断:
C#DataRow[] rows = table.Select("Id = 2"); if (rows.Length > 0) { DataRow row = rows[0]; Console.WriteLine(row["Name"]); }
需要注意,Select() 即使没有匹配结果,也不会返回 null,而是返回长度为 0 的 DataRow[]。
因此不要直接写:
C#DataRow row = table.Select("Id = 100")[0];
如果不存在 Id 为 100 的数据,就会抛出 IndexOutOfRangeException。
使用LINQ查询DataTable
对于比较复杂的筛选逻辑,LINQ 通常比 Select() 更灵活。
C#using System.Linq; var result = table.AsEnumerable() .Where(row => row.Field<int>("Age") >= 25); foreach (DataRow row in result) { Console.WriteLine(row.Field<string>("Name")); }
这里的 AsEnumerable() 可以将 DataTable 转换为可进行 LINQ 操作的数据序列。
Field 相比直接使用:
C#row["Age"]
具有更明确的类型信息。
例如:
C#int age = row.Field<int>("Age"); string name = row.Field<string>("Name");
如果数据库字段可能存在 DBNull,则应使用可空类型:
C#int? age = row.Field<int?>("Age");
这种方式可以避免直接转换 DBNull.Value 引发异常。
查询DataTable中的指定列
如果只需要获取某一列,可以结合 LINQ:
C#var names = table.AsEnumerable() .Select(row => row.Field<string>("Name")); foreach (string name in names) { Console.WriteLine(name); }
也可以同时选择多个字段:
C#var users = table.AsEnumerable() .Select(row => new { Id = row.Field<int>("Id"), Name = row.Field<string>("Name"), Age = row.Field<int>("Age") }); foreach (var user in users) { Console.WriteLine($"{user.Id} {user.Name} {user.Age}"); }
这种方式适合后续需要继续排序、分组或转换数据的场景。
DataTable排序查询
Select() 本身支持第二个参数指定排序表达式:
C#DataRow[] rows = table.Select("", "Age DESC");
第一个参数为空字符串表示不设置筛选条件,第二个参数按照年龄降序排列。
也可以同时排序:
C#DataRow[] rows = table.Select("", "Age DESC, Name ASC");
表示先按照 Age 降序排列,年龄相同的情况下再按照 Name 升序排列。
使用 LINQ 时则可以写成:
C#var result = table.AsEnumerable() .OrderByDescending(row => row.Field<int>("Age")) .ThenBy(row => row.Field<string>("Name"));
添加DataRow的方法
向 DataTable 添加数据最简单的方法是直接使用 Rows.Add():
C#table.Rows.Add(4, "赵六", 35);
如果需要更加明确地操作每一个字段,可以创建 DataRow:
C#DataRow newRow = table.NewRow(); newRow["Id"] = 5; newRow["Name"] = "孙七"; newRow["Age"] = 26; table.Rows.Add(newRow);
NewRow() 会按照当前 DataTable 的结构创建一个新的 DataRow,适合字段较多或者需要动态赋值的场景。
修改DataRow中的数据
获取到 DataRow 后,可以直接通过列名修改:
C#DataRow[] rows = table.Select("Id = 2"); if (rows.Length > 0) { rows[0]["Age"] = 31; }
也可以使用 LINQ 找到目标行:
C#DataRow? row = table.AsEnumerable() .FirstOrDefault(r => r.Field<int>("Id") == 2); if (row != null) { row["Age"] = 31; }
修改后,DataTable 中对应的行会立即发生变化。
删除DataTable中的数据行
删除指定行有多种方式。
直接通过索引删除:
C#table.Rows.RemoveAt(0);
如果已经拿到了 DataRow,可以:
C#DataRow[] rows = table.Select("Id = 3"); if (rows.Length > 0) { table.Rows.Remove(rows[0]); }
还可以调用:
C#rows[0].Delete();
不过 Delete() 和 Remove() 在行为上存在区别。
Delete() 会将行状态标记为 Deleted,特别是在 DataAdapter 与数据源同步时,这种状态具有实际意义。Rows.Remove() 则会直接将行从 DataTable.Rows 集合中移除。
因此,如果 DataTable 后续还需要通过 DataAdapter.Update() 同步数据库,应根据具体的数据更新流程选择删除方式。
批量删除满足条件的数据
如果需要删除多个符合条件的行,不建议在遍历 Rows 的同时直接删除当前集合元素。
例如可以先查询:
C#DataRow[] rows = table.Select("Age < 25"); foreach (DataRow row in rows) { row.Delete(); }
如果不需要保留 Deleted 状态,也可以直接:
C#foreach (DataRow row in rows) { table.Rows.Remove(row); }
这样可以避免直接修改 Rows 集合时产生遍历问题。
获取DataTable的行数
获取当前行数非常简单:
C#int count = table.Rows.Count;
例如:
C#if (table.Rows.Count == 0) { Console.WriteLine("没有数据"); } else { Console.WriteLine($"共有 {table.Rows.Count} 条数据"); }
需要注意,如果表中存在已经被标记为 Deleted 的行,某些场景下直接使用 Rows.Count 与业务意义上的“有效数据数量”并不完全相同。涉及数据修改状态时,应结合 DataRowState 判断。
判断DataTable是否存在指定数据
如果只是判断是否存在某条记录,没有必要把所有数据都查询出来。
LINQ 可以使用 Any():
C#bool exists = table.AsEnumerable() .Any(row => row.Field<int>("Id") == 2); if (exists) { Console.WriteLine("记录存在"); }
如果使用 Select():
C#bool exists = table.Select("Id = 2").Length > 0;
对于只关心“有没有”的场景,Any() 表达意图更加直接。
遍历DataTable所有行
最常见的遍历方式是:
C#foreach (DataRow row in table.Rows) { Console.WriteLine(row["Name"]); }
如果需要遍历所有列:
C#foreach (DataRow row in table.Rows) { foreach (DataColumn column in table.Columns) { Console.Write($"{row[column]} "); } Console.WriteLine(); }
这种方式适合字段结构不固定的动态数据处理。
判断DataRow中的DBNull
DataTable 中的数据库空值通常表示为 DBNull.Value,而不是 C# 的 null。
错误示例:
C#string name = row["Name"].ToString();
虽然 ToString() 在很多情况下不会直接报错,但无法明确表达字段是否真正存在数据库空值。
更稳妥的写法:
C#if (row.IsNull("Name")) { Console.WriteLine("Name为空"); } else { Console.WriteLine(row["Name"]); }
也可以:
C#string name = row.Field<string>("Name") ?? "";
如果字段类型是整数:
C#int? age = row.Field<int?>("Age");
复制DataTable与筛选数据
DataTable.Copy() 可以复制表结构和数据:
C#DataTable copyTable = table.Copy();
如果只需要复制结构:
C#DataTable structureTable = table.Clone();
两者区别很重要:
-
Clone():复制表结构,不复制数据。 -
Copy():复制表结构,同时复制数据。
例如:
C#DataTable newTable = table.Clone(); DataRow[] rows = table.Select("Age >= 30"); foreach (DataRow row in rows) { newTable.ImportRow(row); }
这样可以创建一个只包含符合条件数据的新表。
使用DataTable.Select进行分页
对于内存中的 DataTable,可以利用 Select() 的排序结果结合 LINQ 实现简单分页:
C#var pageData = table.AsEnumerable() .OrderBy(row => row.Field<int>("Id")) .Skip(10) .Take(10); foreach (DataRow row in pageData) { Console.WriteLine(row.Field<string>("Name")); }
这里表示跳过前 10 条,再获取 10 条数据,也就是常见的第二页数据。
不过需要注意,DataTable 分页属于内存分页。如果数据本身来自数据库,并且数据量很大,更推荐直接使用数据库的分页查询,而不是先将全部数据加载到内存。
DataTable查询操作的选择
不同需求可以采用不同方式:
| 操作需求 | 推荐方式 |
|---|---|
| 简单条件筛选 | Select() |
| 多条件动态查询 | LINQ |
| 判断是否存在 | Any() |
| 获取第一条数据 | FirstOrDefault() |
| 排序 | Select() 或 LINQ |
| 批量转换 | LINQ |
| 添加数据 | Rows.Add() / NewRow() |
| 修改数据 | DataRow 索引器 |
| 删除数据 | Delete() / Remove() |
| 复制结构 | Clone() |
| 复制数据 | Copy() |
实际项目中不必拘泥于某一种方法。简单查询使用 Select() 足够直接;当筛选条件、排序、投影和分组逐渐复杂时,LINQ 通常更容易维护。
常见问题与性能注意事项
DataTable 的优势是使用方便,但它本质上是内存数据结构,并不适合无限制地保存大量数据。
如果数据库中有几十万甚至数百万条记录,先执行完整查询,再把所有结果装进 DataTable,可能导致较高的内存占用和较长的处理时间。更合理的方案通常是在数据库层完成 WHERE、排序和分页,只把实际需要的数据返回给应用程序。
此外,频繁执行:
C#table.Select("Id = 100");
虽然使用简单,但如果需要针对大量记录反复进行复杂查找,可以考虑优化数据访问方式,例如建立合适的数据索引、使用 LINQ 的合理查询策略,或者直接让数据库完成查询。
修改行时也要注意数据类型:
C#row["Age"] = "30";
如果 Age 定义为 int,这种赋值可能导致类型转换问题。推荐直接使用正确的数据类型:
C#row["Age"] = 30;
对于来自用户输入的字符串,则应先进行安全转换:
C#if (int.TryParse(inputAge, out int age)) { row["Age"] = age; }
一个完整的DataTable行操作示例
下面将查询、添加、修改和删除操作组合起来:
C#using System; using System.Data; using System.Linq; class Program { static void Main() { DataTable table = new DataTable("Users"); table.Columns.Add("Id", typeof(int)); table.Columns.Add("Name", typeof(string)); table.Columns.Add("Age", typeof(int)); // 添加数据 table.Rows.Add(1, "张三", 25); table.Rows.Add(2, "李四", 30); table.Rows.Add(3, "王五", 28); // 条件查询 DataRow[] rows = table.Select("Age >= 28"); foreach (DataRow row in rows) { Console.WriteLine( $"{row["Id"]} {row["Name"]} {row["Age"]}"); } // 修改数据 DataRow? user = table.AsEnumerable() .FirstOrDefault( r => r.Field<int>("Id") == 1); if (user != null) { user["Age"] = 26; } // 添加一条数据 DataRow newRow = table.NewRow(); newRow["Id"] = 4; newRow["Name"] = "赵六"; newRow["Age"] = 32; table.Rows.Add(newRow); // 删除数据 DataRow[] deleteRows = table.Select("Id = 3"); foreach (DataRow row in deleteRows) { row.Delete(); } // 输出最终数据 foreach (DataRow row in table.Rows) { if (row.RowState != DataRowState.Deleted) { Console.WriteLine( $"{row["Id"]} {row["Name"]} {row["Age"]}"); } } } }
这个示例涵盖了 DataTable 最常见的基本操作:创建表结构、添加 DataRow、条件查询、LINQ 查询、修改字段以及删除数据。
掌握 DataTable.Select()、DataTable.AsEnumerable()、DataRow、Rows.Add()、Delete()、Clone() 和 Copy() 等核心 API 后,就可以应对大多数 C# 内存表数据处理需求。对于简单的数据筛选,优先考虑代码直观的 Select();对于复杂条件、排序和数据转换,则可以使用 LINQ;如果数据规模较大,则应尽可能将查询和分页下沉到数据库层完成。