C# 如何正确检查IEnumerable的现有结果

C# 如何正确检查IEnumerable的现有结果,c#,linq,C#,Linq,检查收藏是否有物品的最佳做法是什么 以下是我所拥有的一个例子: var terminalsToSync = TerminalAction.GetAllTerminals(); if(terminalsToSync.Any()) SyncTerminals(terminalsToSync); else GatewayLogAction.WriteLogInfo(Messages.NoTerminalsForSync); GetAllTerminals()方法将执行一个存储过程,

检查收藏是否有物品的最佳做法是什么

以下是我所拥有的一个例子:

var terminalsToSync = TerminalAction.GetAllTerminals();

if(terminalsToSync.Any())
    SyncTerminals(terminalsToSync);
else
    GatewayLogAction.WriteLogInfo(Messages.NoTerminalsForSync);
GetAllTerminals()
方法将执行一个存储过程,如果我们返回一个结果,(
Any()
true
),
SyncTerminals()
将在元素之间循环;因此,再次枚举它并第二次执行存储过程

避免这种情况的最好方法是什么

我想要一个好的解决方案,也可以在其他情况下使用;可能不需要将其转换为
列表


提前谢谢。

我可能会使用一个
ToArray
电话,然后检查
长度
;无论如何,你都要列举所有的结果,为什么不早点做呢?然而,既然您说过要避免尽早实现可枚举性

我猜
SyncTerminals
有一个
foreach
,在这种情况下,您可以这样编写它:

bool any = false;
foreach(var terminal in terminalsToSync)
{
  if(!any)any = true;
  //....
}

if(!any)
  GatewayLogAction.WriteLogInfo(Messages.NoTerminalsForSync);
好的,在第一个循环之后会有一个冗余的
if
,但我猜额外几个CPU周期的成本不会有多大影响

同样,您也可以用老方法进行迭代,使用
do…while
循环和
GetEnumerator
;从循环中取出第一次迭代;这样就不会浪费任何操作:

var enumerator = terminalsToSync.GetEnumerator();
if(enumerator.MoveNext())
{
  do
  {
    //sync enumerator.Current
  } while(enumerator.MoveNext())
}
else
  GatewayLogAction.WriteLogInfo(Messages.NoTerminalsForSync);

.Length
.Count
更快,因为它不需要通过Any()所需的GetEnumerator()/MoveNext()/Dispose()

我个人不会在这里使用Any,如果集合为空,foreach将不会循环任何项,所以我会这样做。但是,我建议您检查null

如果要预枚举集合,请使用.ToArray()eg将只枚举一次:

var terminalsToSync = TerminalAction.GetAllTerminals().ToArray();

if(terminalsToSync.Any())
    SyncTerminals(terminalsToSync);

如果您看到对
GetAllTerminals()
返回的内容进行求值的两个过程调用,这意味着该过程的结果没有被缓存。在不知道您使用的是什么数据访问策略的情况下,很难用一般方法解决这一问题

正如您所提到的,最简单的解决方案是在执行任何其他操作之前复制调用的结果。如果您愿意,您可以将此行为巧妙地包装在一个
IEnumerable
中,它只执行一次内部可枚举调用:

public class CachedEnumerable<T> : IEnumerable<T>
{
    public CachedEnumerable<T>(IEnumerable<T> enumerable)
    {
        result = new Lazy<List<T>>(() => enumerable.ToList());
    }

    private Lazy<List<T>> result;

    public IEnumerator<T> GetEnumerator()
    {
        return this.result.Value.GetEnumerator();
    }

    System.Collections.IEnumerable GetEnumerator()
    {
        return this.GetEnumerator();
    }
}
公共类CachedEnumerable:IEnumerable
{
公共CachedEnumerable(IEnumerable可枚举)
{
结果=新的惰性(()=>enumerable.ToList());
}
私人懒惰的结果;
公共IEnumerator GetEnumerator()
{
返回此.result.Value.GetEnumerator();
}
System.Collections.IEnumerable GetEnumerator()
{
返回此.GetEnumerator();
}
}

