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>();
这种行为使我感到困惑。有人能解释一下为什么会这样吗?如何绕过它呢?谢谢你。

DbQuery在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>();

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中重用变量有什么原因吗?