¿Eliminar de la matriz durante la enumeración en Swift?

86

Quiero enumerar a través de una matriz en Swift y eliminar ciertos elementos. Me pregunto si es seguro hacerlo y, si no, cómo se supone que debo lograrlo.

Actualmente, estaría haciendo esto:

for (index, aString: String) in enumerate(array) {
    //Some of the strings...
    array.removeAtIndex(index)
}
Andrés
fuente

Respuestas:

72

En Swift 2 esto es bastante fácil de usar enumeratey reverse.

var a = [1,2,3,4,5,6]
for (i,num) in a.enumerate().reverse() {
    a.removeAtIndex(i)
}
print(a)
Johnston
fuente
1
Funciona, pero el filtro es realmente el camino a seguir
13
@Mayerz Falso. "Quiero enumerar a través de una matriz en Swift y eliminar ciertos elementos". filterdevuelve una nueva matriz. No está eliminando nada de la matriz. Ni siquiera llamaría filteruna enumeración. Siempre hay más de una forma de despellejar a un gato.
Johnston
6
derecho, mi mal! Pla no haga la piel a los gatos
56

Puede considerar la filterforma:

var theStrings = ["foo", "bar", "zxy"]

// Filter only strings that begins with "b"
theStrings = theStrings.filter { $0.hasPrefix("b") }

El parámetro de filteres solo un cierre que toma una instancia de tipo de matriz (en este caso String) y devuelve un Bool. Cuando el resultado es true, mantiene el elemento; de lo contrario, el elemento se filtra.

Matteo Piombo
fuente
16
Haría explícito que filterno actualiza la matriz, solo devuelve una nueva
Antonio
Deben eliminarse los paréntesis; eso es un cierre final.
Jessy
@ Antonio tienes razón. De hecho, es por eso que lo publiqué como una solución más segura. Se podría considerar una solución diferente para matrices enormes.
Matteo Piombo
Hm, como dices, esto devuelve una nueva matriz. ¿Es posible convertir el filtermétodo en mutatinguno entonces (como he leído, la mutatingpalabra clave permite que funciones como esta se modifiquen self)?
Gee.E
@ Gee.E ciertamente puede agregar un filtro en el lugar como una extensión al Arraymarcarlo como mutatingy similar al código de la pregunta. De todos modos, considere que esto no siempre puede ser una ventaja. De todos modos, cada vez que elimina un objeto, su matriz puede reorganizarse en la memoria. Por lo tanto, podría ser más eficiente asignar una nueva matriz y luego realizar una sustitución atómica con el resultado de la función de filtro. El compilador podría hacer aún más optimizaciones, dependiendo de su código.
Matteo Piombo
38

En Swift 3 y 4 , esto sería:

Con números, según la respuesta de Johnston:

var a = [1,2,3,4,5,6]
for (i,num) in a.enumerated().reversed() {
   a.remove(at: i)
}
print(a)

Con cadenas como la pregunta del OP:

var b = ["a", "b", "c", "d", "e", "f"]

for (i,str) in b.enumerated().reversed()
{
    if str == "c"
    {
        b.remove(at: i)
    }
}
print(b)

Sin embargo, ahora en Swift 4.2 o posterior, hay una forma mejor y más rápida que fue recomendada por Apple en WWDC2018:

var c = ["a", "b", "c", "d", "e", "f"]
c.removeAll(where: {$0 == "c"})
print(c)

Esta nueva forma tiene varias ventajas:

  1. Es más rápido que las implementaciones con filter.
  2. Elimina la necesidad de invertir matrices.
  3. Elimina elementos en el lugar y, por lo tanto, actualiza la matriz original en lugar de asignar y devolver una nueva matriz.
jvarela
fuente
¿Qué pasa si el elemento es un objeto y necesito verificarlo, {$0 === Class.self}no funciona?
TomSawyer
14

Cuando un elemento en un índice determinado se elimina de una matriz, todos los elementos posteriores tendrán su posición (e índice) cambiados, porque retroceden una posición.

Entonces, la mejor manera es navegar por la matriz en orden inverso, y en este caso sugiero usar un bucle for tradicional:

