使用索引的List<T>.ForEach

33

我正在尝试查找以下代码的LINQ等效版本:

NameValueCollection nvc = new NameValueCollection();

List<BusinessLogic.Donation> donations = new List<BusinessLogic.Donation>();
donations.Add(new BusinessLogic.Donation(0, "", "", "");
donations.Add(new BusinessLogic.Donation(0, "", "", "");
donations.Add(new BusinessLogic.Donation(0, "", "", "");

for(var i = 0; i < donations.Count(); i++)
{
    // NOTE: item_number_ + i - I need to be able to do this
    nvc.Add("item_number_" + i, donations[i].AccountName);
}

我希望能够使用类似这样的东西:

NameValueCollection nvc = new NameValueCollection();

List<BusinessLogic.Donation> donations = new List<BusinessLogic.Donation>();
donations.Add(new BusinessLogic.Donation(0, "", "", "");
donations.Add(new BusinessLogic.Donation(0, "", "", "");
donations.Add(new BusinessLogic.Donation(0, "", "", "");

donations.ForEach(x => nvc.Add("item_name_" + ??, x.AccountName);

但我还没有找到一种确定循环正在进行的迭代次数的方法。任何帮助都将不胜感激!


捐赠是否有一个.IndexOf()方法? - Josh C.
2
你可以使用int i = 0; donations.ForEach(x=> nvc.Add("item_name_" + i++, ...,但是不确定它有多安全。 - lc.
12个回答

38
LINQ没有ForEach方法,这是有原因的。LINQ用于执行查询,旨在从某些数据源获取信息。它不是为了改变数据源而设计的。LINQ查询不应该引起副作用,这正是您在这里所做的。 List类确实有一个ForEach方法,这就是您正在使用的方法。因为它实际上不在System.Linq命名空间中,所以它在技术上不是LINQ的一部分。
您问题中的for循环没有任何问题。从良好实践的角度来看,试图按照您尝试的方式进行更改是错误的。 这里是一个讨论此事的链接。
现在,如果您想忽略该建议并仍然使用ForEach方法,则很容易编写一个提供索引给操作的方法:
public static void ForEach<T>(this IEnumerable<T> sequence, Action<int, T> action)
{
    // argument null checking omitted
    int i = 0;
    foreach (T item in sequence)
    {
        action(i, item);
        i++;
    }
}

嗯,显然我使用了错误的术语。我的 Donation 集合确实有一个 .ForEach() 方法。 - James Hill
ForEach不被定义为IEnumerable<T>的扩展方法,因此不是LINQ的一部分。 - devdigital
6
这是List的一个方法,不是System.Linq中的方法。 - Servy
3
@Servy,我认为显而易见的是我需要暂时离开电脑。我想来一杯咖啡。感谢你帮我纠正了错误。 - James Hill
2
我一直想知道为什么没有一个LINQ实现的ForEach,但这很有道理。+1 链接到该文章。 - JDB
我喜欢这个,尽管您可以放弃索引计算并通过将实现替换为foreach (var e in sequence.Select((item, n) => (item, n))) action(e.n, e.item);来缩短代码。 - Jason C

27

如果你真的想要使用List.ForEach,那很简单:

//[...]
int i=0;
donations.ForEach(x => nvc.Add("item_name_" + i++, x.AccountName);

15

这有点复杂并创建了一个中间集合,但是怎么样:

donations.Select((x, i) => new {Name = "item_name_" + i, x.AccountName})
    .ToList()
    .ForEach(x=> nvc.Add(x.Name, x.AccountName));

这里使用的是 带有索引参数的 Enumerable.Select 重载

我必须要辩论一下,这种方式其实并没有多大的好处。使用中间集合会增加开销,并且在我看来,相比于原始的 for 循环,失去了可读性。

如果你愿意使用 foreach 循环而不是 List.ForEach,你也可以跳过中间集合。请参阅 @wageoghe 的回答(再次强烈建议)。


14

这是一篇旧文章,但在谷歌上排名很高,所以我认为更通用的方法更合适。(另外,我经常忘记该如何完成这个任务,这意味着我每次都要搜索谷歌。)

假设有一个整数列表:

var values = new List<int>() { 2, 3, 4, 0x100, 74, 0xFFFF, 0x0F0F };

要迭代这个列表并且拥有索引,请执行以下操作:

values.Select((x, i) => new
{
    item = x,
    index = i
})
.ToList()
.ForEach(obj =>
{
    int value = obj.item;
    int valueIndex = obj.index;
});

缺点是你失去了values懒惰求值的特性,因为在应用ForEach之前ToList()会强制枚举整个源。例如,在这个程序中看到两种方法的输出差异:链接 - Jason C
我不是开发人员,有时候当开发人员给出像“这不是你应该这样做的方式,因为……”这样的答案时,我会感到生气。我知道总有更好的方法。但是像你这样的回答让我想更多地了解编程。有时候并不是最好的方式。谢谢! List<sbyte> input; byte[] bytedata = new byte[input.Count]; input.Select((x, i) => new { item = x, index = i }).ToList().ForEach(o => bytedata[o.index] = (byte)o.item); - FranciscoNabas
告诉你吧,我进行了一个简单的原始测试,你的方法比使用for循环更快。 - FranciscoNabas

4
借鉴@lc的答案,进一步补充。
foreach (var x in donations.Select((d, i) => new {ItemName = "item_name_" + i, AccountName = d.AccountName}))
{
  nvc.Add(x.ItemName, x.AccountName);
}

现在有了元组,您可以使用(d, i) => ($"item_name_{i}", d.AccountName)来缩短new{}语法。然后要么使用x.Item1x.Item2,要么命名元组字段而不是使用x - Jason C

3

你为什么不使用 Dictionary<string, string> 呢?因为你的名称/键似乎是唯一的,所以这样会更快,并且你可以使用标准查询运算符ToDictionary

此外,如果你想使用扩展方法(尽管像Servy说的for循环是正确的解决方案),那么你可以编写自己的扩展方法-请参见这里


3
这也可以使用聚合来实现。请查看以下示例:
var data = new[]
{
    "A",
    "B",
    "C",
    "D"
};

var count = data.Aggregate(0, (index, item) =>
{
    Console.WriteLine("[{0}] => '{1}'", index, item);

    return index + 1;
});

Console.WriteLine("Total items: {0}", count);

Aggregate在这里作为一个for语句。唯一的缺点是在每次迭代中需要将索引值增加1并返回它。


0

我喜欢以以下方式进行:

NameValueCollection nvc = new NameValueCollection();

List<BusinessLogic.Donation> donations = new List<BusinessLogic.Donation>();
donations.Add(new BusinessLogic.Donation(0, "", "", ""));
donations.Add(new BusinessLogic.Donation(0, "", "", ""));
donations.Add(new BusinessLogic.Donation(0, "", "", ""));

Enumerable
    .Range(0, donations.Count())
    .ToList()
    .ForEach(i => nvc.Add("item_number_" + i, donations[i].AccountName));

-1

试试这个 -

donations.ForEach(x =>
         {
             int index = donations.IndexOf(x);
             nvc.Add("item_name_" + index, x.AccountName);
         });

6
最好是通过索引来枚举中间集合(就像@lc的答案一样),而不是在每次迭代时搜索列表(.IndexOf)。 - Austin Salonen
3
如果一个项目在列表中出现多次会发生什么?(请注意,在 OP 的代码中,所有项目都是相同的。)这不仅仅是关于糟糕的性能;它甚至无法正常工作。 - Servy
那么,在不创建中间集合的情况下是否有其他方法可以找到索引呢? - Rohit Vats
2
@Sevy - 哎呀..!! 现在我明白了。我完全忘记了唯一性的部分。现在我荣幸地接受了负评。 :) - Rohit Vats
@RV1987 当然可以。像 OP 代码中所做的那样使用 for 循环。或者你可以创建一个提供索引的 ForEach 方法,就像我的答案所示,或者你可以在循环中自己增加一个整数计数器 count - Servy
1
@Sevy - 是的,谢谢Servy,我明白了。这就是为什么我说至少告诉我哪里错了。 - Rohit Vats

-2
C# 7(约于2017年)增加了更简洁的元组语法,可以与索引形式的Select一起使用(无需手动计算索引),从而提供了一种简洁明了的语法。例如:
foreach ((var item, int n) in TheItems.Select((i,n)=>(i,n))) {
    // item is the current item, n is its index
}

或者如果你更喜欢:

foreach (var element in TheItems.Select((item,n)=>(item,n))) {
    // element.item is the current item, element.n is its index
}

所以在 OP 的情况下,例如:

foreach ((var item, int n) in donations.Select((i,n)=>(i,n)))
    nvc.Add($"item_number{n}", item.AccountName);

或者,如果你更喜欢:

foreach ((var k, var v) in donations.Select((i,n)=>($"item_number_{n}",i.AccountName)))
    nvc.Add(k, v);

或者,类似于这个答案但稍微简洁一些,但请注意,这将通过调用ToList()来破坏惰性求值,强制在调用ForEach()之前对整个源进行枚举:

TheItems.Select((item,n)=>(item,n)).ToList().ForEach(element => { 
    // element.item is the current item, element.n is its index
});

我觉得这个解决方案应该已经存在了 - 好吧,基于这个Select重载有几个答案。这里有什么新的? - Gert Arnold
@GertArnold 使用foreach遍历Select时,不需要维护索引计数器,代码更加简洁。大多数基于Select的答案都会做一些额外的操作或破坏惰性求值。 - Jason C
这里唯一重要的是 Select 重载。你让它看起来好像你是第一个提到它的人。即便如此,真的没有必要添加无数种使用方法。与此同时,有人可能会想知道为什么还没有人仅使用 SelectToList 直接从 donations 创建 nvc 而不使用 Foreachforeach。那将是最简洁的解决方案。尽管如此,我并没有感到有必要再添加另一个答案。 - Gert Arnold
@GertArnold 一般来说(虽然在 OP 的情况下不是问题),使用 ToList() 将强制枚举整个源序列,然后再进行操作。因此,如果源是某些惰性生成的 IEnumerable,则会失去惰性评估,在某些情况下可能会出现问题。 - Jason C
@GertArnold 关于直接创建nvcan.dreas.k的回答已经非常接近了,但实际上没有任何方法可以直接生成NameValueCollectionToDictionary()无法帮助(但是devdigital的建议可以实现),虽然您可以使用例如Aggregate或其他方法构建NVC,但此时最好直接使用Add。 此外,不清楚OP是否打算创建新的NVC还是添加到现有的NVC中,但从问题中看来似乎更像后者。 - Jason C
@GertArnold 顺便说一下,这里只有另一个答案满足以下两个条件:1)不明确计算索引,2)保留惰性求值。我在这个答案中的主要观点是元组(在早期答案发布时并不存在其当前形式)进一步简化了语法。我确实理解你所说的“你让它看起来好像你是第一个提到[Select]的人”;我可以重新措辞一下。 - Jason C

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