Linq - 向前迭代

9

我正在使用访问者模式迭代一个集合,并需要访问列表中的当前项和下一项。目前,我通过使用扩展方法来完成这个功能,像这样:

public void Visit<TItem>(this IEnumerable<TItem> theList, Action<TItem, TItem> visitor)
{
    for (i = 0; i <= theList.Count - 1; i++) {
        if (i == theList.Count - 1) {
            visitor(theList(i), null);
        } else {
            visitor(theList(i), theList(i + 1));
        }    
    }    
}

我想知道是否有其他/更好/更优雅的方法来实现这一点?目前我认为我只需要访问列表中的当前和下一个项目,但我想知道是否会遇到需要预先查看接下来的'n'个项目的情况。


1
你的代码甚至无法编译。在 IEnumerable<TItem> 上没有名为 Count 的属性(只有一个方法组)。 - jason
@Jason - 谢谢 - 我最初是用VB编写的(似乎支持IEnumerable上的Count!),然后进行了转换 - 这会教训我!谢谢 - Simon Woods
5个回答

10

假设您正在使用 .NET 4,您可以使用 Zip 来实现相同的功能:

var query = original.Zip(original.Skip(1),
                         (current, next) => new { current, next });

虽然这个方法会迭代两次,但是一个更好的替代你当前的扩展方法的方式(我认为你的方法不起作用,因为IEnumerable没有Count属性,并且你还尝试将theList作为方法调用...)可能是:

public static void Visit<TItem>(this IEnumerable<TItem> theList,
                         Action<TItem, TItem> visitor)
{
    TItem prev = default(TItem);
    using (var iterator = theList.GetEnumerator())
    {
        if (!iterator.MoveNext())
        {
            return;
        }
        prev = iterator.Current;
        while (iterator.MoveNext())
        {
            TItem current = iterator.Current;
            visitor(prev, current);
            prev = current;
        }
    }
    visitor(prev, default(TItem)); // Are you sure you want this?
}

更一般的前瞻比较棘手,说实话...您可能需要一些循环缓冲区...可能是自定义集合。


@Jon Skeet - 谢谢您。您能解释一下最后一行的问题吗?我的想法是,如果列表只有一个项目,我仍然希望访问者对其进行处理。但也许我漏掉了什么。 - Simon Woods
@Simon:这意味着您无法在访问者中区分一对,其中一个项目恰好是默认值(null、0或其他),以及仅有一个项目是“真实”的最终对。 - Jon Skeet
@Jon - 啊,是的。好主意。那么在访问列表后,您需要处理最后一个元素?还是您想到了其他方法 - 特殊标志/值或其他什么? - Simon Woods
@Simon:有点像那样。老实说,这取决于具体情况。就我个人而言,我通常更喜欢返回元组序列以进行处理(有点类似Zip),而不是直接执行操作。 - Jon Skeet
@Jon:在这种情况下,一个只有1个元素的列表的元组会是<Value,Nothing>还是<Nothing,Value>? - Simon Woods
@Simon:没有任何东西。只有一个值,你就没有成对的值了。当然,你可以随时调用.Concat(Enumerable.Repeat(default(TItem), 1))来实现从我的行为到你的行为的转换 :) - Jon Skeet

4

当我们遇到类似的任务时,我们定义了一个扩展方法:

/// <summary>
/// Projects a window of source elements in a source sequence into target sequence.
/// Thus
///   target[i] = 
///     selector(source[i], source[i - 1], ... source[i - window + 1])
/// </summary>
/// <typeparam name="T">A type of elements of source sequence.</typeparam>
/// <typeparam name="R">A type of elements of target sequence.</typeparam>
/// <param name="source">A source sequence.</param>
/// <param name="window">A size of window.</param>
/// <param name="lookbehind">
/// Indicate whether to produce target if the number of source elements 
/// preceeding the current is less than the window size.
/// </param>
/// <param name="lookahead">
/// Indicate whether to produce target if the number of source elements 
/// following current is less than the window size.
/// </param>
/// <param name="selector">
/// A selector that derives target element.
/// On input it receives:
///   an array of source elements stored in round-robing fashon;
///   an index of the first element;
///   a number of elements in the array to count.
/// </param>
/// <returns>Returns a sequence of target elements.</returns>
public static IEnumerable<R> Window<T, R>(
  this IEnumerable<T> source,
  int window,
  bool lookbehind,
  bool lookahead,
  Func<T[], int, int, R> selector)
{
  var buffer = new T[window];
  var index = 0;
  var count = 0;

  foreach(var value in source)
  {
    if (count < window)
    {
      buffer[count++] = value;

      if (lookbehind || (count == window))
      {
        yield return selector(buffer, 0, count);
      }
    }
    else
    {
      buffer[index] = value;
      index = index + 1 == window ? 0 : index + 1;

      yield return selector(buffer, index, count);
    }
  }

  if (lookahead)
  {
    while(--count > 0)
    {
      index = index + 1 == window ? 0 : index + 1;

      yield return selector(buffer, index, count);
    }
  }
}

