Question

Contexte: C # 3.0, .Net 3.5
Supposons que j'ai un procédé qui génère des nombres aléatoires (toujours):

private static IEnumerable<int> RandomNumberGenerator() {
    while (true) yield return GenerateRandomNumber(0, 100);
}

Je dois regrouper ces chiffres en groupes de 10, donc je voudrais quelque chose comme:

foreach (IEnumerable<int> group in RandomNumberGenerator().Slice(10)) {
    Assert.That(group.Count() == 10);
}

J'ai défini la méthode tranche, mais je pense qu'il devrait y être un déjà défini. Voici ma méthode tranche, juste pour la référence:

    private static IEnumerable<T[]> Slice<T>(IEnumerable<T> enumerable, int size) {
        var result = new List<T>(size);
        foreach (var item in enumerable) {
            result.Add(item);
            if (result.Count == size) {
                yield return result.ToArray();
                result.Clear();
            }
        }
    }

Question: est-il un moyen plus facile d'accomplir ce que je suis en train de faire? Peut-être Linq?

Note:. Par exemple ci-dessus est une simplification, dans mon programme, j'ai un itérateur qui scans donnés matrice d'une manière non linéaire

EDIT: Pourquoi Skip + Take est pas bon.

En effet ce que je veux est:

var group1 = RandomNumberGenerator().Skip(0).Take(10);
var group2 = RandomNumberGenerator().Skip(10).Take(10);
var group3 = RandomNumberGenerator().Skip(20).Take(10);
var group4 = RandomNumberGenerator().Skip(30).Take(10);

sans passer au numéro de régénération (10 + 20 + 30 + 40) fois. Je besoin d'une solution qui va générer exactement 40 chiffres et briser ceux en 4 groupes de 10.

Était-ce utile?

La solution

J'ai fait quelque chose de similaire. Mais je voudrais qu'il soit plus simple:

//Remove "this" if you don't want it to be a extension method
public static IEnumerable<IList<T>> Chunks<T>(this IEnumerable<T> xs, int size)
{
    var curr = new List<T>(size);

    foreach (var x in xs)
    {
        curr.Add(x);

        if (curr.Count == size)
        {
            yield return curr;
            curr = new List<T>(size);
        }
    }
}

Je pense que le vôtre sont imparfaites. Vous revenez le même tableau pour tous vos gros morceaux / tranches afin que le dernier morceau / tranche que vous prenez aurait les données correctes.

Addition: Version tableau:

public static IEnumerable<T[]> Chunks<T>(this IEnumerable<T> xs, int size)
{
    var curr = new T[size];

    int i = 0;

    foreach (var x in xs)
    {
        curr[i % size] = x;

        if (++i % size == 0)
        {
            yield return curr;
            curr = new T[size];
        }
    }
}

