如何获取IEnumerable中元素的索引?

177
我写了这个:
public static class EnumerableExtensions
{
    public static int IndexOf<T>(this IEnumerable<T> obj, T value)
    {
        return obj
            .Select((a, i) => (a.Equals(value)) ? i : -1)
            .Max();
    }

    public static int IndexOf<T>(this IEnumerable<T> obj, T value
           , IEqualityComparer<T> comparer)
    {
        return obj
            .Select((a, i) => (comparer.Equals(a, value)) ? i : -1)
            .Max();
    }
}

但是我不知道它是否已经存在,它存在吗?

5
Max方法的问题在于:a:它会继续查找,b:当存在重复项时返回最后一个索引(人们通常希望是第一个索引)。请注意不改变原意并使翻译通俗易懂。 - Marc Gravell
4
geekswithblogs.net网站上,比较了4种解决方案及其性能。其中ToList()/FindIndex()技巧表现最佳。 - nixda
@nixda 那个链接不起作用。但ToList()听起来不是最有效的解决方案。Marc Graveli的那个会在找到匹配项时停止。 - KevinVictor
1
@KevinVictor 你仍然可以通过web.archive.org查看它。 - nixda
哦,有趣...如果真的是这样的话,那会改变最佳答案是什么,希望有人可以验证一下。 - KevinVictor
也许这取决于实现IEnumerable接口的底层对象是什么。 - KevinVictor
12个回答

146

我有些疑虑,但也许:

source.TakeWhile(x => x != value).Count();

(如果需要)使用EqualityComparer<T>.Default来模拟!=,但需要注意如果找不到则返回-1...所以也许最好还是按照长方法来实现。
public static int IndexOf<T>(this IEnumerable<T> source, T value)
{
    int index = 0;
    var comparer = EqualityComparer<T>.Default; // or pass in as a parameter
    foreach (T item in source)
    {
        if (comparer.Equals(item, value)) return index;
        index++;
    }
    return -1;
}

11
对“质疑智慧”的支持加一。十次中有九次,这可能首先就是个坏主意。 - Joel Coehoorn
显式循环解决方案在最坏情况下的运行速度也比Select().Max()解决方案快2倍。 - Steve Guidi
1
你可以使用Lambda表达式来计算元素的数量,而无需使用TakeWhile - 这样可以节省一个循环: source.Count(x => x != value); - Kamarey
13
@Kamarey - 不是的,它们的作用不同。TakeWhile方法会在遇到失败时停止;而Count(predicate)会返回符合条件的数量。例如,如果第一个元素不符合条件,但其余所有元素都符合条件,使用TakeWhile(pred).Count()将返回0,而Count(pred)将返回n-1。 - Marc Gravell
3
TakeWhile 很聪明!但要记住,如果元素不存在,则会返回 Count,这与标准行为有所偏差。 - nawfal
“长路”解决方案可能可以通过为可以转换为IList<T>IReadOnlyList<T>的源提供快速路径来改善性能。 - Theodor Zoulias

59

将事物作为IEnumerable输出的全部意义在于您可以懒惰地迭代其内容。因此,实际上并没有索引的概念。如果您需要支持通过索引访问的内容,请将其放入实际列表或集合中,对于一个IEnumerable来说,您所做的事情确实没有太多意义。


8
我看到了这篇文章,因为我正在实现一个通用的IList<>包装器,以便将我的IEnumerable<>对象与仅支持IList类型数据源的第三方组件一起使用。 我同意,在IEnumerable对象中尝试获取元素索引在大多数情况下可能表明有些地方做错了,但是有时候为了找到单个元素的索引而不是为了重现一个大的集合,从而在内存中占用过多的空间,一次查找这样的索引更加有效,特别是当你已经有了IEnumerable对象的时候。 - jpierson
267
-1 原因:有合理的理由想要从 IEnumerable<> 中获取索引。我不相信整个“你不应该这样做”的教条主义。 - John Alexiou
95
同意@ja72的观点;如果你不应该使用IEnumerable来处理索引,那么Enumerable.ElementAt也就不应该存在。IndexOf仅仅是它的反向操作--任何反对IndexOf的论点同样适用于ElementAt - Kirk Woll
9
C#缺少IIndexableEnumerable的概念,这个概念等同于C++ STL术语中的“随机访问”概念。我IndexableEnumerable是指可索引枚举的接口。 - v.oddou
20
像 Select((x, i) => ...) 这样带有重载的扩展方法似乎意味着这些索引应该存在。 - Michael
显示剩余10条评论

32
我会这样实现它:
public static class EnumerableExtensions
{
    public static int IndexOf<T>(this IEnumerable<T> obj, T value)
    {
        return obj.IndexOf(value, null);
    }

    public static int IndexOf<T>(this IEnumerable<T> obj, T value, IEqualityComparer<T> comparer)
    {
        comparer = comparer ?? EqualityComparer<T>.Default;
        var found = obj
            .Select((a, i) => new { a, i })
            .FirstOrDefault(x => comparer.Equals(x.a, value));
        return found == null ? -1 : found.i;
    }
}