将结果包装到该类型的实例中,它将不会多次计算内部可枚举项。

这样做如何,它仍然会延迟执行,但会在执行后对其进行缓冲:

var terminalsToSync = TerminalAction.GetAllTerminals().Lazily();
与:

公共静态类LazyEnumerable{
公共静态IEnumerably(此IEnumerable源){
if(源为LazyWrapper)返回源;
返回新的LazyWrapper(源);
}
类LazyWrapper:IEnumerable{
私有可数源;
私人枪决;
公共LazyWrapper(IEnumerable源代码){
如果(source==null)抛出新的ArgumentNullException(“source”);
this.source=源;
}
IEnumerator IEnumerable.GetEnumerator(){返回GetEnumerator();}
公共IEnumerator GetEnumerator(){
如果(!已执行){
已执行=真;
source=source.ToList();
}
返回source.GetEnumerator();
}
}
}

以下是解决此问题的另一种方法:

int count = SyncTerminals(terminalsToSync);
if(count == 0)  GatewayLogAction.WriteLogInfo(Messages.NoTerminalsForSync);
SyncTerminals
更改为执行以下操作:

int count = 0;
foreach(var obj in terminalsToSync) {
    count++;
    // some code
}
return count;

漂亮且简单。

这里的所有缓存解决方案都是在检索第一个项目时缓存所有项目。如果您在迭代列表中的项目时缓存每个项目,那么这将是非常懒惰的

在本例中可以看出差异:

public class LazyListTest
{
    private int _count = 0;

    public void Test()
    {
        var numbers = Enumerable.Range(1, 40);
        var numbersQuery = numbers.Select(GetElement).ToLazyList(); // Cache lazy
        var total = numbersQuery.Take(3)
            .Concat(numbersQuery.Take(10))
            .Concat(numbersQuery.Take(3))
            .Sum();
        Console.WriteLine(_count);
    }

    private int GetElement(int value)
    {
        _count++;
        // Some slow stuff here...
        return value * 100;
    }
}
如果运行
Test()
方法,则
\u con
t仅为10。如果不缓存,它将是16,如果使用.ToList()它将是40


.

的一个示例,为什么需要第二次执行存储过程?terminalsToSync已经包含SP结果,不是吗?我不需要,也不想第二次执行它。我在问题中所说的是,由于GetAllTerminals()返回IEnumerable,Any()和foreach都将调用它,并且过程将被调用两次。我希望避免这种行为,并且仍然能够检查是否有行。请参阅Marc Gravell@??为什么特别要避免使用列表,这是一种简单而有效的方法?@MarcGravell:既然OP要求“最佳实践”,值得一提的是将大量结果复制到列表中的缺点。感谢您的回答,但我在问题中提到,我希望避免将其转换为列表,因为我正在寻找通用解决方案。@FedorHajdu这到底是什么意思?如何将其转换为列表而不是通用解决方案?为了确定terminalsToSync中是否有任何项,必须首先枚举可枚举项。+1表示ToArray()。使用枚举时,了解(并控制)集合何时稳定非常重要。+1人们经常说Any()更快,因为它会短路,但情况并非总是如此,因为Count()有时可以在集合稳定后使用IList.Count属性。Length在IEnumerable.afaik上不可用。您不能在IEnumerable上调用Count(属性),您可以调用Count()扩展方法,但它比任何()都慢,因为它必须循环整个集合并计算结果,然后在
int count = 0;
foreach(var obj in terminalsToSync) {
    count++;
    // some code
}
return count;
public class LazyListTest
{
    private int _count = 0;

    public void Test()
    {
        var numbers = Enumerable.Range(1, 40);
        var numbersQuery = numbers.Select(GetElement).ToLazyList(); // Cache lazy
        var total = numbersQuery.Take(3)
            .Concat(numbersQuery.Take(10))
            .Concat(numbersQuery.Take(3))
            .Sum();
        Console.WriteLine(_count);
    }

    private int GetElement(int value)
    {
        _count++;
        // Some slow stuff here...
        return value * 100;
    }
}