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

3
如果我使用以下代码,我将获得同时学习课程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>();

这种行为让我感到困惑。有人能解释一下为什么会这样吗?以及如何避免它?谢谢。

1
顺便说一下,.Select(s => s) 是不必要的,Student 已经默认选择了。 - jjj
4个回答

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

如果你稍微玩弄一下,性能应该可以达到或者远远超过 foreach 循环,通过巧妙地使用 HashSet 或者——如果你非常幸运——通过向数据库发送 JOIN 查询。只要小心,因为很容易在 Intersect() 或 Any() 方法中编写某些会产生大量“额外”调用数据库的代码。即便如此,这仍然是我倾向于选择的选项,除了最后可能不需要调用 .ToList() 之外。
这也解释了为什么我对 Entity Framework、linq-to-sql 甚至 NHibernate 或 ActiveRecord 这样的 ORM 没有太多用处。如果我只是写 SQL,我可以知道我得到了正确的联接查询。我也可以用 ORM 做到这一点,但现在我仍然需要知道我正在创建的具体 SQL,而且我还必须知道如何让 ORM 做我想要的事情。

使用C# 5.0。最新版本的C#已经修复了这个问题,因此for/foreach循环的每次迭代都是它自己的变量。


我刚才尝试了你的第三种方法,但它并没有给出期望的结果。它返回了筛选器中“任意”一门课程的学生。我希望它返回筛选器中“所有”课程的学生。 - christiaantober

1

如果您想获取在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 CoehoornMikael Eliasson解释了为什么检索到了最后一门课程中的所有学生。


0
因为“filteredStudents = filteredStudents.Where…”是对变量的直接赋值,所以每次循环时都会完全替换之前的内容。你需要追加而不是替换。尝试搜索“c# AddRange”。

0

我认为这与实体框架无关。这是一个错误(其实不是,而是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()

在这里查看更多信息:C#在foreach中重用变量的原因是什么?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接