1
这实际上非常可爱,+1!它涉及到额外的对象,但它们应该是相对便宜(GEN0),所以不是一个很大的问题。==可能需要修改? - Marc Gravell
1
添加了IEqualityComparer重载,以真正的LINQ风格。 ;) - dahlbyk
1
我想你的意思是... comparer.Equals(x.a, value) =) - Marc
由于Select表达式返回的是组合结果,然后进行处理,我想明确使用KeyValuePair值类型将允许您避免任何堆分配,只要.NET实现在堆栈上分配值类型,并且LINQ可能生成的任何状态机都使用一个字段作为选择的结果,该字段未声明为裸Object(从而导致KVP结果被装箱)。当然,您必须重新设计found == null条件(因为现在found将是KVP值)。也许使用DefaultIfEmpty()或KVP<T, int?>(可空索引) - kornman00
你说得对,中间结果确实需要额外的分配空间。但是如果你非常注重性能,可能会完全避免使用LINQ。 :) - dahlbyk
1
实现很不错,但我建议添加一个检查,以查看 obj 是否实现了 IList<T> 接口,如果是,则在需要时可以调用它的 IndexOf 方法,以防它有类型特定的优化。 - Josh

23

我目前的做法比已经提出的方法要简短一些,据我所知能够得到期望的结果:

 var index = haystack.ToList().IndexOf(needle);

它有点笨重,但它完成了工作并相当简洁。


8
虽然对于小集合而言这个方法是可行的,但如果你有一个包含一百万个元素的“大堆”,对它执行ToList()操作将会迭代遍历全部一百万个元素并将它们添加到列表中。然后程序将再次搜索整个列表以寻找匹配元素的索引。这种做法极其低效,而且当列表变得太大时可能会抛出异常。 - esteuart
5
当然,你需要选择适合你使用情况的方法。我怀疑没有一种通用的解决方案,可能这就是为什么核心库中没有实现的原因。 - Mark Watts
1
@KevinVictor 这是一篇有趣的文章,尽管它可能没有展示整个画面。ToList() 方法有两个分支:一个是当底层对象实现了 ICollection<T>,另一个是没有实现时。似乎作者使用的算法使用了 List 作为后备实例。因为 List<T> 实现了 ICollection<T>,所以它采用了第一个分支,这会对底层数组进行内存复制。这非常快速,并且可以解释结果。我很想看到使用不实现 ICollection 的 IEnumerable<T> 实例进行比较的结果。 - esteuart
1
@KevinVictor 如果源文件足够大,仍然存在 OutOfMemoryException 的问题。 - esteuart
1
@KevinVictor 那是一个非常好的观点。也许最好的解决方案是检查可枚举对象是否为 List<T>,如果是,则执行 IndexOf()(就像你说的那样)。如果不是,则检查它是否实现了 ICollection<T>(数组是这样做的)。如果是,则执行 ToList().IndexOf(),否则使用迭代方法来查找索引。 - esteuart
显示剩余2条评论

13
我认为最好的选择是这样实现:

我认为最佳方案是这样实现:

public static int IndexOf<T>(this IEnumerable<T> enumerable, T element, IEqualityComparer<T> comparer = null)
{
    int i = 0;
    comparer = comparer ?? EqualityComparer<T>.Default;
    foreach (var currentElement in enumerable)
    {
        if (comparer.Equals(currentElement, element))
        {
            return i;
        }

        i++;
    }

    return -1;
}

它也不会创建匿名对象


8

捕获位置的最佳方法是使用FindIndex函数,该函数仅适用于List<>

示例:

int id = listMyObject.FindIndex(x => x.Id == 15); 

如果您有枚举器或数组,请使用以下方式。
int id = myEnumerator.ToList().FindIndex(x => x.Id == 15); 

或者

 int id = myArray.ToList().FindIndex(x => x.Id == 15); 

5
有点晚了,我知道...但这是我最近做的。它与你的略有不同,但允许程序员决定相等操作需要什么(谓词)。当处理不同类型时,我发现这非常有用,因为我可以以通用方式进行操作,而不管对象类型和<T>内置的相等运算符。
它还具有非常小的内存占用和非常快/高效的特点...如果您关心的话。
在最坏的情况下,您只需将其添加到扩展列表中即可。
无论如何...就是这样。
 public static int IndexOf<T>(this IEnumerable<T> source, Func<T, bool> predicate)
 {
     int retval = -1;
     var enumerator = source.GetEnumerator();

     while (enumerator.MoveNext())
     {
         retval += 1;
         if (predicate(enumerator.Current))
         {
             IDisposable disposable = enumerator as System.IDisposable;
             if (disposable != null) disposable.Dispose();
             return retval;
         }
     }
     IDisposable disposable = enumerator as System.IDisposable;
     if (disposable != null) disposable.Dispose();
     return -1;
 }

希望这能帮助到某些人。

