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 循环的每次迭代都是它自己的变量。