Lista genérica - movendo um item dentro da lista

155

Então, eu tenho uma lista genérica, e um oldIndexe um newIndexvalor.

Quero mover o item em oldIndex, para newIndex... o mais simples possível.

Alguma sugestão?

Nota

O item deve terminar entre os itens antes(newIndex - 1) e newIndex depois da remoção.

Richard Ev
fonte
1
Você deve alterar a resposta que marcou. Aquele com newIndex--não resulta no comportamento que você disse que queria.
Miral
1
@Miral - qual resposta você acha que deveria ser a aceita?
Richard Ev 31/01
4
jpierson's. Isso resulta no objeto que costumava estar no oldIndex antes da movimentação e no newIndex após a movimentação. Esse é o comportamento menos surpreendente (e é o que eu precisava quando escrevia algum código de reordenação de arrastar e soltar). É verdade que ele está falando ObservableCollectione não é genérico List<T>, mas é trivial simplesmente trocar as chamadas de método para obter o mesmo resultado.
Miral
O comportamento solicitado (e implementado corretamente nesta resposta ) para mover o item entre os itens em [newIndex - 1]e [newIndex]não é invertível. Move(1, 3); Move(3, 1);não retorna a lista ao estado inicial. Enquanto isso, há um comportamento diferente fornecido ObservableCollectione mencionado nesta resposta , que é invertível .
Lightman

Respostas:

138

Eu sei que você disse "lista genérica", mas não especificou que precisava usar a classe List (T), então aqui está uma amostra de algo diferente.

A classe ObservableCollection (T) possui um método Move que faz exatamente o que você deseja.

public void Move(int oldIndex, int newIndex)

Por baixo, é basicamente implementado assim.

T item = base[oldIndex];
base.RemoveItem(oldIndex);
base.InsertItem(newIndex, item);

Portanto, como você pode ver, o método de troca sugerido por outras pessoas é essencialmente o que o ObservableCollection faz em seu próprio método Move.

ATUALIZAÇÃO 30/12/2015: Agora você pode ver o código-fonte dos métodos Move e MoveItem no corefx sem usar o Reflector / ILSpy, pois o .NET é de código aberto.

