何时使用哪个?(关于IT技术)

6

编辑 下面有附加选项和略微扩展的问题。

考虑这个虚构的、抽象的类体例子。它展示了四种不同的执行“for”迭代的方式。

private abstract class SomeClass
{
    public void someAction();
}

void Examples()
{
    List<SomeClass> someList = new List<SomeClass>();

    //A. for
    for (int i = 0; i < someList.Count(); i++)
    {
        someList[i].someAction();
    }

    //B. foreach
    foreach (SomeClass o in someList)
    {
        o.someAction();
    }

    //C. foreach extension
    someList.ForEach(o => o.someAction());

    //D. plinq
    someList.AsParallel().ForAll(o => o.someAction());
编辑: 根据答案和研究,添加了一些选项。
    //E. ParallelEnumerable
    ParallelEnumerable.Range(0, someList.Count - 1)
        .ForAll(i => someList[i].someAction());

    //F. ForEach Parallel Extension
    Parallel.ForEach(someList, o => o.someAction());

    //G. For Parallel Extension
    Parallel.For(0, someList.Count - 1, i => someList[i].someAction())
}

我的问题分为两部分。我有没有错过一些重要的选项?在可读性和主要性能方面,哪个选项是最佳选择?

请指出 SomeClass 实现的复杂性或 someListCount 是否会影响此选择。

编辑: 由于选项太多,我不想让我的代码被选择所破坏。为了添加问题的第三部分,如果我的列表长度可以是任意长度,我应该默认使用并行选项吗?

作为一个支持的论据。我怀疑对于所有 SomeClass 实现和所有长度的 someList,选项 //E. ParallelEnumerable 将提供最佳的平均性能,因为多处理器架构普遍存在。我没有进行任何测试来证明这一点。

注意:并行扩展将需要使用 System.Threading.Tasks 命名空间。


1
如果这个问题中包含测试数据,那么它会更好。 - Petr Abdulin
@Petr Abdulin,我在问“测试数据”将如何影响选择。这个问题是有意开放的。这是开发人员经常需要做出的决定,所以我想知道我们怎么知道正确的选择是什么。 - Jodrell
更详细的问题和答案请参考https://dev59.com/UGUp5IYBdhLWcg3wf3kG,其中包含测试数据。 - TheAlbear
7个回答

6
只有实现索引的序列才能真正使用选项A,对于那些具有O(1)查找时间的序列,只有这些序列才能表现得很好。通常情况下,我会使用foreach和其变种,除非您有特殊逻辑。
另外请注意,“特殊逻辑”例如for(int i = 1; i < list.Count; i ++)可以使用Linq扩展方法实现:foreach(var item in sequence.Skip(1))。
因此,通常应优先选择B而非A。
关于C:如果其他开发人员不习惯函数式风格,则可能会感到困惑。
关于D:这将取决于很多因素。我猜,对于简单的计算,您不想这样做-只有在循环主体需要一段时间来计算时,您才会真正从并行化中受益。

1
同意。此外,foreach 可以在任何 IEnumerable 上工作,而 for 和索引查找则不行(一旦重构代码可能会很好)。 - Matthias Meid

3

您错过了:

Parallel.ForEach(someList, o => o.someAction())
Parallel.For(0, someList.Length, i => someList[i].someAction())

3
IL代码向我们展示了for循环是最有效率的,因为它没有状态机需要考虑。
使用for关键字会产生以下代码。
IL_0036:  br.s        IL_0048
IL_0038:  ldloc.0     
IL_0039:  ldloc.1     
IL_003A:  callvirt    System.Collections.Generic.List<UserQuery+SomeClass>.get_Item
IL_003F:  callvirt    UserQuery+SomeClass.someAction
IL_0044:  ldloc.1     
IL_0045:  ldc.i4.1    
IL_0046:  add         
IL_0047:  stloc.1     
IL_0048:  ldloc.1     
IL_0049:  ldloc.0     
IL_004A:  call        System.Linq.Enumerable.Count
IL_004F:  blt.s       IL_0038

IL_0051: ret

foreach生成的IL代码展示了状态机的工作原理。LINQ版本和ForEach版本产生的输出类似。

IL_0035:  callvirt    System.Collections.Generic.List<UserQuery+SomeClass>.GetEnumerator
IL_003A:  stloc.3     
IL_003B:  br.s        IL_004B
IL_003D:  ldloca.s    03 
IL_003F:  call        System.Collections.Generic.List<UserQuery+SomeClass>.get_Current
IL_0044:  stloc.1     
IL_0045:  ldloc.1     
IL_0046:  callvirt    UserQuery+SomeClass.someAction
IL_004B:  ldloca.s    03 
IL_004D:  call        System.Collections.Generic.List<UserQuery+SomeClass>.MoveNext
IL_0052:  brtrue.s    IL_003D
IL_0054:  leave.s     IL_0064
IL_0056:  ldloca.s    03 
IL_0058:  constrained. System.Collections.Generic.List<>.Enumerator
IL_005E:  callvirt    System.IDisposable.Dispose
IL_0063:  endfinally  
IL_0064:  ret   