/// <summary>
/// Projects a window of source elements in a source sequence into a 
/// sequence of window arrays.
/// </summary>
/// <typeparam name="T">A type of elements of source sequence.</typeparam>
/// <typeparam name="R">A type of elements of target sequence.</typeparam>
/// <param name="source">A source sequence.</param>
/// <param name="window">A size of window.</param>
/// <param name="lookbehind">
/// Indicate whether to produce target if the number of source elements 
/// preceeding the current is less than the window size.
/// </param>
/// <param name="lookahead">
/// Indicate whether to produce target if the number of source elements 
/// following current is less than the window size.
/// </param>
/// <returns>Returns a sequence of windows.</returns>
public static IEnumerable<T[]> Window<T>(
  this IEnumerable<T> source,
  int window,
  bool lookbehind,
  bool lookahead)
{
  return source.Window(
    window,
    lookbehind,
    lookahead,
    (buffer, index, count) =>
    {
      var result = new T[count];

      for(var i = 0; i < count; ++i)
      {
        result[i] = buffer[index];
        index = index + 1 == buffer.Length ? 0 : index + 1;
      }

      return result;
    });
}

这些函数可以从输入元素的窗口中生成输出元素。
另请参见LINQ扩展

1

没有测试过,但我认为这个可以工作?当访问超出边界时,它会循环到列表的前面。

public class FriendlyEnumerable<T> : IEnumerable<T>
{
    private IEnumerable<T> _enum;

    public FriendlyEnumerable(IEnumerable<T> enumerable) 
    {            
        _enum = enumerable;
    }

    public void VisitAll(Action<T, T> visitFunc)
    {
        VisitAll(visitFunc, 1);
    }

    public void VisitAll(Action<T, T> visitFunc, int lookahead)
    {
        int index = 0;
        int length = _enum.Count();
        _enum.ToList().ForEach(t =>
        {
            for (int i = 1; i <= lookahead; i++)
                visitFunc(t, _enum.ElementAt((index + i) % length));
            index++;
        });
    }

    #region IEnumerable<T> Members
    public IEnumerator<T> GetEnumerator()
    {
        return _enum.GetEnumerator();
    }
    #endregion
}

你可以像这样使用它:

List<string> results = new List<string>();
List<string> strings = new List<string>() 
    { "a", "b", "c", "d", "a", "b", "c", "d" };
FriendlyEnumerable<string> fe = new FriendlyEnumerable<string>(strings);
Action<string, string> compareString = 
    new Action<string,string>((s1, s2) =>
        {
            if (s1 == s2)
                results.Add(s1 + " == " + s2);
        });
fe.VisitAll(compareString);
//no results
fe.VisitAll(compareString, 4);
//8 results

1
public static void VisitLookAhead<TItem>(
  this IEnumerable<TItem> source,
  Action<IEnumerable<TItem>> visitor,
  int targetSize
  )
{
  if (targetSize <= 1)
  {
    throw new Exception("invalid targetSize for VisitLookAhead");
  }

  List<List<TItem>> collections = new List<List<TItem>>();

// after 6th iteration with targetSize 6
//1, 2, 3, 4, 5, 6  <-- foundlist
//2, 3, 4, 5, 6
//3, 4, 5, 6
//4, 5, 6
//5, 6
//6
  foreach(TItem x in source)
  {
    collections.Add(new List<TItem>());
    collections.ForEach(subList => subList.Add(x));
    List<TItem> foundList = collections
      .FirstOrDefault(subList => subList.Count == targetSize);
    if (foundList != null)
    {
      collections.Remove(foundList);
      visitor(foundList);
    }
  }

  //generate extra lists at the end - when lookahead will be missing items.
  foreach(int i in Enumerable.Range(1, targetSize)
  {
    collections.ForEach(subList => subList.Add(default(TItem)));
    List<TItem> foundList = collections
      .FirstOrDefault(subList => subList.Count == targetSize);
    if (foundList != null)
    {
      collections.Remove(foundList);
      visitor(foundList);
    }
  }
}

1

看起来你使用了错误的类型。索引序列的行为将会迭代它直到每次到达指定的索引。为什么不使用 IList<T> 或者 ReadOnlyCollection<T> 呢?


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