DbQuery在foreach循环中的行为不同.为什么
本文关键字:为什么 foreach 循环 DbQuery | 更新日期: 2023-09-27 17:50:23
如果我使用以下代码,我将获得学习课程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>();
这种行为使我感到困惑。有人能解释一下为什么会这样吗?如何绕过它呢?谢谢你。
问题是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>();
3
使用更合适的linq操作符/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();
如果你稍微摆弄一下,性能应该达到或远远超过 foreach循环,通过巧妙地在内部使用HashSets或—如果你很幸运的话—通过向DB发送JOIN查询)。只是要小心,因为很容易在这里编写一些东西,使Intersect()
或Any()
方法内部的DB产生大量"额外"调用。尽管如此,这是我倾向于选择的选项,除了我可能不会在最后调用.ToList()
。
这也说明了为什么我没有太多使用orm,如实体框架,link -to-sql,甚至NHibernate或ActiveRecord。如果我只是写SQL,我可以知道我得到正确的连接查询。我也可以用ORM做到这一点,但是现在我仍然需要知道我正在创建的特定SQL,并且我还必须知道如何让ORM做我想要的。
第四使用c# 5.0。这在最新的c#版本中得到了修复,因此for/foreach循环的每次迭代都是它自己的变量。
如果你想获得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的子集。
编辑
Joel Coehoorn和Mikael Eliasson给出了一个很好的解释,为什么上一门课的所有学生都被检索了。
因为"filteredStudents = filteredStudents. where…"是对变量的直接赋值,每次通过循环,您都完全替换了之前的内容。你需要对它进行追加,而不是替换。尝试搜索"c# adrange"
我不认为这与实体框架有关。这是一个错误(不是真的,但c#中的一个愚蠢的设计),变量在循环外声明。
在这种情况下,这意味着因为IEnumerable是惰性求值的,所以它将使用变量的LAST值。在循环中使用一个temp变量来解决这个问题。
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()
在这里查看更多信息:c# '在foreach中重用变量有什么原因吗?