【问题标题】:Strange behaviour of OrderBy LinqOrderBy Linq 的奇怪行为
【发布时间】:2017-01-19 09:20:40
【问题描述】:

我有一个使用OrderBy() Linq 函数排序的列表,它返回一个IOrderedEnumerable

var testList = myList.OrderBy(obj => obj.ParamName);

ParamName 是一个可以保存整数和字符串的对象。上面的 orderBy 根据整数值对列表进行排序。现在我在 testList 上操作 foreach 并根据其整数值将 ParamName 属性更改为某个字符串,如下所示,

using (var sequenceEnum = testList.GetEnumerator())
{
    while (sequenceEnum.MoveNext())
    {
        sequenceEnum.Current.ParamName = GetStringForInteger(int.Parse(Convert.ToString(sequenceEnum.Current.ParamName)));
    }
}

接下来发生的事情是,在前一个循环之后列表中的项目的顺序被打乱了,并且列表是根据分配的字符串而不是初始顺序进行排序的。

但是,当我将.ToList().OrderBy() 子句结合使用时,会保留顺序。

谁能帮我看看这里发生了什么?

示例输出说明:

【问题讨论】:

  • 如果您包含了填充 myList 的实际数据,将会有所帮助。这很容易包含在您的问题中吗?
  • 您的 testList 不是一个列表,而是一个 LINQ 查询。如果要保留此查询,请使用 ToListToArray。在此之前,它使用延迟执行,这意味着您将始终使用当前条件评估此查询。
  • @NickAllan 排序通常是 Excel 中存在的列,例如 A、B、C、D...AA、AB.. 等等。上面的代码实际上将其转换为数字,对其排序并转换为字符串。
  • 我很好奇这个。虽然您从OrderBy 获得的排序被推迟,但排序发生在第一次调用MoveNext() 之后,所以我不希望这样。你能包括一个最小的完整复制吗?

标签: c# linq ienumerable iorderedenumerable


【解决方案1】:

编辑: 我们都把你的问题弄错了。它以错误方式排序的原因是因为您正在比较“B”和“AA”,并期望 AA 像在 excel 中一样在 B 之后,这当然不会按字母顺序发生。

在排序时指定一个显式比较器,或者在进行排序之前将 ParamName 转换为 Int。


Linq 通常返回 IEnumerable 元素的原因是它具有惰性求值行为。这意味着它将在您需要时评估结果,而不是在您构建它时。

调用 ToList 会强制 linq 评估结果以生成预期的列表。

TL;DR 在进行 linq 查询并在获取结果之前更改源数据集时要非常小心。

【讨论】:

  • 除了使用 .ToList() 之外,我如何才能完成订单的执行,因为我的列表非常大,并且我假设在较大的列表中调用 .ToList() 非常昂贵。您能否建议我可以覆盖惰性评估的任何其他方法?
  • 我真的不明白你在 while 循环中想要做什么。您不能在订购列表之前进行此转换吗?
  • 排序通常是 Excel 中存在的列,如 A、B、C、D...AA、AB.. 等等。上面的代码实际上将其转换为数字,对其排序并转换为字符串
【解决方案2】:

原因是 EF 中查询的分离执行,这意味着对 DB 的实际查询要等到您通过 .ToList() 显式加载到内存中之后才会进行。

正如你所说的那样,.OrderBy() 返回一个 IOrderedEnumerable,它适用于 foreach 习语。那么为什么不简化它做如下的事情呢?

foreach(var item in testList)
{
       item.ParamName = GetStringForInteger(int.Parse(Convert.ToString(item.ParamName)));
}

【讨论】:

    【解决方案3】:

    IEnumerable 对象本身并不表示对象序列,它表示根据请求将序列的第一个元素作为“当前元素”提供给您所需的算法,并为您提供当前元素之后的下一个元素。

    在发明 linq 时,决定 linq 使用延迟执行的概念,通常称为 惰性求值。在使用延迟执行的 Enumerable 函数的 MSDN 描述中,您会发现以下短语:

    这个方法是通过延迟执行来实现的。立即返回值是一个存储执行操作所需的所有信息的对象。在通过直接调用其 GetEnumerator 方法或使用 foreach 枚举对象之前,不会执行此方法表示的查询。

    如果您创建 IEnumerable,并更改 IEnumerable 对象所作用的对象,此更改可能会影响结果。它相当于一个函数,如果函数作用的参数发生变化,则返回不同的值:

    int x = 4;
    int y = 5;
    int MyFunction()
    {
        return x + y;
    }
    
    int a = MyFunction();
    y = 7;
    int b = MyFunction();
    

    现在 b 不等于 a。类似于您的 IEnumerable:

    List<...> myList = CreateMySequence()
    var IEnumerable<...> myOrder = myList.OrderBy(...);
    

    myOrder 不包含结果,但就像一个可以为它计算结果的函数。如果您更改 myOrder 使用的参数之一,结果可能会改变:

    myList.Add(someElement);
    var myResult = myOrder.ToList();
    

    myResult 已更改,因为您更改了函数。

    发明延迟执行的原因是因为您通常不需要枚举序列的所有元素。在以下情况下,如果您要创建完整的序列,则会浪费处理时间:

    • 我只想要第一个元素,
    • 我想跳过 3 个元素,然后取两个元素,
    • 我想要第一个值为 x 的元素
    • 我想知道序列是否包含任何元素

    当然,有些函数需要在您要求第一个元素时立即创建完整的序列:

    • 如果您想要排序序列中的第一个,则必须对所有元素进行排序才能找到第一个。
    • 如果您想要一组元素中的第一个元素,其中该组中的所有元素都具有相同的某个属性 X (Enumerable.GroupBy) 的值

    根据经验,明智的做法是尽可能长时间地将所有序列保持为 IEnumerable,直到您需要结果或更改用于创建序列的源为止。

    在从数据库、文件、互联网获取数据时,后者很重要:您必须在连接关闭之前创建序列。

    以下操作无效

    using (var myDbContext = new MyDbContext)
    {
        return MyDbContext.Customers.Where(customer => customer.Age > 18);
    }
    

    离开 using 语句时,在 Disposed myDbContext 之前未执行数据库查询。因此,只要您请求序列中的任何元素,您就会得到一个异常。

    【讨论】:

    • 如何在不使用 .ToList() 函数的情况下返回订单列表(不是序列)。
    • 延迟执行至少可以追溯到 1971 年。它不是由 linq 发明的,而是 linq 基于成熟的理论和实践。
    【解决方案4】:

    正如这里的每个人都提到的,那是因为 Linq 是懒惰评估的。你可以在这里阅读更多:https://blogs.msdn.microsoft.com/ericwhite/2006/10/04/lazy-evaluation-and-in-contrast-eager-evaluation/

    你想做的大概是这样的:

    var testList = myList.OrderBy(obj => obj.ParamName).Select(obj =>
    {
        obj.ParamName = GetStringForInteger(int.Parse(Convert.ToString(obj.ParamName)));
        return obj;
    });
    

    【讨论】:

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