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


144

我这样写:

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();
    }
}

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


4
方法的问题Max是:a:不断查找,b:重复时返回最后一个索引(人们通常期望第一个索引)
Marc Gravell

1
geekswithblogs.net比较了4个解决方案及其性能。在ToList()/FindIndex()招效果最佳
nixda

Answers:


51

将内容发布为IEnumerable的全部目的是让您可以懒惰地遍历内容。因此,没有真正的指数的概念。对于IEnumerable,您实际上所做的事情并没有多大意义。如果需要支持按索引访问的内容,请将其放入实际的列表或集合中。


8
当前,我遇到了这个线程,因为我正在为IEnumerable <>类型实现一个通用的IList <>包装器,以便将我的IEnumerable <>对象与仅支持IList类型的数据源的第三方组件一起使用。我同意尝试在IEnumerable对象中获取元素的索引在大多数情况下可能是某种错误的标志,有时发现这样的索引一次击败了在内存中复制大量集合,只是为了查找索引当您已经有IEnumerable时,将使用单个元素的元素。
jpierson

215
-1原因:出于合理的原因,您想从中获取索引IEnumerable<>。我不买整个“你应该做的”教条。
约翰·亚历克斯

78
同意@ ja72; 如果您不应该使用索引处理,IEnumerable那么Enumerable.ElementAt将不存在。 IndexOf只是相反的-反对它的任何论点必须同样适用于ElementAt
Kirk Woll

7
显然,C#错过了IIndexableEnumerable的概念。在C ++ STL术语中,这等同于“随机可访问”的概念。
v.oddou

14
带有Select((x,i)=> ...)重载的扩展似乎暗示着这些索引应该存在
Michael

126

我会问智慧,但也许:

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;
}

8
为“质疑智慧” +1。首先,十分之九的想法很糟糕。
乔尔·科恩荷恩

显式循环解决方案的运行速度(在最坏的情况下)也比Select()。Max()解决方案快2倍。
史蒂夫·吉迪

1
您可以不使用TakeWhile就可以通过lambda来计数元素-它节省了一个循环:source.Count(x => x!= value);
Kamarey

10
@Kamarey-不,这样做有所不同。TakeWhile 停止在失败时;Count(谓词)返回匹配的。例如,如果第一个是未命中且其他所有条件都为真,则TakeWhile(pred).Count()将报告0;Count(pred)将报告n-1。
马克·格拉韦尔

1
TakeWhile很聪明!请记住,尽管Count如果元素不存在,则返回此值,这与标准行为背道而驰。
nawfal

27

我会这样实现:

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),因此不是一个大问题。在==可能需要的工作?
马克·格雷夫

1
以真正的LINQ样式添加了IEqualityComparer重载。;)
dahlbyk

1
我认为您的意思是说... comparer.Equals(xa,value)=)
Marc

由于Select表达式返回的是组合结果,然后进行处理,因此,我想像一下,只要.NET impl在堆栈上分配值类型,并且显式地使用KeyValuePair值类型,就可以避免任何类型的堆分配。 LINQ可能生成的任何状态机都使用一个字段作为Select'd结果,而不将其声明为裸对象(因此导致将KVP结果装箱)。当然,您必须重新处理Found == null条件(因为找到的条件现在是KVP值)。也许使用DefaultIfEmpty()或KVP<T, int?>(可空索引)
kornman00

1
不错的实现,尽管我建议添加的一件事是检查obj是否实现IList<T>,如果是,请遵循其IndexOf方法,以防万一它具有特定于类型的优化。
乔什(Josh)2015年

16

我目前执行此操作的方式比已经建议的方式要短一些,据我所知给出的结果是:

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

这有点笨拙,但是可以完成工作并且非常简洁。


6
尽管这对于少量收藏有用,但假设您在“干草堆”中有一百万件物品。对此执行ToList()将遍历所有一百万个元素并将它们添加到列表中。然后它将搜索列表以找到匹配元素的索引。如果列表太大,这将是极其低效的,并且可能引发异常。
esteuart

3
@esteuart一定-您需要选择一种适合您的用例的方法。我怀疑是否有一个适合所有解决方案的规模,这可能就是为什么核心库中没有实现的原因。
马克·瓦茨

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); 

