【问题标题】:Linq orderby clause appears to be duplicating and losing objectsLinq orderby 子句似乎在重复和丢失对象
【发布时间】:2021-01-29 09:28:46
【问题描述】:

我正在编写一个 Windows 窗体程序。我对 linq 很陌生。我想订购内存中的对象列表。这些对象属于 Ctrl 类(并表示屏幕上的控件)。它们的属性包括:CtrlID、名称、X、Y、TabIndex。许多控件的 TabIndex 可能为零,因此我想按 TabIndex、Y、X 对它们进行排序,然后通过它们设置 TabIndex 从一开始递增,因此它是连续的。

我使用的代码是这样的:

            int i;
            Ctrl oCtrl = null;

            Debug.WriteLine("Before");
                for (i = 0; i < _ctrls.Count; i++)
                {
                    oCtrl = _ctrls[i];
                    Debug.WriteLine(string.Format("{0},ID:{1}: Name:{2}, TabIndex:{3}, X:{4}, Y:{5} ", i.ToString(), oCtrl.CtrlID, oCtrl.Name, oCtrl.TabIndex.ToString(), oCtrl.X, oCtrl.Y));
                }

                var ctrls = from ctrl in _ctrls.Items 
                            orderby ctrl.TabIndex, ctrl.Y, ctrl.X 
                            select ctrl;

                Debug.WriteLine("After");
                for (i = 0; i < ctrls.Count(); i++)
                {
                    oCtrl = ctrls.ElementAt(i);
                    Debug.WriteLine(string.Format("{0},ID:{1}: Name:{2}, TabIndex:{3}, X:{4}, Y:{5} ", i.ToString(), oCtrl.CtrlID, oCtrl.Name, oCtrl.TabIndex.ToString(), oCtrl.X, oCtrl.Y));
                    oCtrl.TabIndex = i + 1; // cos they're one-based
                }

我在“输出”窗口中得到的结果如下:

0,ID:125: Name:cmd, TabIndex:0, X:124, Y:12 
1,ID:126: Name:chk, TabIndex:0, X:124, Y:36 
2,ID:127: Name:db, TabIndex:0, X:124, Y:60 
3,ID:128: Name:LABEL1, TabIndex:0, X:8, Y:64 
4,ID:129: Name:LABEL2, TabIndex:0, X:8, Y:88 
5,ID:130: Name:nf, TabIndex:0, X:124, Y:84 
6,ID:131: Name:ni, TabIndex:0, X:124, Y:108 
7,ID:132: Name:LABEL3, TabIndex:0, X:8, Y:112 
8,ID:133: Name:sc, TabIndex:0, X:124, Y:132 
9,ID:134: Name:LABEL4, TabIndex:0, X:8, Y:136 
10,ID:135: Name:sv, TabIndex:0, X:124, Y:156 
11,ID:136: Name:LABEL5, TabIndex:0, X:8, Y:160 
12,ID:137: Name:LABEL6, TabIndex:0, X:8, Y:292 
13,ID:138: Name:txt, TabIndex:0, X:124, Y:288 
14,ID:139: Name:LABEL7, TabIndex:0, X:8, Y:40 
15,ID:140: Name:LABEL8, TabIndex:0, X:8, Y:16 
After
0,ID:125: Name:cmd, TabIndex:0, X:124, Y:12 
1,ID:126: Name:chk, TabIndex:0, X:124, Y:36 
2,ID:127: Name:db, TabIndex:0, X:124, Y:60 
3,ID:130: Name:nf, TabIndex:0, X:124, Y:84 
4,ID:131: Name:ni, TabIndex:0, X:124, Y:108 
5,ID:133: Name:sc, TabIndex:0, X:124, Y:132 
6,ID:135: Name:sv, TabIndex:0, X:124, Y:156 
7,ID:138: Name:txt, TabIndex:0, X:124, Y:288 
8,ID:125: Name:cmd, TabIndex:1, X:124, Y:12 
9,ID:127: Name:db, TabIndex:3, X:124, Y:60 
10,ID:131: Name:ni, TabIndex:5, X:124, Y:108 
11,ID:135: Name:sv, TabIndex:7, X:124, Y:156 
12,ID:125: Name:cmd, TabIndex:9, X:124, Y:12 
13,ID:131: Name:ni, TabIndex:11, X:124, Y:108 
14,ID:125: Name:cmd, TabIndex:13, X:124, Y:12 
15,ID:125: Name:cmd, TabIndex:15, X:124, Y:12

所有名为“LABEL...”的控件都丢失了,其余的一些控件是重复的。 TabIndex 显示为 > 0 的值是因为我在打印输出后设置了它的值,但是由于某些对象被引用了两次,所以新值第二次显示。如果我注释掉 orderby 子句使其成为直接选择,我会按照当前的顺序取回数据,不会出现重复项或缺少项目。

所以:

  1. 为什么我缺少对象?
  2. 为什么我有重复的对象?
  3. 如何让 orderby 做我期望的事情?

