Mucho mejor que cumple todos los requisitos
OK, deseche mi solución anterior (la dejo abajo, solo para referencia). Este es un enfoque mucho mejor que se me ocurrió después de hacer mi publicación inicial.
Escriba una nueva clase que implemente IEnumerator<T>
y proporcione algunas propiedades adicionales: IsValid
y Previous
. Esto es todo lo que necesita para resolver todo el problema con tener que mantener el estado dentro de un bloque iterador usando yield
.
Así es como lo hice (bastante trivial, como se puede ver):
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
}
(me llamaron a este un ChipmunkEnumerator
porque mantener el valor anterior me recordó lo ardillas tienen bolsas en las mejillas donde guardan ¿Realmente importa? Deje de burlarse de mí.)
Ahora, utilizar esta clase en un método de extensión para proporcionar exactamente el comportamiento que desea no es tan difícil.
en cuenta que a continuación he definido GroupConsecutive
vuelven en realidad a un IEnumerable<IGrouping<TKey, T>>
por la sencilla razón de que, si estas se agrupan por la clave de todos modos, tiene sentido para devolver un IGrouping<TKey, T>
en lugar de sólo una IEnumerable<T>
. Como resultado, esto va a ayudarnos a salir adelante de todos modos ...
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)));
}
(Para poner en práctica estos métodos, escribí una sencilla clase que implementa Grouping<TKey, T>
IGrouping<TKey, T>
de la manera más sencilla posible. He omitido el código sólo para seguir avanzando ...)
OK, échale un vistazo. Creo que el siguiente ejemplo de código captura muy bien algo parecido al escenario más realista que describiste en tu pregunta actualizada.
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()
);
}
Salida:
Key: Dan Sum: 10
Key: Bill Sum: 12
Key: Dan Sum: 34
Key: John Sum: 3
Key: Bill Sum: 5
cuenta de esto también corrige el problema con mi respuesta original de tratar con IEnumerator<T>
objetos que eran los tipos de valor. (Con este enfoque, no importa.)
Todavía hay un problema si intenta llamar al ToList
aquí, ya que lo averiguará si lo intenta. Pero teniendo en cuenta que incluyó la ejecución diferida como un requisito , dudo que lo haría de todos modos. Para un foreach
, funciona.
solución original, sucio, y algo estúpida
Algo me dice que voy a ser totalmente refutada por decir esto, pero ...
Sí, es posible (Creo). Vea a continuación una solución desordenada que arrojé. (Detecta una excepción a conocer cuando esté terminado, por lo que sabe es un gran diseño!)
Ahora, el punto de Jon acerca de que hay un problema muy real en el caso de que se intenta hacer, por ejemplo, ToList
, y luego acceder a los valores en la lista resultante por índice, es totalmente válido. Pero si su única intención aquí es ser capaz de bucle sobre un IEnumerable<T>
utilizando un foreach
- y usted es solamente hacer esto en su propio código - entonces, bueno, creo que esto podría funcionar para usted .
De todos modos, aquí hay un rápido ejemplo de como funciona:
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())
);
}
Salida:
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
Y ahora para el (desordenada como basura) Código:
Nota que dado que este enfoque requiere pasar el enumerador real alrededor de una pocos métodos diferentes, no funcionarán si ese enumerador es un tipo de valor, ya que las llamadas a MoveNext
en un método solo afectan a una copia local.
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;
}
}
im asumiendo en su respuesta muestra que quiere decir { "b", "b", "b"} –
@ Josh: Buena captura - He arreglado el problema, gracias! –
En su ejemplo complicado, Sum debe repetir la colección por segunda vez. ¿De qué sirve restringir "Grupo" a una iteración si el código de llamada repetirá los mismos elementos nuevamente? –