[英]Grouping consecutive identical items: IEnumerable<T> to IEnumerable<IEnumerable<T>>
我有一個有趣的問題:給定一個IEnumerable<string>
,是否有可能產生一個IEnumerable<IEnumerable<string>>
序列,它在一次傳遞中對相同的相鄰字符串進行分組?
讓我解釋。
1.基本說明性樣本:
考慮以下IEnumerable<string>
(偽表示):
{"a","b","b","b","c","c","d"}
如何獲得IEnumerable<IEnumerable<string>>
,它將產生以下形式:
{ // IEnumerable<IEnumerable<string>>
{"a"}, // IEnumerable<string>
{"b","b","b"}, // IEnumerable<string>
{"c","c"}, // IEnumerable<string>
{"d"} // IEnumerable<string>
}
方法原型將是:
public IEnumerable<IEnumerable<string>> Group(IEnumerable<string> items)
{
// todo
}
但它也可能是:
public void Group(IEnumerable<string> items, Action<IEnumerable<string>> action)
{
// todo
}
......每個子序列都會調用action
。
2.更復雜的樣本
好的,第一個樣本非常簡單,只是為了使高級意圖清晰。
現在假設我們正在處理IEnumerable<Anything>
,其中Anything
是這樣定義的類型:
public class Anything
{
public string Key {get;set;}
public double Value {get;set;}
}
現在,我們要基於密鑰的子序列,(組每個連續Anything
具有相同的密鑰),以便按組來計算總價值在以后使用它們:
public void Compute(IEnumerable<Anything> items)
{
Console.WriteLine(items.Sum(i=>i.Value));
}
// then somewhere, assuming the Group method
// that returns an IEnumerable<IEnumerable<Anything>> actually exists:
foreach(var subsequence in Group(allItems))
{
Compute(subsequence);
}
3.重要說明
它有可能嗎,你會怎么寫呢?
這是你想要的?
此解決方案依賴於對象狀態,因為很難在使用yield(無ref或out params)的兩個IEnumerable方法之間共享狀態。
internal class Program
{
static void Main(string[] args)
{
var result = new[] { "a", "b", "b", "b", "c", "c", "d" }.Partition();
foreach (var r in result)
{
Console.WriteLine("Group".PadRight(16, '='));
foreach (var s in r)
Console.WriteLine(s);
}
}
}
internal static class PartitionExtension
{
public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> src)
{
var grouper = new DuplicateGrouper<T>();
return grouper.GroupByDuplicate(src);
}
}
internal class DuplicateGrouper<T>
{
T CurrentKey;
IEnumerator<T> Itr;
bool More;
public IEnumerable<IEnumerable<T>> GroupByDuplicate(IEnumerable<T> src)
{
using(Itr = src.GetEnumerator())
{
More = Itr.MoveNext();
while (More)
yield return GetDuplicates();
}
}
IEnumerable<T> GetDuplicates()
{
CurrentKey = Itr.Current;
while (More && CurrentKey.Equals(Itr.Current))
{
yield return Itr.Current;
More = Itr.MoveNext();
}
}
}
編輯:添加了清潔用法的擴展方法。 固定循環測試邏輯,以便首先評估“更多”。
編輯:完成后處理枚舉器
好的,廢棄我之前的解決方案(我將其留在下面,僅供參考)。 這是我初次發布后發生的更好的方法。
編寫一個實現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
,它有效。
有些東西告訴我,我會因為這樣說完全被駁斥,但......
是的 ,有可能(我認為)。 請看下面我扔的一個該死的混亂解決方案。 (抓住一個例外,知道什么時候結束,所以你知道這是一個很棒的設計!)
現在,Jon的觀點是,如果您嘗試執行事件,例如ToList
,然后通過索引訪問結果列表中的值,則完全有效。 但是如果你在這里的唯一目的是能夠使用foreach
IEnumerable<T>
- 並且你只是在自己的代碼中執行此操作 - 那么,我認為這可能適合你。
無論如何,這是一個如何工作的快速示例:
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;
}
}
你的第二顆子彈是有問題的。 原因如下:
var groups = CallMagicGetGroupsMethod().ToList();
foreach (string x in groups[3])
{
...
}
foreach (string x in groups[0])
{
...
}
在這里,它試圖迭代第四組,然后是第一組...如果所有組都被緩沖或者它可以重新讀取序列,那么這顯然只會起作用,這兩者都不是理想的。
我懷疑你想要一個更“反應”的方法 - 我不知道Reactive Extensions是否做你想要的(“連續”要求是不尋常的)但你基本上應該提供某種行動來對每個組執行..這樣一來,這個方法就不用擔心必須在你讀完之后再給你一些可以用的東西了。
如果您希望我嘗試在Rx中找到解決方案,或者您是否對以下內容感到滿意,請告訴我們:
void GroupConsecutive(IEnumerable<string> items,
Action<IEnumerable<string>> action)
這是一個我認為滿足您的要求的解決方案,適用於任何類型的數據項,並且非常簡短和可讀:
public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> list)
{
var current = list.FirstOrDefault();
while (!Equals(current, default(T))) {
var cur = current;
Func<T, bool> equalsCurrent = item => item.Equals(cur);
yield return list.TakeWhile(equalsCurrent);
list = list.SkipWhile(equalsCurrent);
current = list.FirstOrDefault();
}
}
筆記:
TakeWhile
和SkipWhile
都這樣做)。 SkipWhile
); 當您處理返回的IEnumerables時,它會再次迭代集合,但分區本身只迭代一次。 while
條件更改為null
測試。 如果我在某種程度上錯了,我會特別感興趣的是指出錯誤的評論!
非常重要的旁邊:
該解決方案將不允許枚舉產生可枚舉比它提供他們在一個以外的任何命令。不過,我認為原來的海報已在意見很清楚,這是沒有問題的。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.