代码之家  ›  专栏  ›  技术社区  ›  Amber Cahill

LINQ Any()会在没有参数的情况下枚举吗?

  •  0
  • Amber Cahill  · 技术社区  · 10 月前

    鉴于以下情况:

    var nums = GetNums();
    Console.WriteLine(nums.Any());
    Console.WriteLine(string.Join(", ", nums));
        
    static IEnumerable<int> GetNums()
    {
        yield return 1;
        yield return 2;
        yield return 3;
    }
    

    ReSharper将此标记为“可能的多重枚举”。但是,运行它会产生

    True
    1, 2, 3
    

    证明没有跳过任何元素。在没有谓词的情况下调用Any时,情况总是这样吗?或者,在某些情况下,这会产生不必要的副作用吗?

    2 回复  |  直到 10 月前
        1
  •  3
  •   ProgrammingLlama Raveena Sarda    10 月前

    这完全取决于支持什么 IEnumerable<T> 以及它是如何运作的,但你会多次迭代它。有些可枚举项每次枚举时都会产生不同的结果,有些则不会(如您的示例所示)。

    例如, BlockingCollection<T> 允许您获得一个“消费枚举”,它只产生一次项目。

    示例代码:

    var blockingCollection = new BlockingCollection<int>();
    blockingCollection.Add(1);
    blockingCollection.Add(2);
    blockingCollection.Add(3);
    blockingCollection.CompleteAdding();
    
    var nums = blockingCollection.GetConsumingEnumerable();
    Console.WriteLine(nums.Any());
    Console.WriteLine(string.Join(", ", nums));
    Console.WriteLine(nums.Any());
    

    输出:

    True
    2, 3
    False
    

    Try it online

    另一个可能的问题是 nums 如果 IEnumerable<T> 表示数据库查询的结果。这都可能导致额外的不必要的工作,占用应用程序和数据库服务器的不必要资源。

    数据也可能在查询之间发生变化。想象一下这个场景:

    1. 有人将记录插入您的数据库。
    2. 你查一下 nums.Any() 其进行一次查询并指示存在匹配的记录。
    3. 该记录将被删除。
    4. 你打电话 string.Join(", ", nums) 但是它现在是空的,因为第二次运行查询时数据库中没有记录。

    如果你打算多次使用枚举,或者你担心数据可能会在枚举之间发生变化,你可以将其具体化为数组、列表或其他内存集合。请注意 .ToList() 将迭代枚举以生成列表。

    例如:

    var blockingCollection = new BlockingCollection<int>();
    blockingCollection.Add(1);
    blockingCollection.Add(2);
    blockingCollection.Add(3);
    blockingCollection.CompleteAdding();
    
    var nums = blockingCollection.GetConsumingEnumerable()
        .ToList(); // put the values in a list
    Console.WriteLine(nums.Any());
    Console.WriteLine(string.Join(", ", nums));
    Console.WriteLine(nums.Any());
    

    输出:

    True
    1, 2, 3
    True
    
        2
  •  1
  •   Sweeper    10 月前

    Any 列举 IEnumerable<T> .

    在这种情况下, GetNums 事情就是这样 返回a IEnumerable<int> 对象,其 GetEnumerator() 方法返回一个新的枚举器,该枚举器被重置回初始状态。

    以下是一些反编译代码的摘录 https://sharplab.io/ 生成,这非常清楚发生了什么。

    // GetNums returns an instance of type <GetNums>d__0, with a state of -2
    internal static IEnumerable<int> GetEnums()
    {
        return new <GetNums>d__0(-2);
    }
    
    // After Any() consumes the first element of the enumerator, the state is no longer -2
    // so GetEnumerator in <GetNums>d__0 returns 'new <GetNums>d__0(0)', which has a state of 0.
    // this is as if GetEnums has not been executed at all
    IEnumerator<int> IEnumerable<int>.GetEnumerator()
    {
        if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId)
        {
            <>1__state = 0;
            return this;
        }
        return new <GetNums>d__0(0);
    }
    

    阅读 the spec ,我看不出这个行为是在哪里指定的,所以另一个实现很可能会选择 有这种行为。

    如果 GetNum 这不是一个纯粹的函数,而且 yield return SomeSideEffect() 相反, SomeSideEffect() 将被调用两次,第二次可能会产生不同的值。


    为了证明这一点 Any() 打电话 MoveNext 在枚举器上,您可以编写自己的类型来实现 IEnumerable<T> IEnumerator<T> ,在 GetEnumerator 方法,返回 this (与编译器为迭代器块生成的不同)。

    class OneToFive: IEnumerable<int>, IEnumerator<int> {
        public IEnumerator<int> GetEnumerator() => this;
        IEnumerator IEnumerable.GetEnumerator() => this;
        public int Current { get; set; }
        object IEnumerator.Current => Current;
        public bool MoveNext() {
            if (Current > 4) return false;
            Current++;
            return true;
        }
        public void Reset() { Current = 0; }
        public void Dispose() {}
    }
    
    var nums = new OneToFive();
    Console.WriteLine(nums.Any()); // True
    Console.WriteLine(string.Join(", ", nums)); // 2, 3, 4, 5