Perché questo uso di IEnumerable, List e Array sembra essere modificato quando non dovrebbe essere?

2

Ho iniziato a imparare c # e sono confuso dalla seguente funzionalità.

Il seguente codice usa una funzione Algs.Combinations (n, m) per produrre un oggetto IEnumerable che contiene le combinazioni di n oggetti presi da m. La sua implementazione è esattamente come si può trovare nella tabella codici di Rosetta per le combinazioni. Ad esempio, applicando .toList () a Algs.Combinations (2,3) produrrebbe la lista [[0,1], [0,2], [1,2]].

Ora nel ciclo foreach aggiungo ogni int [] a una nuova lista. Inoltre, stampo i valori di ciascun array (ovvero la combinazione) dopo averlo trasformato in un elenco e quindi utilizzando il ForEach integrato. Tutto funziona come previsto.

Nel prossimo blocco io uso il built-in ForEach nella mia nuova lista e sembra che ogni int [] sia diventato [2,2]. Che cosa sta succedendo qui? Presumibilmente nella mia foreach in realtà non sto aggiungendo l'array intero ma una sorta di puntatore?

using System;
using System.Collections.Generic;
using System.Linq;


namespace Examples
{
    class main
    {
        public static IEnumerable<int[]> Combinations(int m, int n)
        {
            int[] result = new int[m];
            Stack<int> stack = new Stack<int>();
            stack.Push(0);

            while (stack.Count > 0)
            {
                int index = stack.Count - 1;
                int value = stack.Pop();

                while (value < n)
                {
                    result[index++] = value++;
                    stack.Push(value);
                    if (index == m)
                    {
                        yield return result;
                        break;
                    }
                }
            }
        }

        static void Main(String[] args)
        {
            var l = Combinations(2, 3);
            var newL = new List<int[]>();
            foreach (int[] combs in l)
            {
                newL.Add(combs);
                combs.ToList().ForEach(p => Console.Write(p + " ")); // prints 0 1, 0 2, 1 2 
                Console.WriteLine();
            }
            newL.ForEach(p =>
            {
                p.ToList().ForEach(q => Console.Write(q + " ")); // prints 2 2, 2 2, 2 2
                Console.WriteLine();
            });
         } 

    }
 }

Google non ha aiutato finora. Saranno apprezzati tutti i collegamenti alla letteratura che dovrei esaminare.

    
posta George231086 05.08.2015 - 22:22
fonte

2 risposte

5

Il tuo problema è che stai restituendo una sola matrice dalla tua funzione Combinations , ma restituendola più volte con i valori modificati all'interno della matrice tra ogni iterazione.

I metodi che utilizzano yield return sono coroutine - in ogni foreach iterazione vengono eseguiti fino alla successiva istruzione yield che trovano e quindi escono, riprendendo da dove sono stati interrotti nella successiva iterazione. Nel tuo primo ciclo foreach stai prendendo il valore int[] enumerato e lo inserisci nella tua variabile newL - ma è sempre lo stesso valore int[] poiché Combinations crea sempre un solo array. La stampa del primo foreach funziona bene perché la coroutine viene messa in pausa in attesa dell'inizio della successiva iterazione.

Hai un paio di opzioni. L'opzione migliore è rendere il tuo Combinations un'implementazione sana, in quanto restituisce un nuovo oggetto per ogni istruzione yield return . Potrebbe sembrare qualcosa del genere:

    public static IEnumerable<int[]> Combinations(int m, int n)
    {
        while (weStillHaveMoreResultsToBuild)
        {
            int[] result = new int[m];

            while (value < n)
            {
                // fill result and break out of loop when done
            }

            yield return result;
        }
    }

Un'altra opzione sarebbe quella di non utilizzare affatto yield return e invece di costruire un elenco all'interno di Combinations e di restituirlo. Questo approccio potrebbe richiedere molta più memoria se i valori di n e m sono grandi.

Una terza opzione è creare una copia degli oggetti restituiti da Combinations e memorizzarli invece:

newL.Add(combs.ToArray()); // Calling ToArray() will make a copy

Questa è la mia opzione meno preferita, perché richiede che qualsiasi chiamante di Combinations sappia che lo stesso oggetto verrà restituito più volte ma modificato in ogni iterazione.

    
risposta data 05.08.2015 - 23:54
fonte
1

Questo non ha nulla con l'utilizzo di IEnumerable o dell'istruzione yield. Se riscrivi la tua funzione "combinazione" in questo modo

    public static List<int[]> Combinations(int m, int n)
    {
        int[] result = new int[m];
        List<int[]> resultList = new List<int[]>();
        Stack<int> stack = new Stack<int>();
        stack.Push(0);

        while (stack.Count > 0)
        {
            int index = stack.Count - 1;
            int value = stack.Pop();

            while (value < n)
            {
                result[index++] = value++;
                stack.Push(value);
                if (index == m)
                {
                    resultList.Add(result); // <-- note what you are adding here
                    break;
                }
            }
        }
        return result;
    }

affronterai lo stesso problema (beh quasi lo stesso, perché ora già il tuo primo ciclo for-each stamperà ripetutamente un paio di valori "2 2").

Questo perché ogni voce in resultList ottiene un riferimento allo stesso array result . E come @Erik ha scritto correttamente, la soluzione corretta qui è fare copie della matrice result , anche se lo farei all'interno della funzione combination , ad esempio modificando la riga yield nel codice originale come questo:

 yield return result.ToArray();

In questo modo, non è responsabilità del chiamante eseguire copie, eliminando il problema di cui parlava Erik.

    
risposta data 06.08.2015 - 10:11
fonte

Leggi altre domande sui tag