7

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

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;
}

它也不会创建匿名对象


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
乔什·加拉格尔

1
简短的答案?效率。长答案:msdn.microsoft.com/en-us/library/9yb8xew9.aspx
MaxOvrdrv

2
从IL来看,似乎性能上的差别是如果实现foreach会调用Dispose枚举器IDisposable。(请参阅stackoverflow.com/questions/4982396/…)由于此答案中的代码不知道调用结果GetEnumerator是否是一次性的,因此应执行相同的操作。那时我还不清楚是否有性能方面的好处,尽管有一些额外的IL的目标并没有冲向我!
Josh Gallagher 2015年

@JoshGallagher我前段时间做了一些关于foreach和for(i)之间的性能好处的研究,而使用for(i)的主要好处是它ByRefs就地将对象而不是重新创建/传递它。返回ByVal。我认为MoveNext与foreach的情况相同,但是我不确定那一点。也许他们俩都使用ByVal ...
MaxOvrdrv

2
阅读此博客(blogs.msdn.com/b/ericlippert/archive/2010/09/30/…)可能是他所指的“迭代器循环”是一个foreach循环,在这种情况下,对于T作为值类型,它可能通过使用while循环来保存box / unbox操作。但是,这不是我从的答案中得到的IL所证实的foreach。我仍然认为有条件地处理迭代器很重要。您可以修改答案以包括该答案吗?
Josh Gallagher 2015年

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>(此IEnumerable <T>列表,T项){int e = list.Select((x,index)=> EqualityComparer <T> .Default.Equals( item,x)?x + 1:-1).FirstOrDefault(x => x> 0); 返回(e == 0)?-1:e-1); }
Anu Thomas Chandy

“不创建额外的对象”。Linq实际上将在后台创建对象,因此这不是完全正确的。两者source.Wheresource.DefaultIfEmpty将例如创建一个IEnumerable
Martin Odhelius

1

今天在寻找答案时偶然发现了这个问题,我想将自己的版本添加到列表中(无双关语)。它使用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个),最坏的情况是您想要的项目在最后,比做起来快2ToList().FindIndex()。如果您想要的物品在中间,速度大约4倍

对于较小的馆藏(约10,000个),这似乎仅稍快一点

这是我如何对其进行测试的方法https://gist.github.com/insulind/16310945247fcf13ba186a45734f254e


1

使用@Marc Gravell的答案,我找到了一种使用以下方法的方法:

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

为了在找不到该项目时获得-1:

internal static class Utils
{

    public static int IndexOf<T>(this IEnumerable<T> enumerable, T item) => enumerable.IndexOf(item, EqualityComparer<T>.Default);

    public static int IndexOf<T>(this IEnumerable<T> enumerable, T item, EqualityComparer<T> comparer)
    {
        int index = enumerable.TakeWhile(x => comparer.Equals(x, item)).Count();
        return index == enumerable.Count() ? -1 : index;
    }
}

我想这种方式可能既最快又更简单。但是,我尚未测试过性能。


0

事实结束后查找索引的另一种方法是包装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

0

通过扩展(充当代理),这可以变得非常酷,例如:

collection.SelectWithIndex(); 
// vs. 
collection.Select((item, index) => item);

它将自动将索引分配给可通过此Index属性访问的集合。

接口:

public interface IIndexable
{
    int Index { get; set; }
}

自定义扩展(可能对使用EF和DbContext最有用):

public static class EnumerableXtensions
{
    public static IEnumerable<TModel> SelectWithIndex<TModel>(
        this IEnumerable<TModel> collection) where TModel : class, IIndexable
    {
        return collection.Select((item, index) =>
        {
            item.Index = index;
            return item;
        });
    }
}

public class SomeModelDTO : IIndexable
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }

    public int Index { get; set; }
}

// In a method
var items = from a in db.SomeTable
            where a.Id == someValue
            select new SomeModelDTO
            {
                Id = a.Id,
                Name = a.Name,
                Price = a.Price
            };

return items.SelectWithIndex()
            .OrderBy(m => m.Name)
            .Skip(pageStart)
            .Take(pageSize)
            .ToList();
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.