for var index = array.count - 1; index >= 0; --index {
    if condition {
        array.removeAtIndex(index)
    }
}

Sin embargo, en mi opinión, el mejor enfoque es utilizar el filtermétodo, como lo describe @perlfly en su respuesta.

Antonio
fuente
pero desafortunadamente se ha eliminado en
Swift
4

No, no es seguro mutar matrices durante la enumeración, su código se bloqueará.

Si desea eliminar solo algunos objetos, puede usar la filterfunción.

Starscream
fuente
3
Esto es incorrecto para Swift. Las matrices son tipos de valor , por lo que se "copian" cuando se pasan a funciones, se asignan a variables o se utilizan en la enumeración. (Swift implementa la funcionalidad de copia en escritura para tipos de valor, por lo que la copia real se mantiene al mínimo). Intente lo siguiente para verificar: var x = [1, 2, 3, 4, 5]; imprimir (x); var i = 0; para v en x {if (v% 2 == 0) {x.remove (at: i)} else {i + = 1}}; print (x)
404compilernotfound
Sí, tiene razón, siempre que sepa exactamente lo que está haciendo. Quizás no expresé mi respuesta con claridad. Debería haber dicho que es posible, pero no es seguro . No es seguro porque está mutando el tamaño del contenedor y si comete un error en su código, su aplicación se bloqueará. Swift se trata de escribir código seguro que no se bloquee inesperadamente en tiempo de ejecución. Es por eso que usar funciones de programación funcional como filteres más seguro . Aquí está mi un ejemplo tonto:var y = [1, 2, 3, 4, 5]; print(y); for (index, value) in y.enumerated() { y.remove(at: index) } print(y)
Starscream
Solo quería distinguir que es posible modificar la colección que se enumera en Swift, a diferencia del comportamiento de lanzar excepciones de hacerlo cuando se itera a través de NSArray con enumeración rápida o incluso los tipos de colección de C #. No es la modificación la que arrojaría una excepción aquí, sino la posibilidad de administrar mal los índices y salir de los límites (porque habían disminuido el tamaño). Pero definitivamente estoy de acuerdo con usted en que generalmente es más seguro y claro usar el tipo de métodos de programación funcional para manipular colecciones. Especialmente en Swift.
404compilernotfound
2

Cree una matriz mutable para almacenar los elementos que se eliminarán y luego, después de la enumeración, elimine esos elementos del original. O bien, cree una copia de la matriz (inmutable), enumere eso y elimine los objetos (no por índice) del original mientras enumera.

Wain
fuente
2

El bucle for tradicional podría reemplazarse por un bucle while simple, útil si también necesita realizar otras operaciones en cada elemento antes de eliminarlo.

var index = array.count-1
while index >= 0 {

     let element = array[index]
     //any operations on element
     array.remove(at: index)

     index -= 1
}
Locutus
fuente
1

Recomiendo establecer los elementos en nil durante la enumeración y, después de completar, eliminar todos los elementos vacíos utilizando el método filter () de matrices.

freele
fuente
1
Eso funciona solo si el tipo almacenado es opcional. Tenga en cuenta también que el filtermétodo no se elimina, genera una nueva matriz.
Antonio
De acuerdo. El orden inverso es la mejor solución.
Freele
0

Solo para agregar, si tiene varias matrices y cada elemento en el índice N de la matriz A está relacionado con el índice N de la matriz B, entonces aún puede usar el método invirtiendo la matriz enumerada (como las respuestas anteriores). Pero recuerde que al acceder y eliminar los elementos de las otras matrices, no es necesario revertirlos.

Like so, (one can copy and paste this on Playground)

var a = ["a", "b", "c", "d"]
var b = [1, 2, 3, 4]
var c = ["!", "@", "#", "$"]

// remove c, 3, #

for (index, ch) in a.enumerated().reversed() {
    print("CH: \(ch). INDEX: \(index) | b: \(b[index]) | c: \(c[index])")
    if ch == "c" {
        a.remove(at: index)
        b.remove(at: index)
        c.remove(at: index)
    }
}

print("-----")
print(a) // ["a", "b", "d"]
print(b) // [1, 2, 4]
print(c) // ["!", "@", "$"]
Glenn Posadas
fuente