jpierson
fonte
28
Gostaria de saber por que isso também não está implementado na lista <T>, alguém para lançar alguma luz sobre isso?
Andreas
Qual é a diferença entre uma lista genérica e uma classe List (T)? Eu pensei que eles eram o mesmo :(
BKSpurgeon
Um "lista genérica" pode significar qualquer tipo de lista ou recolha como a estrutura de dados em .NET que pode incluir ObservableCollection (T) ou outras classes que pode implementar um listy de interface, tais como IList / ICollection / IEnumerable.
Jpierson 30/10
6
Alguém poderia explicar, por favor, por que não há mudança no índice de destino (caso seja maior que o índice de origem)?
Vladius
@ Vladius Eu acredito que a idéia é que o valor newIndex especificado simplesmente especifique o índice desejado que o item deve estar após a mudança e, como uma inserção está sendo usada, não há motivo para ajustes. Se newIndex fosse uma posição em relação ao índice original, acho que seria outra história, mas não é assim que funciona.
Jpierson
129
var item = list[oldIndex];

list.RemoveAt(oldIndex);

if (newIndex > oldIndex) newIndex--; 
// the actual index could have shifted due to the removal

list.Insert(newIndex, item);
Garry Shutler
fonte
9
Sua solução falha se houver duas cópias do item na lista, com uma ocorrendo antes do oldIndex. Você deve usar o RemoveAt para garantir que você esteja certo.
Aaron Maenpaa
6
De fato, caso extremo sorrateira
Garry Shutler
1
@GarryShutler Não vejo como o índice pode mudar se estiver removendo e inserindo um único item. Decrementar o newIndexrealmente interrompe meu teste (veja minha resposta abaixo).
Ben Foster
1
Nota: se a segurança da thread for importante, tudo isso deve estar dentro de uma lockdeclaração.
precisa saber é o seguinte
1
Eu não usaria isso, pois é confuso por várias razões. Definir um método Move (oldIndex, newIndex) em uma lista e chamar Move (15,25) e depois Move (25,15) não é uma identidade, mas uma troca. Também Move (15,25) faz o item passar para o índice 24 e não para 25, o que eu esperaria. Além da troca, pode ser implementado por temp = item [oldindex]; item [oldindex] = item [newindex]; item [newindex] = temp; o que parece mais eficiente em matrizes grandes. Move (0,0) e Move (0,1) também seriam os mesmos, o que também é ímpar. E também Mover (0, Contagem -1) não move o item para o fim.
Wouter
12

Eu sei que esta pergunta é antiga, mas eu adaptei ESTA resposta do código javascript em C #. Espero que ajude

 public static void Move<T>(this List<T> list, int oldIndex, int newIndex)
{

    // exit if possitions are equal or outside array
    if ((oldIndex == newIndex) || (0 > oldIndex) || (oldIndex >= list.Count) || (0 > newIndex) ||
        (newIndex >= list.Count)) return;
    // local variables
    var i = 0;
    T tmp = list[oldIndex];
    // move element down and shift other elements up
    if (oldIndex < newIndex)
    {
        for (i = oldIndex; i < newIndex; i++)
        {
            list[i] = list[i + 1];
        }
    }
        // move element up and shift other elements down
    else
    {
        for (i = oldIndex; i > newIndex; i--)
        {
            list[i] = list[i - 1];
        }
    }
    // put element from position 1 to destination
    list[newIndex] = tmp;
}
Francisco
fonte
9

List <T> .Remove () e List <T> .RemoveAt () não retornam o item que está sendo removido.

Portanto, você deve usar isto:

var item = list[oldIndex];
list.RemoveAt(oldIndex);
list.Insert(newIndex, item);
M4N
fonte
5

Insira o item atualmente em oldIndexestar em newIndexe, em seguida, remova a instância original.

list.Insert(newIndex, list[oldIndex]);
if (newIndex <= oldIndex) ++oldIndex;
list.RemoveAt(oldIndex);

Você deve levar em consideração que o índice do item que você deseja remover pode mudar devido à inserção.

Megacan
fonte
1
Você deve remover antes de inserir ... seu pedido pode fazer com que a lista faça uma alocação.
Jim Balter
4

Eu criei um método de extensão para mover itens em uma lista.

Um índice não deve mudar se estiver movendo um item existente, pois estamos movendo um item para uma posição de índice existente na lista.

O caso de borda a que o @Oliver se refere abaixo (mover um item para o final da lista) realmente causaria uma falha nos testes, mas isso ocorre por design. Para inserir um novo item no final da lista, basta ligar List<T>.Add. list.Move(predicate, list.Count) deve falhar, pois essa posição de índice não existe antes da movimentação.

De qualquer forma, criei dois métodos de extensão adicionais MoveToEnde MoveToBeginning, cuja fonte pode ser encontrada aqui .

/// <summary>
/// Extension methods for <see cref="System.Collections.Generic.List{T}"/>
/// </summary>
public static class ListExtensions
{
    /// <summary>
    /// Moves the item matching the <paramref name="itemSelector"/> to the <paramref name="newIndex"/> in a list.
    /// </summary>
    public static void Move<T>(this List<T> list, Predicate<T> itemSelector, int newIndex)
    {
        Ensure.Argument.NotNull(list, "list");
        Ensure.Argument.NotNull(itemSelector, "itemSelector");
        Ensure.Argument.Is(newIndex >= 0, "New index must be greater than or equal to zero.");

        var currentIndex = list.FindIndex(itemSelector);
        Ensure.That<ArgumentException>(currentIndex >= 0, "No item was found that matches the specified selector.");

        // Copy the current item
        var item = list[currentIndex];

        // Remove the item
        list.RemoveAt(currentIndex);

        // Finally add the item at the new index
        list.Insert(newIndex, item);
    }
}

[Subject(typeof(ListExtensions), "Move")]
public class List_Move
{
    static List<int> list;

    public class When_no_matching_item_is_found
    {
        static Exception exception;

        Establish ctx = () => {
            list = new List<int>();
        };

        Because of = ()
            => exception = Catch.Exception(() => list.Move(x => x == 10, 10));

        It Should_throw_an_exception = ()
            => exception.ShouldBeOfType<ArgumentException>();
    }

    public class When_new_index_is_higher
    {
        Establish ctx = () => {
            list = new List<int> { 1, 2, 3, 4, 5 };
        };

        Because of = ()
            => list.Move(x => x == 3, 4); // move 3 to end of list (index 4)

        It Should_be_moved_to_the_specified_index = () =>
            {
                list[0].ShouldEqual(1);
                list[1].ShouldEqual(2);
                list[2].ShouldEqual(4);
                list[3].ShouldEqual(5);
                list[4].ShouldEqual(3);
            };
    }

    public class When_new_index_is_lower
    {
        Establish ctx = () => {
            list = new List<int> { 1, 2, 3, 4, 5 };
        };

        Because of = ()
            => list.Move(x => x == 4, 0); // move 4 to beginning of list (index 0)

        It Should_be_moved_to_the_specified_index = () =>
        {
            list[0].ShouldEqual(4);
            list[1].ShouldEqual(1);
            list[2].ShouldEqual(2);
            list[3].ShouldEqual(3);
            list[4].ShouldEqual(5);
        };
    }
}
Ben Foster
fonte
Onde é Ensure.Argumentdefinido?
7117 Oliver
1
Normalmente, List<T>você pode ligar Insert(list.Count, element)para colocar algo no final da lista. Então você When_new_index_is_higherdeve ligar o list.Move(x => x == 3, 5)que realmente falha.
7133 Oliver
3
@ Oliver normalmente, List<T>eu chamaria apenas .Addpara inserir um novo item no final de uma lista. Ao mover itens individuais, nunca aumentamos o tamanho original do índice, pois estamos apenas removendo um único item e reinserindo-o. Se você clicar no link na minha resposta, encontrará o código para Ensure.Argument.
9118 Ben Foster
Sua solução espera que o índice de destino seja uma posição, não entre dois elementos. Embora isso funcione bem para alguns casos de uso, não funciona para outros. Além disso, sua movimentação não suporta a mudança para o final (como observado por Oliver), mas em nenhum lugar do seu código você indica essa restrição. Também é contra-intuitivo, se eu tiver uma lista com 20 elementos e quiser mover o elemento 10 para o final, esperaria que o método Move lidasse com isso, em vez de precisar salvar a referência do objeto, remover o objeto da lista e adicione o objeto
Trisped
1
@Trisped, na verdade, se você ler minha resposta, suporte para mover um item para o final / início da lista . Você pode ver as especificações aqui . Sim, meu código espera que o índice seja uma posição (existente) válida dentro da lista. Estamos movendo itens, não os inserindo.
21412 Ben Foster
1

Eu esperaria:

// Makes sure item is at newIndex after the operation
T item = list[oldIndex];
list.RemoveAt(oldIndex);
list.Insert(newIndex, item);

... ou:

// Makes sure relative ordering of newIndex is preserved after the operation, 
// meaning that the item may actually be inserted at newIndex - 1 
T item = list[oldIndex];
list.RemoveAt(oldIndex);
newIndex = (newIndex > oldIndex ? newIndex - 1, newIndex)
list.Insert(newIndex, item);

... faria o truque, mas não tenho o VS nesta máquina para verificar.

Aaron Maenpaa
fonte
1
@GarryShutler Depende da situação. Se sua interface permitir que o usuário especifique a posição na lista por índice, eles ficarão confusos quando solicitar ao item 15 para passar para 20, mas para 19. Se sua interface permitir que o usuário arraste um item para outros na lista, faria sentido diminuir newIndexse for depois oldIndex.
Trisped
-1

Maneira mais simples:

list[newIndex] = list[oldIndex];
list.RemoveAt(oldIndex);

EDITAR

A questão não é muito clara ... Como não nos importamos para onde o list[newIndex]item vai, acho que a maneira mais simples de fazer isso é a seguinte (com ou sem um método de extensão):

    public static void Move<T>(this List<T> list, int oldIndex, int newIndex)
    {
        T aux = list[newIndex];
        list[newIndex] = list[oldIndex];
        list[oldIndex] = aux;
    }

Esta solução é a mais rápida porque não envolve inserções / remoções de listas.

bruno conde
fonte
4
Isso substituirá o item em newIndex, não será inserido.
Garry Shutler
@Garry O resultado final não será o mesmo?
Ozgur Ozcitak
4
Não, você acabará perdendo o valor no newIndex, o que não aconteceria se você o inserisse.
Garry Shutler
-2

São caras mais simples, basta fazer isso

    public void MoveUp(object item,List Concepts){

        int ind = Concepts.IndexOf(item.ToString());

        if (ind != 0)
        {
            Concepts.RemoveAt(ind);
            Concepts.Insert(ind-1,item.ToString());
            obtenernombres();
            NotifyPropertyChanged("Concepts");
        }}

Faça o mesmo com MoveDown, mas altere o if for "if (ind! = Concepts.Count ())" e o Concepts.Insert (ind + 1, item.ToString ());

Richard Aguirre
fonte
-3

Foi assim que implementei um método de extensão de elemento de movimentação. Ele lida muito bem com a movimentação antes / depois e nos extremos dos elementos.

public static void MoveElement<T>(this IList<T> list, int fromIndex, int toIndex)
{
  if (!fromIndex.InRange(0, list.Count - 1))
  {
    throw new ArgumentException("From index is invalid");
  }
  if (!toIndex.InRange(0, list.Count - 1))
  {
    throw new ArgumentException("To index is invalid");
  }

  if (fromIndex == toIndex) return;

  var element = list[fromIndex];

  if (fromIndex > toIndex)
  {
    list.RemoveAt(fromIndex);
    list.Insert(toIndex, element);
  }
  else
  {
    list.Insert(toIndex + 1, element);
    list.RemoveAt(fromIndex);
  }
}
Allan Harper
fonte
2
Esta é uma duplicata da resposta de Francisco.
nivs1978