Removendo da matriz durante a enumeração no Swift?

86

Eu quero enumerar por meio de uma matriz em Swift e remover certos itens. Estou me perguntando se isso é seguro de fazer e, se não, como devo fazer isso.

Atualmente, eu estaria fazendo isso:

for (index, aString: String) in enumerate(array) {
    //Some of the strings...
    array.removeAtIndex(index)
}
Andrew
fonte

Respostas:

72

No Swift 2 isso é bastante fácil de usar enumeratee reverse.

var a = [1,2,3,4,5,6]
for (i,num) in a.enumerate().reverse() {
    a.removeAtIndex(i)
}
print(a)
Johnston
fonte
1
Funciona, mas o filtro é realmente a melhor
13
@Mayerz False. "Quero enumerar por meio de um array em Swift e remover certos itens." filterretorna uma nova matriz. Você não está removendo nada do array. Eu nem mesmo chamaria filteruma enumeração. Sempre há mais de uma maneira de esfolar um gato.
Johnston
6
certo, meu mal! Pla não
56

Você pode considerar a filtermaneira:

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

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

O parâmetro de filteré apenas um encerramento que recebe uma instância do tipo array (neste caso String) e retorna a Bool. Quando o resultado é, trueele mantém o elemento, caso contrário, o elemento é filtrado.

Matteo Piombo
fonte
16
Eu deixaria explícito que filternão atualiza o array, ele apenas retorna um novo
Antonio
Os parênteses devem ser excluídos; isso é um fechamento à direita.
Jessy
@Antonio você está certo. Na verdade, é por isso que o coloquei como uma solução mais segura. Uma solução diferente pode ser considerada para matrizes enormes.
Matteo Piombo
Hm, como você disse, isso retorna uma nova matriz. É possível transformar o filtermétodo em um mutating(como li, a mutatingpalavra - chave permite que funções como essa sejam alteradas self)?
Gee.E
@ Gee.E certamente você pode adicionar um filtro no local como uma extensão ao Arraymarcá-lo como mutatinge semelhante ao código da pergunta. De qualquer forma, considere que isso nem sempre é uma vantagem. De qualquer forma, toda vez que você remove um objeto, seu array pode ser reorganizado na memória. Assim, poderia ser mais eficiente alocar um novo array e então fazer uma substituição atômica com o resultado da função de filtro. O compilador pode fazer ainda mais otimizações, dependendo do seu código.
Matteo Piombo
38

No Swift 3 e 4 , isso seria:

Com números, de acordo com a resposta de Johnston:

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

Com strings como a pergunta do 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)

No entanto, agora no Swift 4.2 ou posterior, existe uma maneira ainda melhor e mais rápida que foi recomendada pela Apple em WWDC2018:

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

Esta nova forma tem várias vantagens:

  1. É mais rápido do que as implementações com filter.
  2. Isso elimina a necessidade de reverter matrizes.
  3. Ele remove itens no local e, portanto, atualiza o array original em vez de alocar e retornar um novo array.
Jvarela
fonte
e se o item for um objeto e eu precisar verificá-lo, {$0 === Class.self}não funciona
TomSawyer,
14

Quando um elemento em um determinado índice é removido de uma matriz, todos os elementos subsequentes terão sua posição (e índice) alterados, porque eles mudam uma posição para trás.

Portanto, a melhor maneira é navegar pelo array na ordem inversa - e, neste caso, sugiro usar um loop for tradicional:

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

No entanto, na minha opinião, a melhor abordagem é usar o filtermétodo, conforme descrito por @perlfly em sua resposta.

Antonio
fonte
mas, infelizmente, foi removido rapidamente 3
Sergey Brazhnik
4

Não, não é seguro transformar arrays durante a enumaração, seu código irá travar.

Se você deseja excluir apenas alguns objetos, você pode usar a filterfunção.

Starscream
fonte
3
Isso é incorreto para o Swift. Arrays são tipos de valor , então eles são "copiados" quando são passados ​​para funções, atribuídos a variáveis ​​ou usados ​​na enumeração. (O Swift implementa a funcionalidade de cópia na gravação para tipos de valor, portanto, a cópia real é reduzida ao mínimo.) Tente o seguinte para verificar: var x = [1, 2, 3, 4, 5]; imprimir (x); var i = 0; para v em x {if (v% 2 == 0) {x.remove (at: i)} else {i + = 1}}; print (x)
404compilernotfounded
Sim, você está certo, desde que saiba exatamente o que está fazendo. Talvez eu não tenha expressado minha resposta claramente. Eu deveria ter dito que é possível, mas não é seguro . Não é seguro porque você está alterando o tamanho do contêiner e, se cometer um erro em seu código, seu aplicativo irá travar. O objetivo do Swift é escrever código seguro que não travará inesperadamente durante a execução. É por isso que usar funções de programação funcional filteré mais seguro . Aqui está meu exemplo idiota:var y = [1, 2, 3, 4, 5]; print(y); for (index, value) in y.enumerated() { y.remove(at: index) } print(y)
Starscream
Eu só queria distinguir que é possível modificar a coleção que está sendo enumerada no Swift, em oposição ao comportamento de lançamento de exceções ao iterar por meio do NSArray com enumeração rápida ou até mesmo os tipos de coleção do C #. Não é a modificação que lançaria uma exceção aqui, mas a possibilidade de gerenciar mal os índices e sair dos limites (porque eles diminuíram o tamanho). Mas eu definitivamente concordo com você que geralmente é mais seguro e claro usar o tipo de programação funcional de métodos para manipular coleções. Especialmente em Swift.
404compilernotfound
2

Crie uma matriz mutável para armazenar os itens a serem excluídos e, em seguida, após a enumeração, remova esses itens do original. Ou crie uma cópia do array (imutável), enumere e remova os objetos (não por índice) do original durante a enumeração.

Wain
fonte
2

O loop for tradicional pode ser substituído por um loop while simples, útil se você também precisar realizar algumas outras operações em cada elemento antes da remoção.

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

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

     index -= 1
}
Locutus
fonte
1

Eu recomendo definir os elementos como nulo durante a enumeração e, depois de concluir, remover todos os elementos vazios usando o método arrays filter ().

Freele
fonte
1
Isso funciona apenas se o tipo armazenado for opcional. Observe também que o filtermétodo não remove, ele gera um novo array.
Antonio
Aceita. A ordem invertida é a melhor solução.
freele
0

Apenas para adicionar, se você tiver vários arrays e cada elemento no índice N do array A estiver relacionado ao índice N do array B, então você ainda pode usar o método invertendo o array enumerado (como nas respostas anteriores). Mas lembre-se de que ao acessar e excluir os elementos dos outros arrays, não há necessidade de revertê-los.

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
fonte