Addition: Version Linq (pas C # 2.0). Comme en pointe, il ne fonctionnera pas sur les suites infinies et sera beaucoup plus lent que les alternatives:

public static IEnumerable<T[]> Chunks<T>(this IEnumerable<T> xs, int size)
{
    return xs.Select((x, i) => new { x, i })
             .GroupBy(xi => xi.i / size, xi => xi.x)
             .Select(g => g.ToArray());
}

Autres conseils

Passer et Prendre de toute utilisation pour vous?

Utilisez une combinaison des deux dans une boucle pour obtenir ce que vous voulez.

list.Skip(10).Take(10);

Saute les 10 premiers enregistrements et prend alors le 10 suivant.

Utilisation Skip et Take serait une très mauvaise idée . Appel Skip sur une collection indexée peut être très bien, mais l'appeler sur toute IEnumerable<T> arbitraire est susceptible d'entraîner une énumération sur le nombre d'éléments ignorés, ce qui signifie que si vous l'appelez, vous êtes à plusieurs reprises sur la séquence énumération un ordre de grandeur plus de fois que vous devez être .

plaignez de « l'optimisation prématurée » tout ce que vous voulez; mais qui est tout simplement ridicule.

Je pense que votre méthode de Slice est à peu près aussi bon qu'il obtient. Je voulais vous proposer une approche différente qui fournirait l'exécution différée et d'éviter l'attribution de tableau intermédiaire, mais qui est un jeu dangereux (par exemple, si vous essayez quelque chose comme ToList sur un tel résultat la mise en œuvre de IEnumerable<T>, sans énumérer sur les collections intérieures , vous finirez dans une boucle sans fin).

(je l'ai retiré ce qui était à l'origine ici, comme les améliorations de l'OP depuis l'affichage de la question ont depuis rendu mes suggestions ici redondantes.)

.

de voir Let si vous avez besoin même la complexité de tranche Si votre nombre aléatoire génère est apatride, je suppose que chaque appel à elle générer des nombres aléatoires uniques, donc peut-être que ce serait suffisant:

var group1 = RandomNumberGenerator().Take(10);  
var group2 = RandomNumberGenerator().Take(10);  
var group3 = RandomNumberGenerator().Take(10);  
var group4 = RandomNumberGenerator().Take(10);

Chaque appel à Take retourne un nouveau groupe de 10 numéros.

Maintenant, si votre se re-graines de générateur de nombres aléatoires avec une valeur spécifique à chaque fois qu'il est réitérée, cela ne fonctionnera pas. Vous aurez simplement obtenir les mêmes 10 valeurs pour chaque groupe. Ainsi, au lieu, vous devez utiliser:

var generator  = RandomNumberGenerator();
var group1     = generator.Take(10);  
var group2     = generator.Take(10);  
var group3     = generator.Take(10);  
var group4     = generator.Take(10);

Cela permet de maintenir une instance du générateur afin que vous puissiez continuer à récupérer des valeurs sans réensemencement le générateur.

Vous pouvez utiliser le Saut et Prendre méthodes avec un objet Enumerable.

Pour modifier:

Que diriez-vous d'une fonction qui prend un nombre de tranches et une taille de tranche comme paramètre?

private static IEnumerable<T> Slice<T>(IEnumerable<T> enumerable, int sliceSize, int sliceNumber) {
    return enumerable.Skip(sliceSize * sliceNumber).Take(sliceSize);
}

Il semble que nous préférerions un IEnumerable<T> d'avoir un compteur de position fixe afin que nous puissions faire

var group1 = items.Take(10);
var group2 = items.Take(10);
var group3 = items.Take(10);
var group4 = items.Take(10);

et obtenir des tranches successives plutôt que d'obtenir les 10 premiers éléments à chaque fois. Nous pouvons le faire avec une nouvelle mise en œuvre de IEnumerable<T> qui maintient une instance de son recenseur et le renvoie à chaque appel de GetEnumerator:

public class StickyEnumerable<T> : IEnumerable<T>, IDisposable
{
    private IEnumerator<T> innerEnumerator;

    public StickyEnumerable( IEnumerable<T> items )
    {
        innerEnumerator = items.GetEnumerator();
    }

    public IEnumerator<T> GetEnumerator()
    {
        return innerEnumerator;
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return innerEnumerator;
    }

    public void Dispose()
    {
        if (innerEnumerator != null)
        {
            innerEnumerator.Dispose();
        }
    }
}

Étant donné que la classe, nous avons pu mettre en œuvre tranche avec

public static IEnumerable<IEnumerable<T>> Slices<T>(this IEnumerable<T> items, int size)
{
    using (StickyEnumerable<T> sticky = new StickyEnumerable<T>(items))
    {
        IEnumerable<T> slice;
        do
        {
            slice = sticky.Take(size).ToList();
            yield return slice;
        } while (slice.Count() == size);
    }
    yield break;
}

qui fonctionne dans ce cas, mais StickyEnumerable<T> est généralement une classe dangereuse d'avoir autour si le code la consommation ne s'y attend pas. Par exemple,

using (var sticky = new StickyEnumerable<int>(Enumerable.Range(1, 10)))
{
    var first = sticky.Take(2);
    var second = sticky.Take(2);
    foreach (int i in second)
    {
        Console.WriteLine(i);
    }
    foreach (int i in first)
    {
        Console.WriteLine(i);
    }
}

impressions

1
2
3
4

plutôt que

3
4
1
2

Jetez un oeil à Take (), TakeWhile () et Skip ()

Je pense que l'utilisation de Slice() serait un peu trompeur. Je pense que comme un moyen de me donner un mandrin d'un tableau dans un nouveau tableau et ne pas causer des effets secondaires. Dans ce scénario vous fait déplacer le dénombrable avant 10.

Une meilleure approche possible est d'utiliser simplement l'extension LINQ Take(). Je ne pense pas que vous devez utiliser Skip() avec un générateur.

Modifier Dang, je suis en train de tester ce comportement avec le code suivant

Remarque:. c'est pas vraiment été correct, je laisse ici pour que les autres ne tombent pas dans la même erreur

var numbers = RandomNumberGenerator();
var slice = numbers.Take(10);

public static IEnumerable<int> RandomNumberGenerator()
{
    yield return random.Next();
}

mais le Count() pour slice est alway 1. J'ai aussi essayé de courir à travers une boucle de foreach car je sais que les extensions Linq sont généralement évaluées paresseusement et il ne bouclées qu'une seule fois. J'ai finalement fait le code ci-dessous au lieu du Take() et il fonctionne:

public static IEnumerable<int> Slice(this IEnumerable<int> enumerable, int size)
{
    var list = new List<int>();
    foreach (var count in Enumerable.Range(0, size)) list.Add(enumerable.First());
    return list;
}

Si vous remarquez j'ajoute le First() à la liste chaque fois, mais étant donné que le dénombrable qui est transmis est le générateur de RandomNumberGenerator() le résultat est différent à chaque fois.

Alors à nouveau avec un générateur à l'aide Skip() n'est pas nécessaire, car le résultat sera différent. Bouclez un IEnumerable est pas toujours effet côté libre.

Modifier Je vais laisser la dernière édition juste pour que personne ne tombe dans la même erreur, mais il a bien fonctionné pour moi faire ceci:

var numbers = RandomNumberGenerator();

var slice1 = numbers.Take(10);
var slice2 = numbers.Take(10);

Les deux tranches étaient différentes.

J'avais fait quelques erreurs dans ma première réponse, mais quelques-uns des points encore debout. Skip () et Take () ne vont pas travailler la même chose avec un générateur comme il le ferait une liste. Bouclez un IEnumerable est pas toujours sans effet secondaire. Quoi qu'il en soit voici mon avis sur obtenir une liste des tranches.

    public static IEnumerable<int> RandomNumberGenerator()
    {
        while(true) yield return random.Next();
    }

    public static IEnumerable<IEnumerable<int>> Slice(this IEnumerable<int> enumerable, int size, int count)
    {
        var slices = new List<List<int>>();
        foreach (var iteration in Enumerable.Range(0, count)){
            var list = new List<int>();
            list.AddRange(enumerable.Take(size));
            slices.Add(list);
        }
        return slices;
    }

Je suis cette solution pour le même problème:

int[] ints = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
IEnumerable<IEnumerable<int>> chunks = Chunk(ints, 2, t => t.Dump());
//won't enumerate, so won't do anything unless you force it:
chunks.ToList();

IEnumerable<T> Chunk<T, R>(IEnumerable<R> src, int n, Func<IEnumerable<R>, T> action){
  IEnumerable<R> head;
  IEnumerable<R> tail = src;
  while (tail.Any())
  {
    head = tail.Take(n);
    tail = tail.Skip(n);
    yield return action(head);
  }
}

si vous voulez juste les morceaux retournés, ne rien faire avec eux, l'utilisation chunks = Chunk(ints, 2, t => t). Ce que je voudrais vraiment est d'avoir t=>t comme action par défaut, mais je n'ai pas trouvé comment faire encore.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top