DbQuery在foreach循环中表现不同。为什么?

时间:2015-05-22 17:08:56

标签: c# sql-server entity-framework linq-to-entities

如果我使用以下代码,我会得到一份学习课程1和课程2的学生名单。(这几乎就是我想要的。)

IQueryable<Student> filteredStudents = context.Students;
filteredStudents = filteredStudents
    .Where(s => s.Courses.Select(c => c.CourseID).Contains(1)).Select(s => s);
filteredStudents = filteredStudents
    .Where(s => s.Courses.Select(c => c.CourseID).Contains(2)).Select(s => s);
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))
            .Select(s => s);
    }
}
List<Student> studentList = filteredStudents.ToList<Student>();

这种行为让我感到困惑。任何人都可以解释为什么会这样吗?以及如何绕过它?谢谢。

4 个答案:

答案 0 :(得分:4)

问题是foreach循环只为所有循环迭代创建一个 course变量,然后将这个单个变量捕获到一个闭包中。还要记住,在循环之后,过滤器才能实际执行。将它们组合在一起,当过滤器执行时,单个course变量已经前进到Courses过滤器中的最后一项;你只检查最后一个课程。

我看到了解决问题的四种方法。

第一

为循环的每次迭代创建一个新变量(这可能是您最好的快速修复)

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运算符/ 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循环,或者 - 如果你非常幸运的是 - 通过向DB发送JOIN查询。要小心,因为在这里写一些东西很容易,这些东西会产生大量的&#34;额外的&#34;在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 :(得分:1)

如果您要尝试在Student中的每个课程中注册每个filter.Courses,您可以尝试:

var courseIDs = filter.Courses.Select(c => c.CourseID);
var filteredStudents = context.Students
    .Where(s => !courseIDs.Except(s.Courses.Select(c => c.CourseId)).Any())

会过滤courseIDsStudent的{​​{1}}课程ID。

修改

subsetJoel Coehoorn很好地解释了为什么上一课程中的所有学生都被检索出来。

答案 2 :(得分:0)

因为&#34; filteredStudents = filteredStudents.Where ...&#34;是对变量的直接赋值,每次循环都会完全替换之前的变量。你需要追加它,而不是替换它。尝试搜索&#34; c #AddRange&#34;

答案 3 :(得分:0)

我不认为这与Entity Framework有关。它是一个错误(不是真的,但在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()

在此处查看更多信息:Is there a reason for C#'s reuse of the variable in a foreach?