如果我使用以下代码,我会得到一份学习课程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>();
这种行为让我感到困惑。任何人都可以解释为什么会这样吗?以及如何绕过它?谢谢。
答案 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())
会过滤courseIDs
是Student
的{{1}}课程ID。
修改强>
subset和Joel 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?