如何获取 IEnumerable 中元素的索引?
Posted
技术标签:
【中文标题】如何获取 IEnumerable 中元素的索引?【英文标题】:How to get the index of an element in an IEnumerable? 【发布时间】:2009-08-17 21:37:23 【问题描述】:这是我写的:
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();
但我不知道它是否已经存在,是吗?
【问题讨论】:
Max
方法的问题在于 a:它一直在寻找,而 b:当有重复时它返回 last 索引(人们通常期望第一个索引)
geekswithblogs.net 比较了 4 个解决方案及其性能。 ToList()/FindIndex()
技巧表现最好
@nixda 该链接无效。但是 ToList() 听起来并不是最有效的解决方案。 Marc Graveli 在找到匹配项时停止。
@KevinVictor 你仍然可以通过web.archive.org查看它
哦,有趣...如果确实如此,那将改变最佳答案。 (希望有人可以验证)
【参考方案1】:
我会质疑智慧,但也许:
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;
【讨论】:
+1 表示“质疑智慧”。 10 次中有 9 次首先可能是个坏主意。 显式循环解决方案的运行速度也比 Select().Max() 解决方案快 2 倍(在最坏的情况下)。 您可以在不使用 TakeWhile 的情况下通过 lambda 计数元素 - 它可以节省一个循环:source.Count(x => x != value); @Kamarey - 不,这会有所不同。 TakeWhile 在失败时停止; Count(predicate) 返回匹配的那些。即,如果第一个是未命中并且其他一切都是真的,TakeWhile(pred).Count() 将报告 0; Count(pred) 将报告 n-1。TakeWhile
很聪明!请记住,如果元素不存在,则返回 Count
,这与标准行为有偏差。【参考方案2】:
将事物输出为 IEnumerable 的全部意义在于,您可以懒惰地迭代内容。因此,真的没有索引的概念。对于 IEnumerable,您正在做的事情真的没有多大意义。如果您需要支持按索引访问的内容,请将其放入实际的列表或集合中。
【讨论】:
目前我遇到了这个线程,因为我正在为 IEnumerable 类型实现一个通用 IList 包装器,以便将我的 IEnumerable 对象与仅支持数据源类型的第三方组件一起使用列表。我同意,在大多数情况下,尝试在 IEnumerable 对象中获取元素的索引可能是做错事情的迹象,有时一旦找到这样的索引而不是为了找到索引而在内存中复制大型集合当你已经有一个 IEnumerable 时,单个元素的数量。 -1 原因:您有正当理由希望从IEnumerable<>
中获取索引。我不相信整个“你应该这样做”的教条。
同意@ja72;如果您不应该使用IEnumerable
处理索引,那么Enumerable.ElementAt
将不存在。 IndexOf
正好相反——任何反对它的论点都必须同样适用于ElementAt
。
很清楚,C# 错过了 IIndexableEnumerable 的概念。这相当于 C++ STL 术语中的“随机访问”概念。
像 Select((x, i) => ...) 这样的重载扩展似乎暗示这些索引应该存在【参考方案3】:
我会这样实现它:
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!它涉及额外的对象,但它们应该相对便宜(GEN0),所以不是一个大问题。==
可能需要工作?
添加了 IEqualityComparer 重载,采用真正的 LINQ 样式。 ;)
我想你的意思是说... comparer.Equals(x.a, value) =)
由于 Select 表达式返回组合结果,然后对其进行处理,我想明确地使用 KeyValuePair 值类型将允许您避免任何类型的堆分配,只要 .NET impl 在堆栈上分配值类型,并且 LINQ 可能生成的任何状态机都使用未声明为裸对象的 Select'd 结果字段(从而导致 KVP 结果被装箱)。当然,您必须重新处理 found==null 条件(因为 found 现在将是 KVP 值)。也许使用 DefaultIfEmpty() 或 KVP<T, int?>
(可空索引)
不错的实现,虽然我建议添加的一件事是检查 obj 是否实现 IList<T>
,如果是,请遵循其 IndexOf 方法,以防它具有特定于类型的优化。 【参考方案4】:
我目前这样做的方式比已经建议的方式要短一些,据我所知,它给出了预期的结果:
var index = haystack.ToList().IndexOf(needle);
它有点笨拙,但它可以完成工作并且相当简洁。
【讨论】:
虽然这适用于小型收藏品,但假设您在“干草堆”中有一百万件物品。对其执行 ToList() 将遍历所有一百万个元素并将它们添加到列表中。然后它将搜索列表以找到匹配元素的索引。如果列表变得太大,这将非常低效并且可能引发异常。 @esteuart 绝对 - 您需要选择适合您用例的方法。我怀疑是否存在一刀切的解决方案,这可能是核心库中没有实现的原因。 @esteuart 嗯...请参阅主要问题下 cmets 中 nixda 的链接。我的想法和你的类似,但分析表明 ToList() / FindIndex() 更有效。 @KevinVictor 这是一篇有趣的文章,尽管它可能无法展示全部内容。ToList()
方法有两个分支:一个,当底层对象实现 ICollection<T>
时,另一个没有。似乎作者使用的算法使用 List 作为后备实例。因为List<T>
实现了ICollection<T>
,所以它采用第一个分支,该分支执行底层数组的内存副本。这是非常快的,并且可以解释结果。我有兴趣查看使用未实现 ICollection 的 IEnumerable<T>
实例的比较。
@KevinVictor 如果源足够大,OutOfMemoryException
仍然存在问题。【参考方案5】:
我认为最好的选择是这样实现:
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;
它也不会创建匿名对象
【讨论】:
【参考方案6】:获取位置的最佳方式是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】:游戏有点晚了,我知道...但这是我最近所做的。它与您的略有不同,但允许程序员决定相等操作需要是什么(谓词)。在处理不同类型时,我发现它非常有用,因为无论对象类型如何,我都有一种通用的方法,并且 <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;
希望这对某人有所帮助。
【讨论】:
也许我遗漏了什么,但为什么GetEnumerator
和MoveNext
而不仅仅是foreach
?
简短回答?效率。长答案:msdn.microsoft.com/en-us/library/9yb8xew9.aspx
查看IL,性能差异似乎是foreach
将在枚举器上调用Dispose
,如果它实现IDisposable
。 (请参阅***.com/questions/4982396/…)由于此答案中的代码不知道调用GetEnumerator
的结果是否是一次性的,它应该做同样的事情。在那一点上,我仍然不清楚是否有性能优势,尽管有一些额外的 IL,其目的并没有突显到我身上!
@JoshGallagher 我不久前对 foreach 和 for(i) 之间的性能优势进行了一些研究,使用 for(i) 的主要好处是它通过引用对象就地而不是而不是重新创建它/将它传回 ByVal。我会假设这同样适用于 MoveNext 与 foreach,但我不确定那个。也许他们都使用 ByVal...
阅读此博客 (blogs.msdn.com/b/ericlippert/archive/2010/09/30/…) 可能是他所指的“迭代器循环”是一个 foreach
循环,在这种情况下,T
是一个值类型它可能正在使用 while 循环保存一个装箱/拆箱操作。但是,我从您使用foreach
的答案版本中得到的 IL 并不能证实这一点。不过,我仍然认为迭代器的条件处理很重要。您能否修改答案以包含该内容?【参考方案8】:
几年后,但这使用了 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 IndexOfsource.Where
和 source.DefaultIfEmpty
都将创建一个 IEnumerable
。【参考方案9】:
今天在寻找答案时偶然发现了这个,我想我会把我的版本添加到列表中(没有双关语的意思)。它利用了 c#6.0 的 null 条件运算符
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 个),最坏的情况是您想要的物品在最后,这比它快 2 倍比做ToList().FindIndex()
。如果您想要的项目在中间,它 ~4x 更快。
对于较小的集合(约 10,000 个),它似乎只是稍微快了一点
这是我的测试方法https://gist.github.com/insulind/16310945247fcf13ba186a45734f254e
【讨论】:
【参考方案10】:事后查找索引的另一种方法是包装 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 也很有帮助。【参考方案11】:使用@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;
我想这种方式可能是最快和最简单的。但是,我还没有测试过性能。
【讨论】:
【参考方案12】:这可以通过扩展(充当代理)变得非常酷,例如:
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();
【讨论】:
以上是关于如何获取 IEnumerable 中元素的索引?的主要内容,如果未能解决你的问题,请参考以下文章
在 Razor 中,如何根据元素的属性检查 ViewBag IEnumerable 对象中是不是存在对象?