DbQuery 在 foreach 循环中的行为不同。为什么?
DbQuery behaves differently in a foreach loop. Why?
如果我使用下面的代码,我会得到学习课程 1 和课程 2 的学生列表。(这几乎就是我想要的。)
IQueryable<Student> filteredStudents = context.Students;
filteredStudents = filteredStudents
.Where(s => s.Courses.Select(c => c.CourseID).Contains(1));
filteredStudents = filteredStudents
.Where(s => s.Courses.Select(c => c.CourseID).Contains(2));
List<Student> studentList = filteredStudents.ToList<Student>();
但是,如果我尝试在 foreach 循环中执行此操作(如以下代码所示),那么我会得到一个列表,其中包含在循环中注册最后一门课程的所有学生。
IQueryable<Student> filteredStudents = context.Students;
foreach (Course course in filter.Courses) {
if (course != null) {
filteredStudents = filteredStudents
.Where(s => s.Courses.Select(c => c.CourseID).Contains(course.CourseID));
}
}
List<Student> studentList = filteredStudents.ToList<Student>();
这种行为让我很困惑。谁能解释为什么会这样?以及如何绕过它?谢谢。
因为 "filteredStudents = filteredStudents.Where..." 是对变量的直接赋值,每次循环你都在完全替换之前的内容。您需要附加到它,而不是替换它。尝试搜索 "c# AddRange"
如果您想要获取 filter.Courses
中每门课程注册的每个 Student
,您可以尝试:
var courseIDs = filter.Courses.Select(c => c.CourseID);
var filteredStudents = context.Students
.Where(s => !courseIDs.Except(s.Courses.Select(c => c.CourseId)).Any())
过滤 courseIDs
是否是 Student
课程 ID 的 subset。
编辑
and 很好地解释为什么检索了上一门课程中的所有学生。
我认为这与 Entity Framework 无关。这是一个错误(不是真的,而是 c# 中的愚蠢设计),其中变量在循环外声明。
在这种情况下,这意味着因为 IEnumerable 是延迟计算的,所以它将使用变量的最后一个值。在循环中使用临时变量来解决它。
foreach (Course course in filter.Courses) {
if (course != null) {
var cId = course.CourseID;
filteredStudents = filteredStudents
.Where(s => s.Courses.Select(c => c.CourseID).Contains(cId))
.Select(s => s);
}
}
如果您正确定义了导航属性,那就更好了。只是做:
var studentList = filter.Courses.SelectMany(c => c.Students).ToList()
在此处查看更多信息:Is there a reason for C#'s reuse of the variable in a foreach?
问题是 foreach 循环只为所有循环迭代创建一个 单个 course
变量,然后将这个单个变量捕获到闭包中。还请记住,过滤器直到循环之后才真正执行。将它们放在一起,当过滤器执行时,这个 course
变量已经前进到课程过滤器中的最后一项;你只检查最后一门课程。
我看到了四种解决问题的方法。
第一个
为循环的每次迭代创建一个新变量(这可能是您最好的快速解决方法)
IQueryable<Student> filteredStudents = context.Students;
foreach (Course course in filter.Courses) {
if (course != null) {
int CourseID = course.CourseID;
filteredStudents = filteredStudents
.Where(s => s.Courses.Select(c => c.CourseID).Contains(CourseID));
}
}
List<Student> studentList = filteredStudents.ToList<Student>();
第二
在循环内解析 IEnumerable 表达式(可能效率低得多):
IEnumerable<Student> filteredStudents = context.Students;
foreach (Course course in filter.Courses) {
if (course != null) {
filteredStudents = filteredStudents
.Where(s => s.Courses.Select(c => c.CourseID).Contains(course.CourseID))
.ToList();
}
}
List<Student> studentList = filteredStudents.ToList<Student>();
第三
使用更合适的linq operators/lambda表达式来消除foreach循环:
var studentList = context.Students.Where(s => s.Courses.Select(c => c.CourseID).Intersect(filter.Courses.Select(c => c.CourseID)).Any()).ToList();
或者更易读的方式:
IQueryable<Student> filteredStudents = context.Students;
var courses = filter.Courses.Select(c => c.CourseID);
var studentList = filteredStudents
.Where(s => s.Courses.Select(c => c.CourseID)
.Intersect(courses)
.Any()
).ToList();
如果你稍微玩一下,性能应该满足或 远远超过 通过巧妙地内部使用 HashSets 的 foreach 循环或— 如果您很幸运 — 通过向数据库发送 JOIN 查询)。请小心,因为很容易在此处编写一些东西,使 "extra" 调用 Intersect()
或 Any()
方法中的数据库。即便如此,这是我倾向于选择的选项,除了我可能不会在最后调用 .ToList()
之外。
这也说明了为什么我不太喜欢 ORM,例如 Entity Framework、linq-to-sql,甚至 NHibernate 或 ActiveRecord。如果我只是写 SQL,我可以 知道 我得到了正确的连接查询。我也可以用 ORM 做到这一点,但现在我 仍然 需要了解我正在创建的特定 SQL,而且我还必须知道如何获得ORM 做我想做的事。
第四个
使用 C# 5.0。 This is fixed in the most recent version of C#,这样 for/foreach 循环的每次迭代都是它自己的变量。
如果我使用下面的代码,我会得到学习课程 1 和课程 2 的学生列表。(这几乎就是我想要的。)
IQueryable<Student> filteredStudents = context.Students;
filteredStudents = filteredStudents
.Where(s => s.Courses.Select(c => c.CourseID).Contains(1));
filteredStudents = filteredStudents
.Where(s => s.Courses.Select(c => c.CourseID).Contains(2));
List<Student> studentList = filteredStudents.ToList<Student>();
但是,如果我尝试在 foreach 循环中执行此操作(如以下代码所示),那么我会得到一个列表,其中包含在循环中注册最后一门课程的所有学生。
IQueryable<Student> filteredStudents = context.Students;
foreach (Course course in filter.Courses) {
if (course != null) {
filteredStudents = filteredStudents
.Where(s => s.Courses.Select(c => c.CourseID).Contains(course.CourseID));
}
}
List<Student> studentList = filteredStudents.ToList<Student>();
这种行为让我很困惑。谁能解释为什么会这样?以及如何绕过它?谢谢。
因为 "filteredStudents = filteredStudents.Where..." 是对变量的直接赋值,每次循环你都在完全替换之前的内容。您需要附加到它,而不是替换它。尝试搜索 "c# AddRange"
如果您想要获取 filter.Courses
中每门课程注册的每个 Student
,您可以尝试:
var courseIDs = filter.Courses.Select(c => c.CourseID);
var filteredStudents = context.Students
.Where(s => !courseIDs.Except(s.Courses.Select(c => c.CourseId)).Any())
过滤 courseIDs
是否是 Student
课程 ID 的 subset。
编辑
我认为这与 Entity Framework 无关。这是一个错误(不是真的,而是 c# 中的愚蠢设计),其中变量在循环外声明。
在这种情况下,这意味着因为 IEnumerable 是延迟计算的,所以它将使用变量的最后一个值。在循环中使用临时变量来解决它。
foreach (Course course in filter.Courses) {
if (course != null) {
var cId = course.CourseID;
filteredStudents = filteredStudents
.Where(s => s.Courses.Select(c => c.CourseID).Contains(cId))
.Select(s => s);
}
}
如果您正确定义了导航属性,那就更好了。只是做:
var studentList = filter.Courses.SelectMany(c => c.Students).ToList()
在此处查看更多信息:Is there a reason for C#'s reuse of the variable in a foreach?
问题是 foreach 循环只为所有循环迭代创建一个 单个 course
变量,然后将这个单个变量捕获到闭包中。还请记住,过滤器直到循环之后才真正执行。将它们放在一起,当过滤器执行时,这个 course
变量已经前进到课程过滤器中的最后一项;你只检查最后一门课程。
我看到了四种解决问题的方法。
第一个
为循环的每次迭代创建一个新变量(这可能是您最好的快速解决方法)
IQueryable<Student> filteredStudents = context.Students;
foreach (Course course in filter.Courses) {
if (course != null) {
int CourseID = course.CourseID;
filteredStudents = filteredStudents
.Where(s => s.Courses.Select(c => c.CourseID).Contains(CourseID));
}
}
List<Student> studentList = filteredStudents.ToList<Student>();
第二
在循环内解析 IEnumerable 表达式(可能效率低得多):
IEnumerable<Student> filteredStudents = context.Students;
foreach (Course course in filter.Courses) {
if (course != null) {
filteredStudents = filteredStudents
.Where(s => s.Courses.Select(c => c.CourseID).Contains(course.CourseID))
.ToList();
}
}
List<Student> studentList = filteredStudents.ToList<Student>();
第三
使用更合适的linq operators/lambda表达式来消除foreach循环:
var studentList = context.Students.Where(s => s.Courses.Select(c => c.CourseID).Intersect(filter.Courses.Select(c => c.CourseID)).Any()).ToList();
或者更易读的方式:
IQueryable<Student> filteredStudents = context.Students;
var courses = filter.Courses.Select(c => c.CourseID);
var studentList = filteredStudents
.Where(s => s.Courses.Select(c => c.CourseID)
.Intersect(courses)
.Any()
).ToList();
如果你稍微玩一下,性能应该满足或 远远超过 通过巧妙地内部使用 HashSets 的 foreach 循环或— 如果您很幸运 — 通过向数据库发送 JOIN 查询)。请小心,因为很容易在此处编写一些东西,使 "extra" 调用 Intersect()
或 Any()
方法中的数据库。即便如此,这是我倾向于选择的选项,除了我可能不会在最后调用 .ToList()
之外。
这也说明了为什么我不太喜欢 ORM,例如 Entity Framework、linq-to-sql,甚至 NHibernate 或 ActiveRecord。如果我只是写 SQL,我可以 知道 我得到了正确的连接查询。我也可以用 ORM 做到这一点,但现在我 仍然 需要了解我正在创建的特定 SQL,而且我还必须知道如何获得ORM 做我想做的事。
第四个
使用 C# 5.0。 This is fixed in the most recent version of C#,这样 for/foreach 循环的每次迭代都是它自己的变量。