感谢您提供的任何帮助。

【问题讨论】:

  • 首先:不要通过IEnumerable&lt;T&gt; 进行基于索引的访问,而只是通过foreach。或者在您的查询结果上致电ToList。第二:您是否验证过您的输入集合没有有那些重复项?
  • shortest 修复是在分配给ctrls 之后添加一行ctrls = ctrls.ToList();。这里要理解的关键点是你反复OrderBy。 @HimBromBeere 建议使用 foreach 而不是 for 可能是 更好的 解决方案。 stackoverflow.com/questions/4830039/… 将向您展示如何使用 foreach 获取索引 (i)。
  • 谢谢两位。您能解释一下“反复执行 OrderBy”是什么意思吗?在var ctrls = from ctrl in _ctrls.Items... 语句之后 orderby 是否继续运行?即当我更改 TabIndex 的值时?
  • 好吧,查询并没有返回一个collection,而只是一个iterator。查看docs.microsoft.com/en-us/dotnet/standard/linq/… 以了解其中的区别。简而言之,在 iterator 上多次执行某些操作(在您的情况下调用 ElementAt)将多次执行底层 query
  • It seems strange with an orderby clause that you would lazy evaulate it though, as you need to have ordered all the elements to work out which one comes first. 了解您的 LINQ 查询更像是 recipe 而不是 meal 是关键。您已经告诉它如何订购,但实际上并没有订购。想象一下,你按名字命令你的孩子。你要第一个,然后更改他们的名字。然后你要求第二个 - 但是,唉,与此同时,他们重新订购以反映他们的新名字。因此,您可能会再次获得“第一个”。 ToList 强制“保持秩序,并留在那里”。

标签: c# linq duplicates


【解决方案1】:

我最初的评估是错误的,我是按照 mjwills 非常有效的观点来思考的。但是,即使我指出的解决方案可行,这种特殊情况的根本原因似乎也不是突变本身。

您遇到此特定问题的根本原因是使用 ElementAt (https://referencesource.microsoft.com/#system.core/system/linq/Enumerable.cs,1231)

public static TSource ElementAt<TSource>(this IEnumerable<TSource> source, int index) {
if (source == null) throw Error.ArgumentNull("source");
        IList<TSource> list = source as IList<TSource>;
        if (list != null) return list[index];
        if (index < 0) throw Error.ArgumentOutOfRange("index");
        using (IEnumerator<TSource> e = source.GetEnumerator()) {
            while (true) {
                if (!e.MoveNext()) throw Error.ArgumentOutOfRange("index");
                if (index == 0) return e.Current;
                index--;
            }
        }
    }

似乎如果源不是IList&lt;TSource&gt;,每次都会生成一个新的IEnumerator&lt;TSource&gt;,并进行一次迭代,直到到达该元素。如果是的话,我相信你会使用索引。这似乎就是你的顺序改变的原因。 (自我宣传 IList 与 IEnumerable 的良好阅读:https://stackoverflow.com/a/52450636/8695782

最重要的是,这种访问元素的方式非常低效,即使在这种情况下并不引人注意。除了效率低下之外,在不可回退的 IEnumerables(即非列表支持的永久收益器)的情况下,您也可能遇到其他麻烦。

如果您将循环更改为:

Console.WriteLine("After");
var index = 1;
foreach (var ctrl in _ctrls)
{
    oCtrl = ctrl;
    oCtrl.TabIndex = index++;
    Console.WriteLine(string.Format("Name:{0}, TabIndex:{1}", oCtrl.Name, oCtrl.TabIndex.ToString()));
}

那么您的代码应该也能正常工作,因为我们在编写 foreach 行时只隐式创建了一个 IEnumerator&lt;TSource&gt;

[原始评估-(部分)错误]

正如 mjwills 在他的评论中漂亮地指出的那样,您的 LINQ 查询与其说是一顿饭,不如说是一个食谱,因此它可能会在您迭代它时重新评估。从长远来看,在迭代任何类型的 IEnumerable/IQueryable 时改变对象通常会导致灾难。这就是你得到重复/丢失项目的原因。

3。 在你的具体情况下,改变

orderby ctrl.TabIndex, ctrl.Y, ctrl.X 

orderby ctrl.Y, ctrl.X

将实现我认为所需的结果,因为 TabIndex 是您尝试计算和更新的东西。

从我在代码中可以看出,它们都以 TabIndex = 0 开始。所以在这种特殊情况下,仅按 X 和 Y 进行排序意味着更改 TabIndex 不会影响排序。

【讨论】:

  • Mutating objects while iterating any kind of IEnumerable/IQueryable 从技术上讲,他迭代时并没有改变它们——但我同意你其余的大部分论点。
  • @mjwillis,感谢您指出这一点!睡觉后,我将不得不更新/完善我的答案。你是完全正确的,“支持”列表本身必须像添加或删除项目一样发生变异,以符合我的暗示。我不知道是什么驱使我提出这个主张。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-26
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多