Way Better Solution, отвечающий всем требованиям
ОК, отмените мое предыдущее решение (оставлю его ниже, только для справки). Вот гораздо лучший подход, который пришёл ко мне после написания моего первого поста.
Напишите новый класс, который реализует IEnumerator<T>
и предоставляет несколько дополнительных свойств: IsValid
и Previous
. Это все, что вам действительно нужно, чтобы разрешить весь беспорядок с необходимостью поддерживать состояние внутри блока итератора, используя yield
.
Вот как я это сделал (как вы видите, довольно тривиально):
internal class ChipmunkEnumerator<T> : IEnumerator<T> {
private readonly IEnumerator<T> _internal;
private T _previous;
private bool _isValid;
public ChipmunkEnumerator(IEnumerator<T> e) {
_internal = e;
_isValid = false;
}
public bool IsValid {
get { return _isValid; }
}
public T Previous {
get { return _previous; }
}
public T Current {
get { return _internal.Current; }
}
public bool MoveNext() {
if (_isValid)
_previous = _internal.Current;
return (_isValid = _internal.MoveNext());
}
public void Dispose() {
_internal.Dispose();
}
#region Explicit Interface Members
object System.Collections.IEnumerator.Current {
get { return Current; }
}
void System.Collections.IEnumerator.Reset() {
_internal.Reset();
_previous = default(T);
_isValid = false;
}
#endregion
}
(Я назвал это ChipmunkEnumerator
, потому что поддержание предыдущего значения напомнило мне о том, как у бурундуков есть мешочки на щеках, где они держат орехи. Это действительно имеет значение? Перестаньте смеяться надо мной.)
Теперь использование этого класса в методе расширения для обеспечения именно того поведения, которое вам нужно, не так сложно!
Обратите внимание, что ниже я определил GroupConsecutive
для фактического возврата IEnumerable<IGrouping<TKey, T>>
по той простой причине, что, если они в любом случае сгруппированы по ключу, имеет смысл возвращать IGrouping<TKey, T>
, а не просто IEnumerable<T>
, Оказывается, это все равно поможет нам позже ...
public static IEnumerable<IGrouping<TKey, T>> GroupConsecutive<T, TKey>(this IEnumerable<T> source, Func<T, TKey> keySelector)
where TKey : IEquatable<TKey> {
using (var e = new ChipmunkEnumerator<T>(source.GetEnumerator())) {
if (!e.MoveNext())
yield break;
while (e.IsValid) {
yield return e.GetNextDuplicateGroup(keySelector);
}
}
}
public static IEnumerable<IGrouping<T, T>> GroupConsecutive<T>(this IEnumerable<T> source)
where T : IEquatable<T> {
return source.GroupConsecutive(x => x);
}
private static IGrouping<TKey, T> GetNextDuplicateGroup<T, TKey>(this ChipmunkEnumerator<T> e, Func<T, TKey> keySelector)
where TKey : IEquatable<TKey> {
return new Grouping<TKey, T>(keySelector(e.Current), e.EnumerateNextDuplicateGroup(keySelector));
}
private static IEnumerable<T> EnumerateNextDuplicateGroup<T, TKey>(this ChipmunkEnumerator<T> e, Func<T, TKey> keySelector)
where TKey : IEquatable<TKey> {
do {
yield return e.Current;
} while (e.MoveNext() && keySelector(e.Previous).Equals(keySelector(e.Current)));
}
(Для реализации этих методов я написал простой класс Grouping<TKey, T>
, который реализует IGrouping<TKey, T>
самым простым способом. Я опустил код только для того, чтобы продолжать двигаться ...)
ОК, проверь. Я думаю, что приведенный ниже пример кода довольно хорошо отражает нечто, похожее на более реалистичный сценарий, который вы описали в своем обновленном вопросе.
var entries = new List<KeyValuePair<string, int>> {
new KeyValuePair<string, int>( "Dan", 10 ),
new KeyValuePair<string, int>( "Bill", 12 ),
new KeyValuePair<string, int>( "Dan", 14 ),
new KeyValuePair<string, int>( "Dan", 20 ),
new KeyValuePair<string, int>( "John", 1 ),
new KeyValuePair<string, int>( "John", 2 ),
new KeyValuePair<string, int>( "Bill", 5 )
};
var dupeGroups = entries
.GroupConsecutive(entry => entry.Key);
foreach (var dupeGroup in dupeGroups) {
Console.WriteLine(
"Key: {0} Sum: {1}",
dupeGroup.Key.PadRight(5),
dupeGroup.Select(entry => entry.Value).Sum()
);
}
Выход:
Key: Dan Sum: 10
Key: Bill Sum: 12
Key: Dan Sum: 34
Key: John Sum: 3
Key: Bill Sum: 5
Обратите внимание, что это также устраняет проблему с моим первоначальным ответом на работу с IEnumerator<T>
объектами, которые были типами значений. (При таком подходе это не имеет значения.)
Там все еще будет проблема, если вы попробуете позвонить ToList
здесь, как вы узнаете, если вы попробуете. Но учитывая, что вы включили отложенное выполнение как требование , я сомневаюсь, что вы все равно это сделаете. Для foreach
это работает.
Оригинальное, грязное и несколько глупое решение
Что-то подсказывает мне, что меня полностью опровергнут за это, но ...
Да , это возможно (я думаю). Смотрите ниже для чертовски грязного решения, которое я бросил вместе. (Ловит исключение, чтобы знать, когда он закончится, поэтому вы знаете , это отличный дизайн!)
Теперь замечание Джона о том, что в том случае, если вы попытаетесь выполнить, например, ToList
, а затем получить доступ к значениям в результирующем списке по индексу, будет очень реальной проблемой, является полностью верным. Но если ваше намерение only заключается в том, чтобы иметь возможность зацикливаться на IEnumerable<T>
с использованием foreach
- и вы only делаете это в своем own код - тогда, я думаю, это может сработать для вас.
В любом случае, вот краткий пример того, как это работает:
var ints = new int[] { 1, 3, 3, 4, 4, 4, 5, 2, 3, 1, 6, 6, 6, 5, 7, 7, 8 };
var dupeGroups = ints.GroupConsecutiveDuplicates(EqualityComparer<int>.Default);
foreach (var dupeGroup in dupeGroups) {
Console.WriteLine(
"New dupe group: " +
string.Join(", ", dupeGroup.Select(i => i.ToString()).ToArray())
);
}
Выход:
New dupe group: 1
New dupe group: 3, 3
New dupe group: 4, 4, 4
New dupe group: 5
New dupe group: 2
New dupe group: 3
New dupe group: 1
New dupe group: 6, 6, 6
New dupe group: 5
New dupe group: 7, 7
New dupe group: 8
А теперь код (грязный как дерьмо):
Обратите внимание, что, поскольку этот подход требует передачи фактического перечислителя между несколькими различными методами, он не будет работать , если этот перечислитель является типом значения, как вызовы MoveNext
в один способ влияют только локальные копии.
public static IEnumerable<IEnumerable<T>> GroupConsecutiveDuplicates<T>(this IEnumerable<T> source, IEqualityComparer<T> comparer) {
using (var e = source.GetEnumerator()) {
if (e.GetType().IsValueType)
throw new ArgumentException(
"This method will not work on a value type enumerator."
);
// get the ball rolling
if (!e.MoveNext()) {
yield break;
}
IEnumerable<T> nextDuplicateGroup;
while (e.FindMoreDuplicates(comparer, out nextDuplicateGroup)) {
yield return nextDuplicateGroup;
}
}
}
private static bool FindMoreDuplicates<T>(this IEnumerator<T> enumerator, IEqualityComparer<T> comparer, out IEnumerable<T> duplicates) {
duplicates = enumerator.GetMoreDuplicates(comparer);
return duplicates != null;
}
private static IEnumerable<T> GetMoreDuplicates<T>(this IEnumerator<T> enumerator, IEqualityComparer<T> comparer) {
try {
if (enumerator.Current != null)
return enumerator.GetMoreDuplicatesInner(comparer);
else
return null;
} catch (InvalidOperationException) {
return null;
}
}
private static IEnumerable<T> GetMoreDuplicatesInner<T>(this IEnumerator<T> enumerator, IEqualityComparer<T> comparer) {
while (enumerator.Current != null) {
var current = enumerator.Current;
yield return current;
if (!enumerator.MoveNext())
break;
if (!comparer.Equals(current, enumerator.Current))
break;
}
}