【问题标题】:LINQ's deferred execution, but how?LINQ 延迟执行,但是怎么做呢?
【发布时间】:2014-11-04 15:43:56
【问题描述】:

这一定很简单。但无论如何我都会问它,因为我认为其他人也会为此而苦苦挣扎。为什么以下简单的 LINQ 查询不总是使用新的变量值执行,而不是总是使用第一个?

static void Main(string[] args)
{
    Console.WriteLine("Enter something:");
    string input = Console.ReadLine();       // for example ABC123
    var digits = input.Where(Char.IsDigit);  // 123
    while (digits.Any())
    {
        Console.WriteLine("Enter a string which doesn't contain digits");
        input = Console.ReadLine();         // for example ABC
    }
    Console.WriteLine("Bye");
    Console.ReadLine();
}

在注释示例中,它将进入循环,因为输入 ABC123 包含数字。但即使您输入类似ABC 的内容,它也永远不会离开它,因为digits 仍然是123

那么为什么 LINQ 查询不评估新的 input-value 而总是第一个呢?

我知道我可以用这条附加线修复它:

while (digits.Any())
{
    Console.WriteLine("Enter a string which doesn't contain digits");
    input = Console.ReadLine();          
    digits = input.Where(Char.IsDigit);  // now it works as expected
}

或者——更优雅——直接在循环中使用查询:

while (input.Any(Char.IsDigit))
{
    // ...
}

【问题讨论】:

  • 当你将变量作为参数传递给函数时,它是按值传递的。
  • 如此简单的一段代码,却有着如此复杂的副作用。
  • 天哪,你从 The Actual Raymond Chen™ 那里得到了答案!
  • @s.m. Raymond Chen 的评论和 Jon Skeet 的回答。

标签: c# .net linq lazy-evaluation


【解决方案1】:

不同之处在于您更改的是 input 变量的值,而不是变量所引用的对象的 contents...所以 digits 仍然是指原创收藏。

与此代码比较:

List<char> input = new List<char>(Console.ReadLine());
var digits = input.Where(Char.IsDigit);  // 123
while (digits.Any())
{
    Console.WriteLine("Enter a string which doesn't contain digits");
    input.Clear();
    input.AddRange(Console.ReadLine());
}

这一次,我们正在修改input 所指的集合的内容 - 由于digits 实际上是该集合的视图,我们可以看到变化。

【讨论】:

  • 我是否正确 - 由于字符串的不变性,在问题示例中发生了这种情况?
  • @fex:字符串的不变性不是这个问题的原因,但它是我困惑的原因。如果字符串是List&lt;string&gt; 之类的集合,我会直接修改它,而不是为变量分配新列表。
  • @fex:不直接。如果字符串是可变的,那么更改 input 的值仍然不会影响 digits... 但 input 引用的内容可能已经更改。
【解决方案2】:

您正在为input 分配一个新值,但digits 序列仍然从input 的初始值派生而来。换句话说,当您执行digits = input.Where(Char.IsDigit) 时,它会捕获input 变量的当前值,而不是变量本身。为input 分配新值对digits 没有影响。

【讨论】:

    【解决方案3】:

    这一行:

    input.Where(Char.IsDigit)
    

    相当于:

    Enumerable.Where(input, Char.IsDigit)
    

    因此,input 的值作为.Where 查询的源被传递,而不是input引用

    您提出的第一个修复有效,因为它使用了前行中新分配的 input 值。

    【讨论】:

    • 是的。可以说input 是一个ByValue 参数,而不是一个ByRef 参数(它没有说refout)。
    • @JeppeStigNielsen:这实际上不是真的。字符串是引用类型(这就是为什么你可以有一个空字符串)。这意味着变量input 实际上包含对字符串的引用。如果string 是可变的,您可以将字符串作为普通参数传递给函数,修改内部字符串也会导致修改原始字符串。但是由于你不能修改字符串,所以在某些场景下它与传递参数的效果类似。
    • @tomp 我知道string 是一个引用类型。那不是我想要的。我正在考虑该参数是否为 ByRef 参数(ref stringout string)。所以我们同意。不幸的是,两个不同的概念“引用类型”(例如class(等),而不是struct)和“按引用参数”(refout)具有如此相似的名称。这会导致很多误解。我实际上意识到了这一点并试图使我的措辞准确,但我仍然被误解了......
    • @JeppeStigNielsen:哦,你当然是对的,我想太多了。
    【解决方案4】:

    可枚举的数字是指创建可枚举时input 包含的字符串的副本。它不包含对input 变量的引用,并且更改存储在input 中的值不会导致可枚举的具体化使用新值。

    请记住,Where 是一个静态扩展方法,并接受您正在调用它的对象作为参数。

    【讨论】:

      【解决方案5】:

      这几乎是评论,但包含结构化代码,因此我将其作为答案提交。

      对您的代码进行以下轻微修改即可:

        Console.WriteLine("Enter something:");
        string input = Console.ReadLine();       // for example ABC123
        Func<bool> anyDigits = () => input.Any(Char.IsDigit);  // will capture 'input' as a field
        while (anyDigits())
        {
          Console.WriteLine("Enter a string which doesn't contain digits");
          input = Console.ReadLine();         // for example ABC
        }
        Console.WriteLine("Bye");
        Console.ReadLine();
      

      这里inputFunc&lt;bool&gt; 类型的委托捕获(关闭)。

      【讨论】:

      • ReSharper 抱怨道,“访问修改后的闭包”首次使用 input。建议您更改为Func&lt;string,bool&gt; 并使用while (anyDigits(input)) 调用(可以说提高了可读性)。
      • @onedaywhen 是的!这并不是编写代码的最佳方式。这只是对实际有效的原始代码(来自问题)的“最小”更改。我不鼓励或推荐人们使用上面的代码。 ReSharper 提醒您的原因是闭包语义可能会让代码的读者感到困惑。要获得所需的功能,我们不需要访问修改后的闭包(并且提问者知道更好的方法来使问题已经在问题中工作)。
      【解决方案6】:

      我回答只是为了增加其他好的答案的准确性,关于延迟执行

      即使尚未评估 LINQ 查询(使用 .Any()),该查询在内部始终引用变量的初始内容。即使 LINQ 查询在变量受到新的影响后评估,初始内容也不会改变,延迟执行将使用查询一直引用的初始内容: p>

      var input = "ABC123";
      var digits = input.Where(Char.IsDigit);
      input = "NO DIGIT";
      var result = digits.ToList();   // 3 items
      

      【讨论】:

      • 请注意,“初始内容”可能指向可变结构。例如input = new List&lt;int&gt;{1}; var even = input.where (x =&gt; x%2 ==0); input.Add(2); var result = even.ToList();
      • @NPSF3000:如果它是一个可变集合,我不会问这个问题,因为没有问题;-)
      猜你喜欢
      • 1970-01-01
      • 2013-07-09
      • 2011-06-12
      • 1970-01-01
      • 1970-01-01
      • 2012-05-26
      • 2011-04-23
      • 2011-11-11
      相关资源
      最近更新 更多