Ответ 1
В .NET 4 это становится еще проще: -
var input = new[] { "a", "b", "c", "d", "e", "f" };
var result = input.Zip(input.Skip(1), (a, b) => Tuple.Create(a, b));
Если у меня есть IEnumerable, например:
string[] items = new string[] { "a", "b", "c", "d" };
Я хотел бы пройти через все пары последовательных элементов (скользящее окно размером 2). Что будет
("a","b"), ("b", "c"), ("c", "d")
Мое решение было в этом
public static IEnumerable<Pair<T, T>> Pairs(IEnumerable<T> enumerable) {
IEnumerator<T> e = enumerable.GetEnumerator(); e.MoveNext();
T current = e.Current;
while ( e.MoveNext() ) {
T next = e.Current;
yield return new Pair<T, T>(current, next);
current = next;
}
}
// used like this :
foreach (Pair<String,String> pair in IterTools<String>.Pairs(items)) {
System.Out.PrintLine("{0}, {1}", pair.First, pair.Second)
}
Когда я писал этот код, я задавался вопросом, есть ли уже в .NET Framework функции, которые делают то же самое и делают это не только для пар, но и для кортежей любого размера. ИМХО должен быть хороший способ сделать такие операции скользящего окна.
Я использую С# 2.0, и могу себе представить, что с С# 3.0 (w/LINQ) есть больше (и более приятных) способов сделать это, но меня в первую очередь интересуют решения С# 2.0. Хотя, я также буду признателен за решения С# 3.0.
В .NET 4 это становится еще проще: -
var input = new[] { "a", "b", "c", "d", "e", "f" };
var result = input.Zip(input.Skip(1), (a, b) => Tuple.Create(a, b));
Вместо того, чтобы требовать тип кортежа (пары), почему бы просто не принять селектор:
public static IEnumerable<TResult> Pairwise<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TSource, TResult> resultSelector)
{
TSource previous = default(TSource);
using (var it = source.GetEnumerator())
{
if (it.MoveNext())
previous = it.Current;
while (it.MoveNext())
yield return resultSelector(previous, previous = it.Current);
}
}
Позволяет пропустить промежуточный объект, если вы хотите:
string[] items = new string[] { "a", "b", "c", "d" };
var pairs = items.Pairwise((x, y) => string.Format("{0},{1}", x, y));
foreach(var pair in pairs)
Console.WriteLine(pair);
Или вы можете использовать анонимный тип:
var pairs = items.Pairwise((x, y) => new { First = x, Second = y });
Самый простой способ - использовать ReactiveExtensions
using System.Reactive;
using System.Reactive.Linq;
и сделайте себе метод расширения для набора bash this вместе
public static IEnumerable<IList<T>> Buffer<T>(this IEnumerable<T> seq, int bufferSize, int stepSize)
{
return seq.ToObservable().Buffer(bufferSize, stepSize).ToEnumerable();
}
Немного поздно для вечеринки, но в качестве альтернативы всем этим методам расширения можно использовать фактическое "скользящее" Collection
для хранения (и удаления) данных.
Вот один, который я сделал сегодня:
public class SlidingWindowCollection<T> : ICollection<T>
{
private int _windowSize;
private Queue<T> _source;
public SlidingWindowCollection(int windowSize)
{
_windowSize = windowSize;
_source = new Queue<T>(windowSize);
}
public void Add(T item)
{
if (_source.Count == _windowSize)
{
_source.Dequeue();
}
_source.Enqueue(item);
}
public void Clear()
{
_source.Clear();
}
...and just keep forwarding all other ICollection<T> methods to _source.
}
Использование:
int pairSize = 2;
var slider = new SlidingWindowCollection<string>(pairSize);
foreach(var item in items)
{
slider.Add(item);
Console.WriteLine(string.Join(", ", slider));
}
Развернувшись на предыдущем ответе, чтобы избежать подхода O (n 2), явно используя переданный итератор:
public static IEnumerable<IEnumerable<T>> Tuples<T>(this IEnumerable<T> input, int groupCount) {
if (null == input) throw new ArgumentException("input");
if (groupCount < 1) throw new ArgumentException("groupCount");
var e = input.GetEnumerator();
bool done = false;
while (!done) {
var l = new List<T>();
for (var n = 0; n < groupCount; ++n) {
if (!e.MoveNext()) {
if (n != 0) {
yield return l;
}
yield break;
}
l.Add(e.Current);
}
yield return l;
}
}
Для С# 2 перед процедурами расширения отпустите "this" из входного параметра и вызовите как статический метод.
Решение С# 3.0 (извините:)
public static IEnumerable<IEnumerable<T>> Tuples<T>(this IEnumerable<T> sequence, int nTuple)
{
if(nTuple <= 0) throw new ArgumentOutOfRangeException("nTuple");
for(int i = 0; i <= sequence.Count() - nTuple; i++)
yield return sequence.Skip(i).Take(nTuple);
}
Это не самая результативная в мире, но на нее приятно смотреть.
Действительно, единственное, что делает это решение С# 3.0, это конструкция .Skip.Take, поэтому, если вы просто измените это на добавление элементов в этом диапазоне к списку вместо этого, оно должно быть золотым для 2.0. Тем не менее, он все еще не работает.
Вот мое решение, используя Stack. Это короткий и краткий.
string[] items = new string[] { "a", "b", "c", "d" };
Stack<string> stack = new Stack<string>(items.Reverse());
while(stack.Count > 1)
{
Console.WriteLine("{0},{1}", stack.Pop(), stack.Peek());
}
Альтернативная реализация Pairs
, используя последнюю пару для сохранения предыдущего значения:
static IEnumerable<Pair<T, T>> Pairs( IEnumerable<T> collection ) {
Pair<T, T> pair = null;
foreach( T item in collection ) {
if( pair == null )
pair = Pair.Create( default( T ), item );
else
yield return pair = Pair.Create( pair.Second, item );
}
}
Простая реализация Window
(только безопасная для частного использования, если вызывающий абонент не сохраняет возвращенные массивы, см. примечание):
static IEnumerable<T[]> Window( IEnumerable<T> collection, int windowSize ) {
if( windowSize < 1 )
yield break;
int index = 0;
T[] window = new T[windowSize];
foreach( var item in collection ) {
bool initializing = index < windowSize;
// Shift initialized window to accomodate new item.
if( !initializing )
Array.Copy( window, 1, window, 0, windowSize - 1 );
// Add current item to window.
int itemIndex = initializing ? index : windowSize - 1;
window[itemIndex] = item;
index++;
bool initialized = index >= windowSize;
if( initialized )
//NOTE: For public API, should return array copy to prevent
// modifcation by user, or use a different type for the window.
yield return window;
}
}
Пример использования:
for( int i = 0; i <= items.Length; ++i ) {
Console.WriteLine( "Window size {0}:", i );
foreach( string[] window in IterTools<string>.Window( items, i ) )
Console.WriteLine( string.Join( ", ", window ) );
Console.WriteLine( );
}
Модуль F # Seq
определяет парную функцию над IEnumerable<T>
, но эта функция не находится в платформе .NET.
Если бы он был уже в платформе .NET, вместо того, чтобы возвращать пары, он, вероятно, согласился бы с селекторной функцией из-за отсутствия поддержки кортежей на таких языках, как С# и VB.
var pairs = ns.Pairwise( (a, b) => new { First = a, Second = b };
Я не думаю, что любой из ответов здесь действительно улучшит вашу простую реализацию итератора, которая казалась мне самой natural мне (и плакат dahlbyk по внешнему виду вещей!) тоже.
Что-то вроде этого:
public static IEnumerable<TResult> Pairwise<T, TResult>(this IEnumerable<T> enumerable, Func<T, T, TResult> selector)
{
var previous = enumerable.First();
foreach (var item in enumerable.Skip(1))
{
yield return selector(previous, item);
previous = item;
}
}
Просто для удобства, вот версия ответа @dahlbyk без селекторной версии.
public static IEnumerable<Tuple<T, T>> Pairwise<T>(this IEnumerable<T> enumerable)
{
var previous = default(T);
using (var e = enumerable.GetEnumerator())
{
if (e.MoveNext())
previous = e.Current;
while (e.MoveNext())
yield return Tuple.Create(previous, previous = e.Current);
}
}