【问题标题】:DbQuery behaves differently in a foreach loop. Why?DbQuery 在 foreach 循环中的行为不同。为什么?
【发布时间】:2015-05-22 17:08:56
【问题描述】:

如果我使用以下代码,我会得到一个学习课程 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>();

这种行为让我很困惑。谁能解释为什么会这样?以及如何绕过它?谢谢。

【问题讨论】:

  • .Select(s =&gt; s)是不需要的,Student已经默认选中了

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


【解决方案1】:

问题在于 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 运算符/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();

如果你稍微玩一下,性能应该会通过巧妙的内部使用 HashSet 或 - 如果你非常幸运 - 达到或 远远超过 foreach 循环通过向数据库发送 JOIN 查询)。请小心,因为很容易在这里编写一些内容,从而在Intersect()Any() 方法中对数据库进行大量“额外”调用。即便如此,这是我倾向于选择的选项,除了我可能不会在最后打电话给.ToList()

这也说明了为什么我对 Entity Framework、linq-to-sql 甚至 NHibernate 或 ActiveRecord 等 ORM 没有太多用处。如果我只是在编写 SQL,我可以知道我得到了正确的连接查询。我也可以使用 ORM 来做到这一点,但是现在我仍然需要了解我正在创建的特定 SQL,并且我还必须知道如何让 ORM 来做我想做的事情。

第四

使用 C# 5.0。 This is fixed in the most recent version of C#,这样 for/foreach 循环的每次迭代都是它自己的变量。

【讨论】:

  • 我刚刚尝试了您的第三种方法,但它没有给出预期的结果。它返回在过滤器中有“任何”课程的学生。我希望它返回过滤器中有“所有”课程的学生。
【解决方案2】:

如果您想获取在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

编辑

Joel CoehoornMikael Eliasson 很好地解释了为什么最后一门课程的所有学生都被检索到了。

【讨论】:

    【解决方案3】:

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

    【讨论】:

      【解决方案4】:

      我认为这与实体框架无关。这是一个错误(不是真的,而是 c# 中的一个愚蠢的设计),变量在循环外声明。

      在这种情况下,这意味着因为 IEnumerable 被延迟评估,它将使用变量的 LAST 值。在循环中使用临时变量来解决它。

      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?

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-06-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多