(请参阅我写的答案,以便更好地了解情况。)
下面是一个很好的查询,在选定的表STUDENTS行上运行。然后一个编辑破坏query
变量。这有什么不对?
students
是从导入数据表中选择的行,部分由以下部分定义:
importTable.Columns.Add("SECTION", typeof(string));
importTable.Columns.Add("NUMBER", typeof(string));
importTable.Columns.Add("ID", typeof(string));
(因为DataTable是无类型的,我需要将数据转换为字符串以使用字段)。
然后叫:
IEnumerable<DataRow> s = importTable.AsEnumerable();
IEnumerable<DataRow> t = s
.OrderBy(r => r["HALL"]);
IEnumerable<DataRow> sortedTable = t
.OrderBy(r =>
{ //if (r["ID"] is DBNull)
// return "";
//else
return r["ID"]; // ERROR
});
IEnumerable<DataRow> tue = sortedTable.Where(r => r["DAY"].Equals("TUE"));
IEnumerable<DataRow> wed = sortedTable.Where(r => r["DAY"].Equals("WED"));
AssignSections(tue);
AssignSections(wed);
这是查询:
public void AssignSections(IEnumerable<DataRow> students)
{
IEnumerable<IEnumerable<DataRow>> query = from e in students.AsEnumerable()
orderby (e["SHORTSCHOOL"] as string).PadRight(30) + e["SEED"] as string
group e by new { DAY=e["DAY"], GRADE=e["GRADE"] } into g
orderby g.Key.GRADE as string
select g.AsEnumerable();
var queryList = query.ToList(); // ArgumentException during "WED" call
foreach (var grade in query)
foreach (var student in grade)
if (student["ID"] == DBNull.Value)
{
student["SECTION"] = "S";
student["ID"] = "ID1";
}
}
分配SECTION工作,没有问题。分配ID会导致query
看起来像:
query
现在看来无效。 query
的未来用途也证明是无效的(尽管foreach完成得很好)。对于它的价值,grade
很好,但students
也通过原始表无效也似乎也没问题。
这里没有魔力。它是LINQ查询Deferred Execution和DBNull
的使用的组合,无法与其他类型进行比较。
延迟执行已经多次解释,所以我不会花时间在它上面。简而言之,查询是在枚举时唯一(但随时)执行的。枚举意味着foreach
,ToList
等,从技术上说,当调用可枚举的GetEnumerator(或调查员的第一个MoveNext)时。
您需要记住的是,在您定义它们时,不会执行(评估)返回LINQ查询的IEnumerable<T>
(或IQueryable<T>
),而是每次枚举它们(直接或间接)。这应该解释为“令我惊讶的答案是LINQ重新排序代码”部分来自你自己的答案。不,LINQ不对代码重新排序,通过在与定义查询变量的位置不同的某些点重新评估LINQ查询,这是您的代码。如果您只想在特定点评估它们一次,那么可以通过添加ToList
,ToArray
和类似的方法来实现,这些方法枚举查询并将结果存储在内存集合中的某些内容中,并使用该集合进行进一步处理。它仍然是IEnumerable<T>
,但是进一步的枚举将枚举查询结果而不是重新评估查询。
主要问题是DBNull
。从你的解释看起来最初所有的ID
值都是DBNull
,所以第一个查询运行良好(DBNull
知道如何与自己比较:)。一旦源包含至少一个不是DBNull
的值,使用OrderBy
的任何进一步查询该列与默认的IComparer
将失败。
它可以使用以下简单代码轻松复制无数据表:
var data = new[]
{
new { Id = (object)DBNull.Value },
new { Id = (object)DBNull.Value }
};
var query = data.OrderBy(e => e.Id);
query.ToList(); // Success
data[1] = new { Id = (object)"whatever" };
query.ToList(); // Fail
显示延迟查询执行和重新评估,或直接(以证明问题不是编辑):
new[]
{
new { Id = (object)DBNull.Value },
new { Id = (object)"whatever" }
}
.OrderBy(e => e.Id)
.ToList(); // Fail
解决方案是完全避免使用DBNull
。使用as string
最简单(并且比ToString()
或DataTable
好得多)是使用DataRowExtensions.Field扩展方法而不是object
返回索引器,除了提供对列的强类型访问外,还会自动处理DBNull
s(当你请求null
时将它们转换为string
或可空类型),所以你不会遇到这样的问题。
可以通过将有问题的代码更改为来证明
.OrderBy(r => r.Field<string>("ID"))
问题就会消失。我强烈建议也为其他列访问器执行此操作。
令我惊讶的是,LINQ重新排序代码。背景是这样的:
IEnumerable<DataRow> s = importTable.AsEnumerable();
IEnumerable<DataRow> t = s
.OrderBy(r => r["HALL"]);
IEnumerable<DataRow> sortedTable = t
.OrderBy(r =>
{ //if (r["ID"] is DBNull)
// return "";
//else
return r["ID"]; // ERROR
});
IEnumerable<DataRow> tue = sortedTable.Where(r => r["DAY"].Equals("TUE"));
IEnumerable<DataRow> wed = sortedTable.Where(r => r["DAY"].Equals("WED"));
AssignSections(tue);
AssignSections(wed);
3条注释线表示故障。然后发生了什么:sortedTable被部分初始化,以便提供Where子句来初始化tue。但是后来,sortedTable完成了初始化,在代码中出现了对assign wed的调用,但是恰好及时在AssignSections中构造的查询中使用了wed!
所以ERROR在AssignSections期间出现,当代码绕过完成sortedTable的初始化时,我可以通过添加3个禁用行并在“return”上设置断点来检测到这一点;
魔法?
DBNull和null不一样...... 正如您的原始错误消息所示“对象必须是字符串类型”(要分配给字符串) DBNull无法强制转换为字符串,it is a class ... 您需要在代码中处理此案例。有关简单的帮助方法,请参阅此链接: Unable to cast object of type 'System.DBNull' to type 'System.String
using System;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
DBNull dbNull = DBNull.Value;
Console.WriteLine(typeof(string).IsAssignableFrom(typeof(DBNull)));//False
Console.WriteLine(dbNull is string); //False
//Console.WriteLine((string)dbNull); // compile time error
//Console.WriteLine(dbNull as string); // compile time error
Console.ReadLine();
}
}
}
另外,请确保阅读“延迟加载”/“延迟执行”如何在LINQ / IEnumerable上工作。 您不必一直使用IEnumerable,特别是如果您不确定它是如何工作的。