1
也许我漏掉了什么,但为什么要使用 GetEnumeratorMoveNext 而不是直接使用 foreach - Josh Gallagher
1
短答案是“效率”。长答案请参考:http://msdn.microsoft.com/en-us/library/9yb8xew9.aspx。 - MaxOvrdrv
2
从 IL 的角度来看,性能差异似乎在于 foreach 会调用枚举器的 Dispose 方法(如果它实现了 IDisposable 接口)。 (请参见 https://dev59.com/rm445IYBdhLWcg3wNXg_)由于此答案中的代码不知道调用 GetEnumerator 的结果是可处理的还是不可处理的,因此它应该执行相同的操作。 在这一点上,我仍然不清楚是否存在性能优势,尽管有些额外的 IL 对我来说目的并不明显! - Josh Gallagher
2
阅读这篇博客(http://blogs.msdn.com/b/ericlippert/archive/2010/09/30/the-truth-about-value-types.aspx),也许他所指的“迭代器循环”是一个`foreach`循环,在这种情况下,对于`T`是值类型的特定情况,使用while循环可能会节省一次装箱/拆箱操作。然而,通过使用`foreach`得到的IL并不支持这一点。但我仍然认为迭代器的条件释放很重要。你能否修改答案以包含这一点? - Josh Gallagher
抱歉 - 这是危险的过早优化的一个好例子,缺乏足够的理解并忘记进行基准测试???o.O...上面的处理没有正确实现,当“正确”时(比上面少),性能在所有意图和目的上都是相同的。(嗯,foreach循环本质上被降低到相同的代码,所以当然!...)而且它们分配的完全相同。因此,我会避免这个答案。 - Jens
显示剩余2条评论

5
几年后,使用 Linq 实现,如果未找到则返回 -1,不会创建额外的对象,并且在找到时“应该”短路 [与在整个 IEnumerable 上迭代相比]:
public static int IndexOf<T>(this IEnumerable<T> list, T item)
{
    return list.Select((x, index) => EqualityComparer<T>.Default.Equals(item, x)
                                     ? index
                                     : -1)
               .FirstOr(x => x != -1, -1);
}

“FirstOr”是什么:

public static T FirstOr<T>(this IEnumerable<T> source, T alternate)
{
    return source.DefaultIfEmpty(alternate)
                 .First();
}

public static T FirstOr<T>(this IEnumerable<T> source, Func<T, bool> predicate, T alternate)
{
    return source.Where(predicate)
                 .FirstOr(alternate);
}

另一种方法可以是:public static int IndexOf<T>(this IEnumerable<T> list, T item) { int e = list.Select((x, index) => EqualityComparer<T>.Default.Equals(item, x) ? x + 1 : -1) .FirstOrDefault(x => x > 0); return (e == 0) ? -1 : e - 1); } - Anu Thomas Chandy
“不会创建额外的对象”。LINQ实际上会在后台创建对象,因此这并不完全正确。例如,source.Wheresource.DefaultIfEmpty都会创建一个IEnumerable - Martin Odhelius

4

今天在寻找答案时偶然发现了这个问题,我想把我的版本加入到列表中(无意冒犯)。它利用了C#6.0的空条件运算符。

IEnumerable<Item> collection = GetTheCollection();

var index = collection
.Select((item,idx) => new { Item = item, Index = idx })
//or .FirstOrDefault(_ =>  _.Item.Prop == something)
.FirstOrDefault(_ => _.Item == itemToFind)?.Index ?? -1;

我已经进行了一些“老马赛跑”(测试),对于大型集合(~100,000),最坏情况下你想要的项在末尾,这比使用ToList().FindIndex()2倍。如果你想要的项在中间,则速度快约4倍
对于较小的集合(~10,000),它似乎只快了一点点。
以下是我的测试方法:https://gist.github.com/insulind/16310945247fcf13ba186a45734f254e

1

寻找索引的另一种方法是将Enumerable进行包装,这与使用Linq GroupBy()方法有些相似。

public static class IndexedEnumerable
{
    public static IndexedEnumerable<T> ToIndexed<T>(this IEnumerable<T> items)
    {
        return IndexedEnumerable<T>.Create(items);
    }
}

public class IndexedEnumerable<T> : IEnumerable<IndexedEnumerable<T>.IndexedItem>
{
    private readonly IEnumerable<IndexedItem> _items;

    public IndexedEnumerable(IEnumerable<IndexedItem> items)
    {
        _items = items;
    }

    public class IndexedItem
    {
        public IndexedItem(int index, T value)
        {
            Index = index;
            Value = value;
        }

        public T Value { get; private set; }
        public int Index { get; private set; }
    }

    public static IndexedEnumerable<T> Create(IEnumerable<T> items)
    {
        return new IndexedEnumerable<T>(items.Select((item, index) => new IndexedItem(index, item)));
    }

    public IEnumerator<IndexedItem> GetEnumerator()
    {
        return _items.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

这提供了一个使用案例:

var items = new[] {1, 2, 3};
var indexedItems = items.ToIndexed();
foreach (var item in indexedItems)
{
    Console.WriteLine("items[{0}] = {1}", item.Index, item.Value);
}

很好的基线。同时添加成员IsEven,IsOdd,IsFirst和IsLast也是有帮助的。 - JJS

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