【发布时间】:2012-11-08 15:12:47
【问题描述】:
假设我有以下字符串数组:
string[] str = new string[] {"max", "min", "avg", "max", "avg", "min"}
是否可以使用 LINQ 获取与一个字符串匹配的索引列表?
例如,我想搜索字符串“avg”并得到一个包含
的列表2、4
表示“avg”可以在 str[2] 和 str[4] 处找到。
【问题讨论】:
假设我有以下字符串数组:
string[] str = new string[] {"max", "min", "avg", "max", "avg", "min"}
是否可以使用 LINQ 获取与一个字符串匹配的索引列表?
例如,我想搜索字符串“avg”并得到一个包含
的列表2、4
表示“avg”可以在 str[2] 和 str[4] 处找到。
【问题讨论】:
.Select 有一个很少使用的产生索引的重载。你可以这样使用它:
str.Select((s, i) => new {i, s})
.Where(t => t.s == "avg")
.Select(t => t.i)
.ToList()
结果将是一个包含 2 和 4 的列表。
【讨论】:
Where 也提供了重载。
你可以这样做:
str.Select((v,i) => new {Index = i, Value = v}) // Pair up values and indexes
.Where(p => p.Value == "avg") // Do the filtering
.Select(p => p.Index); // Keep the index and drop the value
关键步骤是使用the overload of Select 为您的函子提供当前索引。
【讨论】:
您可以使用传递索引的Enumerable.Select 的重载,然后在匿名类型上使用Enumerable.Where:
List<int> result = str.Select((s, index) => new { s, index })
.Where(x => x.s== "avg")
.Select(x => x.index)
.ToList();
如果您只想查找第一个/最后一个索引,您还可以使用内置方法 List.IndexOf 和 List.LastIndexOf:
int firstIndex = str.IndexOf("avg");
int lastIndex = str.LastIndexOf("avg");
(或者您可以使用this overload 以开始索引来指定开始位置)
【讨论】:
首先,您的代码实际上并没有迭代列表两次,它只迭代一次。
也就是说,您的 Select 实际上只是获取所有索引的序列;使用 Enumerable.Range 更容易做到这一点:
var result = Enumerable.Range(0, str.Count)
.Where(i => str[i] == "avg")
.ToList();
理解为什么列表实际上没有迭代两次需要一些时间来适应。我会尽量给出一个基本的解释。
您应该将大多数 LINQ 方法(例如 Select 和 Where)视为管道。每种方法都做了一些微小的工作。在 Select 的情况下,你给它一个方法,它本质上说,“每当有人问我下一个项目时,我会首先向我的输入序列询问一个项目,然后使用我必须将其转换为其他东西的方法,然后把那个东西交给任何使用我的人。”或多或少是在说,“每当有人问我要一个项目时,我都会向我的输入序列询问一个项目,如果函数说它很好,我会传递它,如果不是,我会继续要求项目直到我得到一个通过。”
因此,当您链接它们时,会发生 ToList 要求第一项,它转到 Where to 作为它的第一项, Where 转到 Select 并询问它的第一项, Select 转到列表询问它对于它的第一个项目。然后列表提供它的第一项。 Select 然后将该项目转换为它需要吐出的内容(在这种情况下,只是 int 0)并将其提供给 Where。 Where 获取该项目并运行它的函数,该函数确定它是真的,因此向 ToList 吐出 0,将其添加到列表中。然后整个事情又发生了9次。这意味着 Select 最终将只要求列表中的每个项目一次,并将其每个结果直接提供给 Where,后者会将“通过测试”的结果直接提供给 ToList,后者将它们存储在列表中.所有 LINQ 方法都经过精心设计,只对源序列进行一次迭代(当它们被迭代一次时)。
请注意,虽然这对您来说一开始看起来很复杂,但实际上计算机可以轻松完成所有这些工作。它实际上并不像最初看起来那样需要大量性能。
【讨论】:
虽然您可以使用 Select 和 Where 的组合,但这可能是您创建自己的函数的好选择:
public static IEnumerable<int> Indexes<T>(IEnumerable<T> source, T itemToFind)
{
if (source == null)
throw new ArgumentNullException("source");
int i = 0;
foreach (T item in source)
{
if (object.Equals(itemToFind, item))
{
yield return i;
}
i++;
}
}
【讨论】:
itemToFind 应该是一个合法的用例,但对不同的东西有投票权。
您需要一个组合的 select 和 where 运算符,与接受的答案相比,这会更便宜,因为不需要中间对象:
public static IEnumerable<TResult> SelectWhere<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, bool> filter, Func<TSource, int, TResult> selector)
{
int index = -1;
foreach (var s in source)
{
checked{ ++index; }
if (filter(s))
yield return selector(s, index);
}
}
【讨论】: