如何获得索引使用LINQ?

给定一个这样的数据源:

var c = new Car[]
{
new Car{ Color="Blue", Price=28000},
new Car{ Color="Red", Price=54000},
new Car{ Color="Pink", Price=9999},
// ..
};

如何用LINQ找到满足特定条件的第一辆车的指数 ?

编辑:

我可以想到这样的东西,但看起来很可怕:

int firstItem = someItems.Select((item, index) => new
{
ItemName = item.Color,
Position = index
}).Where(i => i.ItemName == "purple")
.First()
.Position;

用一个普通的循环来解决这个问题会是最好的吗?

506324 次浏览
myCars.Select((v, i) => new {car = v, index = i}).First(myCondition).index;

或者稍微短一点的

myCars.Select((car, index) => new {car, index}).First(myCondition).index;

或者稍微短一点的

myCars.Select((car, index) => (car, index)).First(myCondition).index;

IEnumerable不是有序集 虽然大多数IEnumerables是有序的,但有些(如DictionaryHashSet)不是有序的

因此,LINQ没有IndexOf方法。

但是,你可以自己写一个:

///<summary>Finds the index of the first item matching an expression in an enumerable.</summary>
///<param name="items">The enumerable to search.</param>
///<param name="predicate">The expression to test the items against.</param>
///<returns>The index of the first matching item, or -1 if no items match.</returns>
public static int FindIndex<T>(this IEnumerable<T> items, Func<T, bool> predicate) {
if (items == null) throw new ArgumentNullException("items");
if (predicate == null) throw new ArgumentNullException("predicate");


int retVal = 0;
foreach (var item in items) {
if (predicate(item)) return retVal;
retVal++;
}
return -1;
}
///<summary>Finds the index of the first occurrence of an item in an enumerable.</summary>
///<param name="items">The enumerable to search.</param>
///<param name="item">The item to find.</param>
///<returns>The index of the first matching item, or -1 if the item was not found.</returns>
public static int IndexOf<T>(this IEnumerable<T> items, T item) { return items.FindIndex(i => EqualityComparer<T>.Default.Equals(item, i)); }

这是我刚刚做的一个扩展。

public static class PositionsExtension
{
public static Int32 Position<TSource>(this IEnumerable<TSource> source,
Func<TSource, bool> predicate)
{
return Positions<TSource>(source, predicate).FirstOrDefault();
}
public static IEnumerable<Int32> Positions<TSource>(this IEnumerable<TSource> source,
Func<TSource, bool> predicate)
{
if (typeof(TSource) is IDictionary)
{
throw new Exception("Dictionaries aren't supported");
}


if (source == null)
{
throw new ArgumentOutOfRangeException("source is null");
}
if (predicate == null)
{
throw new ArgumentOutOfRangeException("predicate is null");
}
var found = source.Where(predicate).First();
var query = source.Select((item, index) => new
{
Found = ReferenceEquals(item, found),
Index = index


}).Where( it => it.Found).Select( it => it.Index);
return query;
}
}

然后你可以这样叫它。

IEnumerable<Int32> indicesWhereConditionIsMet =
ListItems.Positions(item => item == this);


Int32 firstWelcomeMessage ListItems.Position(msg =>
msg.WelcomeMessage.Contains("Hello"));

我将在这里做出我的贡献……为什么?这是一个不同的实现,基于任何LINQ扩展,和一个委托。下面就是:

public static class Extensions
{
public static int IndexOf<T>(
this IEnumerable<T> list,
Predicate<T> condition) {
int i = -1;
return list.Any(x => { i++; return condition(x); }) ? i : -1;
}
}


void Main()
{
TestGetsFirstItem();
TestGetsLastItem();
TestGetsMinusOneOnNotFound();
TestGetsMiddleItem();
TestGetsMinusOneOnEmptyList();
}


void TestGetsFirstItem()
{
// Arrange
var list = new string[] { "a", "b", "c", "d" };


// Act
int index = list.IndexOf(item => item.Equals("a"));


// Assert
if(index != 0)
{
throw new Exception("Index should be 0 but is: " + index);
}


"Test Successful".Dump();
}


void TestGetsLastItem()
{
// Arrange
var list = new string[] { "a", "b", "c", "d" };


// Act
int index = list.IndexOf(item => item.Equals("d"));


// Assert
if(index != 3)
{
throw new Exception("Index should be 3 but is: " + index);
}


"Test Successful".Dump();
}


void TestGetsMinusOneOnNotFound()
{
// Arrange
var list = new string[] { "a", "b", "c", "d" };


// Act
int index = list.IndexOf(item => item.Equals("e"));


// Assert
if(index != -1)
{
throw new Exception("Index should be -1 but is: " + index);
}


"Test Successful".Dump();
}


void TestGetsMinusOneOnEmptyList()
{
// Arrange
var list = new string[] {  };


// Act
int index = list.IndexOf(item => item.Equals("e"));


// Assert
if(index != -1)
{
throw new Exception("Index should be -1 but is: " + index);
}


"Test Successful".Dump();
}


void TestGetsMiddleItem()
{
// Arrange
var list = new string[] { "a", "b", "c", "d", "e" };


// Act
int index = list.IndexOf(item => item.Equals("c"));


// Assert
if(index != 2)
{
throw new Exception("Index should be 2 but is: " + index);
}


"Test Successful".Dump();
}
myCars.TakeWhile(car => !myCondition(car)).Count();

它的工作原理!想想看。第一个匹配项的索引等于它前面(不匹配)项的个数。

故事时间

我也不喜欢你在你的问题中已经建议的糟糕的标准溶液。就像接受的答案一样,我选择了一个简单的循环,尽管有轻微的修改:

public static int FindIndex<T>(this IEnumerable<T> items, Predicate<T> predicate) {
int index = 0;
foreach (var item in items) {
if (predicate(item)) break;
index++;
}
return index;
}

注意,当没有匹配时,它将返回项的数量而不是-1。但让我们暂时忽略这个小烦恼。事实上,在这种情况下糟糕的标准溶液我考虑返回一个超界的索引会崩溃。

现在ReSharper告诉我循环可以转换为linq表达式。虽然大多数情况下,该功能的可读性会变差,但这次的结果令人惊叹。所以,向JetBrains致敬。

分析

优点

  • 简洁的
  • 可与其他LINQ组合使用
  • 避免newing匿名对象
  • 在谓词第一次匹配之前,只计算可枚举值

因此,我认为在保持可读性的同时,它在时间和空间上是最佳的。

缺点

  • 一开始不太明显
  • 当没有匹配时不返回-1

当然,您总是可以将它隐藏在扩展方法后面。在没有匹配的情况下,如何做最好,很大程度上取决于上下文。

下面是投票最多的答案的实现,当项目没有找到时返回-1:

public static int FindIndex<T>(this IEnumerable<T> items, Func<T, bool> predicate)
{
var itemsWithIndices = items.Select((item, index) => new { Item = item, Index = index });
var matchingIndices =
from itemWithIndex in itemsWithIndices
where predicate(itemWithIndex.Item)
select (int?)itemWithIndex.Index;


return matchingIndices.FirstOrDefault() ?? -1;
}

简单地做:

int index = List.FindIndex(your condition);

如。

int index = cars.FindIndex(c => c.ID == 150);