Birisi, linq'te belirli bir boyutta partiler oluşturmanın bir yolunu önerebilir mi?
İdeal olarak, bazı yapılandırılabilir miktardaki parçalar halinde işlemler gerçekleştirebilmek istiyorum.
Yanıtlar:
Herhangi bir kod yazmanıza gerek yok. Kaynak dizisini boyutlu paketler halinde gruplayan MoreLINQ Batch yöntemini kullanın (MoreLINQ, yükleyebileceğiniz bir NuGet paketi olarak mevcuttur):
int size = 10;
var batches = sequence.Batch(size);
Hangisi şu şekilde uygulanır:
public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(
this IEnumerable<TSource> source, int size)
{
TSource[] bucket = null;
var count = 0;
foreach (var item in source)
{
if (bucket == null)
bucket = new TSource[size];
bucket[count++] = item;
if (count != size)
continue;
yield return bucket;
bucket = null;
count = 0;
}
if (bucket != null && count > 0)
yield return bucket.Take(count).ToArray();
}
Batch(new int[] { 1, 2 }, 1000000)
public static class MyExtensions
{
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> items,
int maxItems)
{
return items.Select((item, inx) => new { item, inx })
.GroupBy(x => x.inx / maxItems)
.Select(g => g.Select(x => x.item));
}
}
ve kullanım şöyle olacaktır:
List<int> list = new List<int>() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
foreach(var batch in list.Batch(3))
{
Console.WriteLine(String.Join(",",batch));
}
ÇIKTI:
0,1,2
3,4,5
6,7,8
9
GroupBy
başladığında numaralandırma, tam kaynağını numaralandırmak zorunda değildir? Bu, kaynağın tembel değerlendirmesini ve dolayısıyla bazı durumlarda, gruplamanın tüm faydalarını kaybeder!
Bir sequence
olarak tanımlanmış olarak başlarsanız IEnumerable<T>
ve birden çok kez güvenli bir şekilde numaralandırılabileceğini biliyorsanız (örneğin, bir dizi veya bir liste olduğu için), öğeleri toplu işlerde işlemek için bu basit modeli kullanabilirsiniz:
while (sequence.Any())
{
var batch = sequence.Take(10);
sequence = sequence.Skip(10);
// do whatever you need to do with each batch here
}
Yukarıdakilerin tümü, büyük gruplar veya düşük bellek alanıyla korkunç performans gösterir. Boru hattını kendim yazmam gerekiyordu (hiçbir yerde öğe birikimine dikkat edin):
public static class BatchLinq {
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size) {
if (size <= 0)
throw new ArgumentOutOfRangeException("size", "Must be greater than zero.");
using (IEnumerator<T> enumerator = source.GetEnumerator())
while (enumerator.MoveNext())
yield return TakeIEnumerator(enumerator, size);
}
private static IEnumerable<T> TakeIEnumerator<T>(IEnumerator<T> source, int size) {
int i = 0;
do
yield return source.Current;
while (++i < size && source.MoveNext());
}
}
Düzenleme: Bu yaklaşımla ilgili bilinen sorun, her partinin bir sonraki partiye geçmeden önce tam olarak numaralandırılması ve numaralandırılması gerektiğidir. Örneğin bu çalışmıyor:
//Select first item of every 100 items
Batch(list, 100).Select(b => b.First())
Bu tamamen tembel, düşük ek yük, Batch'in herhangi bir biriktirme yapmayan tek işlevli uygulamasıdır. Nick Whaley'nin çözümüne dayanır (ve sorunları giderir) EricRoller yardımıyla.
Yineleme doğrudan temeldeki IEnumerable'dan gelir, bu nedenle öğeler kesin sırayla numaralandırılmalı ve bir kereden fazla erişilmemelidir. Bazı öğeler bir iç döngüde tüketilmezse, atılırlar (ve kaydedilmiş bir yineleyici aracılığıyla bunlara yeniden erişmeye çalışmak,InvalidOperationException: Enumeration already finished.
).
NET Fiddle'da eksiksiz bir örneği test edebilirsiniz .
public static class BatchLinq
{
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size)
{
if (size <= 0)
throw new ArgumentOutOfRangeException("size", "Must be greater than zero.");
using (var enumerator = source.GetEnumerator())
while (enumerator.MoveNext())
{
int i = 0;
// Batch is a local function closing over `i` and `enumerator` that
// executes the inner batch enumeration
IEnumerable<T> Batch()
{
do yield return enumerator.Current;
while (++i < size && enumerator.MoveNext());
}
yield return Batch();
while (++i < size && enumerator.MoveNext()); // discard skipped items
}
}
}
done
zaman e.Count()
sonra arayarak çeki ortadan kaldırabilirsiniz yield return e
. Tanımlanmamış davranışı source.Current
if çağırmamak için BatchInner'daki döngüyü yeniden düzenlemeniz gerekir i >= size
. Bu, BatchInner
her parti için yeni bir ayırma ihtiyacını ortadan kaldıracaktır .
i
bu nedenle bu, ayrı bir sınıf tanımlamaktan daha verimli olmayabilir, ancak biraz daha temiz olduğunu düşünüyorum.
Merak ediyorum, neden hiç kimse eski tarz bir for-loop çözümü yayınlamadı. İşte burada:
List<int> source = Enumerable.Range(1,23).ToList();
int batchsize = 10;
for (int i = 0; i < source.Count; i+= batchsize)
{
var batch = source.Skip(i).Take(batchsize);
}
Bu basitlik mümkündür çünkü Take yöntemi:
...
source
öğeler verilinceyecount
veyasource
başka öğe içermeyene kadar öğeleri numaralandırır ve verir . Eğercount
eleman sayısını aşarsasource
, tüm unsurlarısource
döndürülür
Feragatname:
Döngünün içinde Atla ve Al seçeneğinin kullanılması, numaralandırılabilirin birden çok kez numaralandırılacağı anlamına gelir. Numaralandırılırsa bu tehlikelidir. Bir veritabanı sorgusunun veya bir web isteğinin veya bir dosyanın okunmasının birden çok yürütülmesine neden olabilir. Bu örnek, açık bir şekilde ertelenmemiş bir Listenin kullanımı içindir, bu nedenle daha az sorun teşkil eder. Skip, her çağrıldığında koleksiyonu numaralandıracağından, hala yavaş bir çözümdür.
Bu, GetRange
yöntem kullanılarak da çözülebilir , ancak olası bir dinlenme grubunu çıkarmak için ekstra bir hesaplama gerektirir:
for (int i = 0; i < source.Count; i += batchsize)
{
int remaining = source.Count - i;
var batch = remaining > batchsize ? source.GetRange(i, batchsize) : source.GetRange(i, remaining);
}
İşte bunu halletmenin 2 döngü ile çalışan üçüncü bir yolu. Bu, koleksiyonun yalnızca 1 kez numaralandırılmasını sağlar !:
int batchsize = 10;
List<int> batch = new List<int>(batchsize);
for (int i = 0; i < source.Count; i += batchsize)
{
// calculated the remaining items to avoid an OutOfRangeException
batchsize = source.Count - i > batchsize ? batchsize : source.Count - i;
for (int j = i; j < i + batchsize; j++)
{
batch.Add(source[j]);
}
batch.Clear();
}
Skip
ve Take
enumerable birden çok kez numaralandırılan döngü vasıtası iç. Numaralandırılabilir ertelenmişse bu tehlikelidir. Bir veritabanı sorgusunun veya bir web isteğinin veya bir dosyanın okunmasının birden çok yürütülmesine neden olabilir. Örnekte, bir var List
bir sorun daha azdır bu nedenle, ertelenmiş edilememesinden kaynaklanmaktadır.
MoreLINQ ile aynı yaklaşım, ancak Array yerine List'i kullanıyor. Kıyaslama yapmadım, ancak okunabilirlik bazı insanlar için daha önemli:
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size)
{
List<T> batch = new List<T>();
foreach (var item in source)
{
batch.Add(item);
if (batch.Count >= size)
{
yield return batch;
batch.Clear();
}
}
if (batch.Count > 0)
{
yield return batch;
}
}
size
parametreyi sizin new List
hesabınıza iletin.
batch.Clear();
ilebatch = new List<T>();
İşte Nick Whaley'nin ( bağlantı ) ve infogulch'un ( bağlantı ) tembel Batch
uygulamalarının iyileştirilmesi denenmiştir . Bu katı. Ya partileri doğru sırada numaralandırırsınız ya da bir istisna alırsınız.
public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(
this IEnumerable<TSource> source, int size)
{
if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size));
using (var enumerator = source.GetEnumerator())
{
int i = 0;
while (enumerator.MoveNext())
{
if (i % size != 0) throw new InvalidOperationException(
"The enumeration is out of order.");
i++;
yield return GetBatch();
}
IEnumerable<TSource> GetBatch()
{
while (true)
{
yield return enumerator.Current;
if (i % size == 0 || !enumerator.MoveNext()) break;
i++;
}
}
}
}
Ve burada Batch
tür kaynakları için tembel bir uygulama IList<T>
. Bu, numaralandırmaya herhangi bir kısıtlama getirmez. Partiler kısmen, herhangi bir sırayla ve birden fazla numaralandırılabilir. Numaralandırma sırasında koleksiyonu değiştirmeme kısıtlaması yine de yürürlüktedir. Bu, enumerator.MoveNext()
herhangi bir parça veya öğe vermeden önce sahte bir çağrı yaparak elde edilir . Olumsuz yanı, numaralandırmanın ne zaman biteceği bilinmediğinden, numaralandırıcının elden çıkarılmamış olmasıdır.
public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(
this IList<TSource> source, int size)
{
if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size));
var enumerator = source.GetEnumerator();
for (int i = 0; i < source.Count; i += size)
{
enumerator.MoveNext();
yield return GetChunk(i, Math.Min(i + size, source.Count));
}
IEnumerable<TSource> GetChunk(int from, int toExclusive)
{
for (int j = from; j < toExclusive; j++)
{
enumerator.MoveNext();
yield return source[j];
}
}
}
Yani işlevsel bir şapka takılıyken, bu önemsiz görünüyor ... ama C # 'da bazı önemli dezavantajlar var.
Muhtemelen bunu IEnumerable'ın bir açılımı olarak görürsünüz (google it ve muhtemelen bazı Haskell belgelerinde yer alacaksınız, ancak F # biliyorsanız, Haskell belgelerinde şaşkınlık varsa, açılmayı kullanan bazı F # maddeleri olabilir ve anlamda).
Unfold, IEnumerable girdisi üzerinden yinelemekten ziyade katlama ("aggregate") ile ilgilidir, çıktı veri yapılarını yineler (IEnumerable ve IObservable arasında benzer bir ilişki, aslında IObservable'ın create adında bir "açılma" uyguladığını düşünüyorum. ..)
her neyse, önce bir açılma yöntemine ihtiyacınız var, bence bu işe yarıyor (maalesef eninde sonunda büyük "listeler" için yığını patlatacak ... bunu concat yerine verim! kullanarak güvenle yazabilirsiniz);
static IEnumerable<T> Unfold<T, U>(Func<U, IEnumerable<Tuple<U, T>>> f, U seed)
{
var maybeNewSeedAndElement = f(seed);
return maybeNewSeedAndElement.SelectMany(x => new[] { x.Item2 }.Concat(Unfold(f, x.Item1)));
}
bu biraz abartılı çünkü C # işlevsel dillerin kabul ettiği bazı şeyleri uygulamıyor ... ama temelde bir tohum alır ve sonra IEnumerable ve sonraki tohumda bir sonraki öğenin "Belki" cevabını üretir (Belki C # 'da mevcut değil, bu yüzden onu taklit etmek için IEnumerable kullandık) ve cevabın geri kalanını birleştirdik (bunun "O (n?)" karmaşıklığına kefil olamam).
Bunu yaptıktan sonra;
static IEnumerable<IEnumerable<T>> Batch<T>(IEnumerable<T> xs, int n)
{
return Unfold(ys =>
{
var head = ys.Take(n);
var tail = ys.Skip(n);
return head.Take(1).Select(_ => Tuple.Create(tail, head));
},
xs);
}
hepsi oldukça temiz görünüyor ... IEnumerable'da "n" elemanlarını "sonraki" eleman olarak alırsınız ve "kuyruk" işlenmemiş listenin geri kalanıdır.
kafada hiçbir şey yoksa ... bittiniz ... "Hiçbir şey" döndürürsünüz (ancak boş bir IEnumerable> olarak takılıyorsunuz) ... aksi takdirde baş öğesini ve kuyruğu işlemek için döndürürsünüz.
Bunu muhtemelen IObservable kullanarak yapabilirsiniz, muhtemelen zaten orada "Toplu İş" benzeri bir yöntem vardır ve muhtemelen bunu kullanabilirsiniz.
Yığın taşması riski endişeleniyorsa (muhtemelen gerekir), o zaman F # uygulamalısınız (ve muhtemelen bununla birlikte bir F # kitaplığı (FSharpX?) Vardır).
(Bununla ilgili yalnızca bazı temel testler yaptım, bu yüzden orada tuhaf hatalar olabilir).
Çok geç katılıyorum ama daha ilginç bir şey buldum.
Yani burada Skip
ve Take
daha iyi performans için kullanabiliriz .
public static class MyExtensions
{
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> items, int maxItems)
{
return items.Select((item, index) => new { item, index })
.GroupBy(x => x.index / maxItems)
.Select(g => g.Select(x => x.item));
}
public static IEnumerable<T> Batch2<T>(this IEnumerable<T> items, int skip, int take)
{
return items.Skip(skip).Take(take);
}
}
Sonra 100000 kayıtla kontrol ettim. Döngü yalnızca şu durumlarda daha fazla zaman alıyorBatch
Konsol uygulamasının kodu.
static void Main(string[] args)
{
List<string> Ids = GetData("First");
List<string> Ids2 = GetData("tsriF");
Stopwatch FirstWatch = new Stopwatch();
FirstWatch.Start();
foreach (var batch in Ids2.Batch(5000))
{
// Console.WriteLine("Batch Ouput:= " + string.Join(",", batch));
}
FirstWatch.Stop();
Console.WriteLine("Done Processing time taken:= "+ FirstWatch.Elapsed.ToString());
Stopwatch Second = new Stopwatch();
Second.Start();
int Length = Ids2.Count;
int StartIndex = 0;
int BatchSize = 5000;
while (Length > 0)
{
var SecBatch = Ids2.Batch2(StartIndex, BatchSize);
// Console.WriteLine("Second Batch Ouput:= " + string.Join(",", SecBatch));
Length = Length - BatchSize;
StartIndex += BatchSize;
}
Second.Stop();
Console.WriteLine("Done Processing time taken Second:= " + Second.Elapsed.ToString());
Console.ReadKey();
}
static List<string> GetData(string name)
{
List<string> Data = new List<string>();
for (int i = 0; i < 100000; i++)
{
Data.Add(string.Format("{0} {1}", name, i.ToString()));
}
return Data;
}
Geçen zaman böyledir.
İlk - 00: 00: 00.0708, 00: 00: 00.0660
İkinci (Birini Al ve Atla) - 00: 00: 00.0008, 00: 00: 00.0008
GroupBy
tek bir satır oluşturmadan önce tam olarak numaralandırır. Bu, gruplama yapmanın iyi bir yolu değildir.
foreach (var batch in Ids2.Batch(5000))
için var gourpBatch = Ids2.Batch(5000)
ve zamanlanmış sonuçlarını kontrol edin. veya listeye ekle, var SecBatch = Ids2.Batch2(StartIndex, BatchSize);
eğer zamanlama sonuçlarınız değişirse ilgilenirim.
Linq olmadan çalışan ve veriler üzerinde tek bir numaralandırmayı garanti eden özel bir IEnumerable uygulaması yazdım. Ayrıca tüm bunları, büyük veri kümelerinde bellek patlamalarına neden olan yedekleme listeleri veya dizileri gerektirmeden gerçekleştirir.
İşte bazı temel testler:
[Fact]
public void ShouldPartition()
{
var ints = new List<int> {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
var data = ints.PartitionByMaxGroupSize(3);
data.Count().Should().Be(4);
data.Skip(0).First().Count().Should().Be(3);
data.Skip(0).First().ToList()[0].Should().Be(0);
data.Skip(0).First().ToList()[1].Should().Be(1);
data.Skip(0).First().ToList()[2].Should().Be(2);
data.Skip(1).First().Count().Should().Be(3);
data.Skip(1).First().ToList()[0].Should().Be(3);
data.Skip(1).First().ToList()[1].Should().Be(4);
data.Skip(1).First().ToList()[2].Should().Be(5);
data.Skip(2).First().Count().Should().Be(3);
data.Skip(2).First().ToList()[0].Should().Be(6);
data.Skip(2).First().ToList()[1].Should().Be(7);
data.Skip(2).First().ToList()[2].Should().Be(8);
data.Skip(3).First().Count().Should().Be(1);
data.Skip(3).First().ToList()[0].Should().Be(9);
}
Verileri bölümlemek için Uzantı Yöntemi.
/// <summary>
/// A set of extension methods for <see cref="IEnumerable{T}"/>.
/// </summary>
public static class EnumerableExtender
{
/// <summary>
/// Splits an enumerable into chucks, by a maximum group size.
/// </summary>
/// <param name="source">The source to split</param>
/// <param name="maxSize">The maximum number of items per group.</param>
/// <typeparam name="T">The type of item to split</typeparam>
/// <returns>A list of lists of the original items.</returns>
public static IEnumerable<IEnumerable<T>> PartitionByMaxGroupSize<T>(this IEnumerable<T> source, int maxSize)
{
return new SplittingEnumerable<T>(source, maxSize);
}
}
Bu uygulama sınıfıdır
using System.Collections;
using System.Collections.Generic;
internal class SplittingEnumerable<T> : IEnumerable<IEnumerable<T>>
{
private readonly IEnumerable<T> backing;
private readonly int maxSize;
private bool hasCurrent;
private T lastItem;
public SplittingEnumerable(IEnumerable<T> backing, int maxSize)
{
this.backing = backing;
this.maxSize = maxSize;
}
public IEnumerator<IEnumerable<T>> GetEnumerator()
{
return new Enumerator(this, this.backing.GetEnumerator());
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
private class Enumerator : IEnumerator<IEnumerable<T>>
{
private readonly SplittingEnumerable<T> parent;
private readonly IEnumerator<T> backingEnumerator;
private NextEnumerable current;
public Enumerator(SplittingEnumerable<T> parent, IEnumerator<T> backingEnumerator)
{
this.parent = parent;
this.backingEnumerator = backingEnumerator;
this.parent.hasCurrent = this.backingEnumerator.MoveNext();
if (this.parent.hasCurrent)
{
this.parent.lastItem = this.backingEnumerator.Current;
}
}
public bool MoveNext()
{
if (this.current == null)
{
this.current = new NextEnumerable(this.parent, this.backingEnumerator);
return true;
}
else
{
if (!this.current.IsComplete)
{
using (var enumerator = this.current.GetEnumerator())
{
while (enumerator.MoveNext())
{
}
}
}
}
if (!this.parent.hasCurrent)
{
return false;
}
this.current = new NextEnumerable(this.parent, this.backingEnumerator);
return true;
}
public void Reset()
{
throw new System.NotImplementedException();
}
public IEnumerable<T> Current
{
get { return this.current; }
}
object IEnumerator.Current
{
get { return this.Current; }
}
public void Dispose()
{
}
}
private class NextEnumerable : IEnumerable<T>
{
private readonly SplittingEnumerable<T> splitter;
private readonly IEnumerator<T> backingEnumerator;
private int currentSize;
public NextEnumerable(SplittingEnumerable<T> splitter, IEnumerator<T> backingEnumerator)
{
this.splitter = splitter;
this.backingEnumerator = backingEnumerator;
}
public bool IsComplete { get; private set; }
public IEnumerator<T> GetEnumerator()
{
return new NextEnumerator(this.splitter, this, this.backingEnumerator);
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
private class NextEnumerator : IEnumerator<T>
{
private readonly SplittingEnumerable<T> splitter;
private readonly NextEnumerable parent;
private readonly IEnumerator<T> enumerator;
private T currentItem;
public NextEnumerator(SplittingEnumerable<T> splitter, NextEnumerable parent, IEnumerator<T> enumerator)
{
this.splitter = splitter;
this.parent = parent;
this.enumerator = enumerator;
}
public bool MoveNext()
{
this.parent.currentSize += 1;
this.currentItem = this.splitter.lastItem;
var hasCcurent = this.splitter.hasCurrent;
this.parent.IsComplete = this.parent.currentSize > this.splitter.maxSize;
if (this.parent.IsComplete)
{
return false;
}
if (hasCcurent)
{
var result = this.enumerator.MoveNext();
this.splitter.lastItem = this.enumerator.Current;
this.splitter.hasCurrent = result;
}
return hasCcurent;
}
public void Reset()
{
throw new System.NotImplementedException();
}
public T Current
{
get { return this.currentItem; }
}
object IEnumerator.Current
{
get { return this.Current; }
}
public void Dispose()
{
}
}
}
}
Sadece başka bir tek satırlık uygulama. Boş bir listeyle bile çalışır, bu durumda sıfır boyutlu bir toplu iş koleksiyonu elde edersiniz.
var aList = Enumerable.Range(1, 100).ToList(); //a given list
var size = 9; //the wanted batch size
//number of batches are: (aList.Count() + size - 1) / size;
var batches = Enumerable.Range(0, (aList.Count() + size - 1) / size).Select(i => aList.GetRange( i * size, Math.Min(size, aList.Count() - i * size)));
Assert.True(batches.Count() == 12);
Assert.AreEqual(batches.ToList().ElementAt(0), new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9 });
Assert.AreEqual(batches.ToList().ElementAt(1), new List<int>() { 10, 11, 12, 13, 14, 15, 16, 17, 18 });
Assert.AreEqual(batches.ToList().ElementAt(11), new List<int>() { 100 });
Herkesin bu işi yapmak için karmaşık sistemler kullandığını biliyorum ve nedenini gerçekten anlamıyorum. Al ve atla, Func<TSource,Int32,TResult>
dönüştürme işlevi ile ortak seçimi kullanan tüm bu işlemlere izin verecektir . Sevmek:
public IEnumerable<IEnumerable<T>> Buffer<T>(IEnumerable<T> source, int size)=>
source.Select((item, index) => source.Skip(size * index).Take(size)).TakeWhile(bucket => bucket.Any());
source
çok sık yineleneceği için bu çok verimsiz olabilir .
Enumerable.Range(0, 1).SelectMany(_ => Enumerable.Range(0, new Random().Next()))
.
Başka bir yol da Rx Buffer operatörünü kullanmaktır
//using System.Linq;
//using System.Reactive.Linq;
//using System.Reactive.Threading.Tasks;
var observableBatches = anAnumerable.ToObservable().Buffer(size);
var batches = aList.ToObservable().Buffer(size).ToList().ToTask().GetAwaiter().GetResult();
GetAwaiter().GetResult()
. Bu, zaman uyumsuz kodu zorla çağıran eşzamanlı kod için bir kod kokusudur.
static IEnumerable<IEnumerable<T>> TakeBatch<T>(IEnumerable<T> ts,int batchSize)
{
return from @group in ts.Select((x, i) => new { x, i }).ToLookup(xi => xi.i / batchSize)
select @group.Select(xi => xi.x);
}