2016-06-23 14 views
9

Recentemente, ho scritto questo codice senza pensarci molto:Rimuovere elemento dalla raccolta durante l'iterazione con forEach

myObject.myCollection.forEach { myObject.removeItem($0) } 

dove myObject.removeItem(_) rimuove un elemento da myObject.myCollection.

Guardando il codice ora, sono perplesso sul perché questo funziona anche - non dovrei ottenere un'eccezione lungo le linee di Collection was mutated while being enumerated? Lo stesso codice funziona anche quando si usa un normale ciclo for-in!

È questo comportamento previsto o sono "fortunato" che non si blocca?

+1

parola di avvertimento: 'removeItem (_ :)' è 'O (n)', come è 'foreach (_ :)'. Questa linea è 'O (n^2)' in totale. Potrebbe essere utile utilizzare l'aritmetica impostata. – Alexander

+0

Non è necessariamente 'O (n)' - gli insiemi sono 'O (1)'. Fornisco il metodo 'removeItem (_ :)' (non esiste un metodo di questo tipo su una raccolta rapida) e faccio alcune altre cose di rimozione. –

+1

Inoltre, questa raccolta contiene in genere 2-4 elementi, quindi ho preso consapevolmente il potenziale riscontro di runtime;) –

risposta

11

Questo è in effetti un comportamento previsto, ed è dovuto al fatto che uno Array in Swift (così come molte altre raccolte nella libreria standard) è un tipo di valore con semantica copy-on-write. Ciò significa che il suo buffer sottostante (che è memorizzato indirettamente) sarà copiato dopo essere stato mutato (e, come ottimizzazione, solo quando non è referenziato in modo univoco).

(Si noti che questa risposta precedentemente affermato che la copia sarebbe accaduto dopo essere passato al iteratore che, come @MartinR giustamente osservato, non è ciò che accade.)

Quando si arriva a iterare su una Sequence (come un array), che si tratti di forEach(_:) o di un ciclo standard for in, viene creato un iteratore dal metodo makeIterator() della sequenza e il metodo next() viene applicato ripetutamente per generare elementi in sequenza.

Si può pensare di iterazione su una sequenza come guardare in questo modo:

let sequence = [1, 2, 3, 4] 
var iterator = sequence.makeIterator() 

// next() will return the next element, or nil if it's reached the end of the sequence. 
while let element = iterator.next() { 
    // do something with the element 
} 

Nel caso di Array, un IndexingIterator viene utilizzato come è iteratore - che iterare attraverso gli elementi di un dato di raccolta, semplicemente memorizzando la raccolta insieme all'indice corrente dell'iterazione. Ogni volta che viene chiamato next(), la raccolta di base viene contrassegnata con l'indice, che viene poi incrementato fino a raggiungere endIndex (è possibile vedere il suo exact implementation here).

Pertanto, quando si arriva a mutare l'array nel ciclo, il buffer sottostante è non con riferimento univoco, poiché l'iteratore ha anche una vista su di esso. Ciò impone una copia del buffer, che quindi usa myCollection.

Quindi, ora ci sono due array: quello su cui viene ripetuto e quello che si sta modificando. Eventuali ulteriori mutazioni nel loop non attiveranno un'altra copia, purché il buffer di myCollection rimanga referenziato in modo univoco.

Ciò significa che è perfettamente sicuro mutazione di una raccolta con semantica del valore mentre si enumera su di essa. L'enumerazione verrà ripetuta per tutta la lunghezza della raccolta, completamente indipendente da tutte le mutazioni che esegui, dato che verranno eseguite su una copia.

+0

Grazie per la risposta! Fornisco da solo il metodo 'removeItem (_ :)' (non esiste un metodo simile per una raccolta rapida, e il nome è solo un segnaposto per rendere chiaro il mio punto), e faccio altre cose di rimozione per l'oggetto rimosso lì , quindi non posso semplicemente chiamare 'removeAll()' sulla collezione. –

+0

@David Ah okay, ha senso ora. Felice di aiutare :) – Hamish

+0

Ciao @DavidGanster, mi dispiace disturbarla per tanto tempo dopo aver postato la domanda. Solo per farti sapere che oggi ho fatto un bel montaggio per questa risposta, dato che non ero troppo contento della versione originale. Se hai tempo, non esitare a dare un'occhiata e controlla che tu sia ancora soddisfatto: sono felice di rispondere a qualsiasi domanda :) – Hamish

6

Ho chiesto uno similar question nel forum degli sviluppatori Apple e la risposta è "sì, a causa della semantica del valore dell'array".

@ originaluser2 ha detto che già, ma direi un po 'diversa: Quando myObject.removeItem($0) viene chiamato, un nuovo array viene creato e memorizzato sotto il nome myObject, ma la matrice che forEach() è stata chiamata non viene modificato.

Ecco un esempio più semplice che dimostra l'effetto:

extension Array { 
    func printMe() { 
     print(self) 
    } 
} 

var a = [1, 2, 3] 
let pm = a.printMe // The instance method as a closure. 
a.removeAll() // Modify the variable `a`. 
pm() // Calls the method on the value that it was created with. 
// Output: [1, 2, 3] 
+0

In effetti, questo è un esempio molto più chiaro del mio :) Il principio è lo stesso anche se - 'a.removeAll()' creerà una copia di 'a', rimuoverà tutti gli elementi, quindi assegnerà di nuovo a' a'. Il valore originale di 'a' non è influenzato e puoi mantenerlo mantenuto mantenendo un riferimento a uno dei suoi metodi di istanza e richiamarlo in seguito per dimostrarlo. – Hamish

+0

@ originaluser2: Sì, la differenza nell'argomento è marginale. Hai detto (se lo interpreto correttamente) che l'iteratore ottiene una copia dell'array. Dico che gli operatori iteratori sull'array originale e la mutazione che ne fa una copia. Ma come puoi vedere dalla mia domanda nel forum, mi ci è voluto un po 'per ottenerlo. –

+0

Sicuramente entrambi fanno una copia (teorica)? L'iteratore deve fare una copia poiché è inizializzata con l'array originale, che viene quindi assegnato a una proprietà interna (i tipi di valore indicano che ciò dovrebbe fare una copia) - e quindi la mutazione eseguirà sempre una copia, eseguirà la mutazione e quindi -assegnare. – Hamish

Problemi correlati