我没有进行任何测试,但我认为这是一个安全的假设。

话虽如此,并不意味着for关键字总是应该被使用。这完全取决于你的风格、你的团队风格或者如果你正在编写的那段代码真的需要每一个CPU周期。

我不认为我会将AsParallel()与for、foreach或lambda等价物进行比较。你可以使用AsParallel()来分解CPU密集型任务或阻塞操作,但不应该仅仅用它来迭代一个“普通”的集合。


2
只有在查看索引集合时才是正确的。尽管 OP 在他的示例中使用了 List,但问题似乎更普遍地涉及迭代结构而不是列表的迭代结构。对于某些类型的集合,collection[i] 甚至没有意义(您必须执行 ElementAt()),并且可能性能更低。 - tvanfosson

1

就性能而言,我认为其中一个会表现最佳。

  //A. for
    for (int i = 0; i < someList.Count(); i++)
    {
        someList[i].someAction();
    }

或者

 //D. plinq
    someList.AsParallel().ForAll(o => o.someAction());

虽然在 A 的情况下,我倾向于不要每次执行 someList.Count()。

for 在性能方面表现更好,而 foreach 则相对较差。D 可能比 A 更好,但这取决于具体情况。如果 somelist 中有大量数据,则并行处理可能会有所帮助,但如果数据较小,则可能会增加额外负担。


哈里斯,最近我们发现在数组上 for 循环的性能稍微好一些。你知道为什么吗?我真的很好奇,因为 foreachIEnumerable 实现中应该执行类似的操作,不是吗? - Matthias Meid
“for” 不必在每次迭代中通过“IEnumerable”代码获取下一个项目,我认为这使它比“foreach”更快。 - Haris Hasan

1
通常我会根据逻辑选择合适的方法。如果我要循环整个列表,我会使用foreach,但如果我要循环子集,则使用for循环。此外,如果您在循环中修改集合,则必须使用for循环。
我知道的唯一其他选项是手动执行foreach正在执行的操作,这对于需要在创建枚举器的范围之外维护枚举器状态的情况非常有用。
using(var myEnum = aList.GetEnumerator()){
    while(myEnum.MoveNext()){
        myEnum.Current.SomeAction();
    }
}

0

for(int i = 0...) 要使用这种方法,您必须拥有一个可以逐个访问每个元素的数组。

foreach (SomeClass o in someList) 这种语法可用于可枚举类,即实现了 IEnumerable 接口的类。IEnumerable 具有一个名为 GetEnumerator() 的方法,该方法知道如何遍历集合中的每个元素。现在,上面的数组确实实现了 IEnumerable 接口。它知道如何枚举集合的方式就是您在上面定义的方式。然而,并非所有可使用 foreach 语法的 IEnumerable 类都能使用第一种方法,因为并非所有集合都提供对每个元素的访问权限。请考虑以下函数(未经测试):

public IEnumerable<int> GetACoupleOfInts()
{
yield return 1;
yield return 2;
}

}

这个方法允许您使用foreach结构,因为运行时知道如何枚举GetACoupleInts()的值,但不允许for结构。

someList.ForEach(o => o.someAction()); - 我的理解是,这个lambda表达式将被转换为与foreach(SomeClass o in someList)相同的表达式。

someList.AsParallel().ForAll(o => o.someAction()); - 在决定是否使用PLINQ时,您必须决定“得失是否值得”。如果someAction()中的工作量微不足道,则运行时尝试组织所有并发操作的数据的开销太大,您最好逐个执行。

长话短说 - 前三种方式可能会导致相同的调用并且对性能没有实际影响,尽管它们在框架内具有不同的含义。使用第四种选项需要更多考虑。


0

除了(C)之外,其他的都可以在特定情况下使用。此外,根据你所做的事情,你也可以将标准LINQ加入其中。例如,如果你的循环仅使用列表项来创建其他对象。

 (E) var someOtherCollection = someList.Select( l => transform(l) );

对于选项(A),如果您需要知道列表中的位置以及使用该项,则通常会使用选项(B)或(E)。如果列表很大且操作可以并行化(项目之间没有或可管理的依赖关系),则选项(D)是有意义的。
由于您正在使用通用列表,除了(E)之外,所有选项都是O(N)。 Count()应该是O(1)操作,因为它在变量中保持内部。对于其他可枚举类型,您需要知道数据结构的构造方式。如果您不知道集合的类型,则应使用foreach实现或LINQ覆盖索引实现,因为集合可能没有索引,这可能会将您的枚举转换为O(N ^ 2)操作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接