Lista genérica: mover un elemento dentro de la lista

155

Entonces tengo una lista genérica, y un oldIndexy un newIndexvalor.

Quiero mover el elemento a oldIndex, a newIndex... lo más simple posible.

¿Alguna sugerencia?

Nota

El elemento debe terminar entre los elementos en (newIndex - 1)y newIndex antes de ser eliminado.

Richard Ev
fuente
1
Debería cambiar la respuesta que marcó. El que newIndex--tiene no produce el comportamiento que dijiste que querías.
Miral
1
@Miral: ¿qué respuesta crees que debería ser la aceptada?
Richard Ev
44
jpierson's. Resulta en el objeto que solía estar en oldIndex antes del movimiento para estar en newIndex después del movimiento. Este es el comportamiento menos sorprendente (y es lo que necesitaba cuando estaba escribiendo un código de reordenamiento de arrastrar y soltar). Es cierto que está hablando ObservableCollectiony no es un genérico List<T>, pero es trivial simplemente intercambiar las llamadas a métodos para obtener el mismo resultado.
Miral
El comportamiento solicitado (e implementado correctamente en esta respuesta ) para mover el elemento entre los elementos en [newIndex - 1]y [newIndex]no es invertible. Move(1, 3); Move(3, 1);no devuelve la lista al estado inicial. Mientras tanto, hay un comportamiento diferente proporcionado ObservableCollectiony mencionado en esta respuesta , que es invertible .
Lightman

Respuestas:

138

Sé que dijiste "lista genérica" ​​pero no especificaste que necesitabas usar la clase Lista (T) , así que aquí hay una oportunidad de algo diferente.

La clase ObservableCollection (T) tiene un método Move que hace exactamente lo que desea.

public void Move(int oldIndex, int newIndex)

Debajo se implementa básicamente de esta manera.

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

Como puede ver, el método de intercambio que otros han sugerido es esencialmente lo que hace ObservableCollection en su propio método Move.

ACTUALIZACIÓN 2015-12-30: Puede ver el código fuente de los métodos Move y MoveItem en corefx ahora sin usar Reflector / ILSpy ya que .NET es de código abierto.

jpierson
fuente
28
Me pregunto por qué esto no se implementa también en la Lista <T>, ¿alguien para arrojar algo de luz sobre esto?
Andreas
¿Cuál es la diferencia entre una lista genérica y una clase Lista (T)? Pensé que eran lo mismo :(
BKSpurgeon
A "lista genérica" podría significar cualquier tipo de lista o colección como estructura de datos en .NET que podría incluir ObservableCollection (T) u otras clases que puede implementar un listy interfaz tal como IList / ICollection / IEnumerable.
jpierson
66
¿Podría alguien explicar, por favor, por qué no hay un cambio en el índice de destino (en caso de que sea mayor que el índice de origen)?
Vladius
@vladius Creo que la idea es que el valor newIndex especificado simplemente debe especificar el índice deseado de que el elemento debe estar después del movimiento y, dado que se está utilizando un inserto, no hay razón para el ajuste. Si newIndex fuera una posición relativa al índice original, sería otra historia, supongo, pero no es así como 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
fuente
9
Su solución se descompone si hay dos copias de un elemento en la lista, y una se produce antes de oldIndex. Debe usar RemoveAt para asegurarse de obtener el correcto.
Aaron Maenpaa
66
De hecho, caso furtivo
Garry Shutler
1
@GarryShutler No puedo ver cómo podría cambiar el índice si estamos eliminando y luego insertando un solo elemento. La disminución de la newIndexrealidad rompe mi prueba (ver mi respuesta a continuación).
Ben Foster, el
1
Nota: si la seguridad del hilo es importante, todo esto debería estar dentro de una lockdeclaración.
rory.ap
1
No usaría esto, ya que es confuso por varias razones. Definir un método Move (oldIndex, newIndex) en una lista y llamar a Move (15,25) y luego Move (25,15) no es una identidad sino un intercambio. También Move (15,25) hace que el elemento se mueva al índice 24 y no a 25, lo que esperaría. Además el intercambio se puede implementar con temp = item [oldindex]; item [oldindex] = item [newindex]; item [newindex] = temp; que parece más eficiente en matrices grandes. También Move (0,0) y Move (0,1) serían lo mismo, lo que también es extraño. Y también Move (0, Count -1) no mueve el elemento hasta el final.
Wouter
12

Sé que esta pregunta es antigua, pero adapté ESTA respuesta del código de JavaScript a C #. Espero eso ayude

 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
fuente
9

List <T> .Remove () y List <T> .RemoveAt () no devuelven el elemento que se está eliminando.

Por lo tanto, debes usar esto:

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

Inserte el elemento actualmente en oldIndexestar en newIndexy luego elimine la instancia original.

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

Debe tener en cuenta que el índice del elemento que desea eliminar puede cambiar debido a la inserción.

Megacan
fuente
1
Debe eliminar antes de insertar ... su pedido puede hacer que la lista haga una asignación.
Jim Balter
4

Creé un método de extensión para mover elementos en una lista.

Un índice no debe cambiar si estamos moviendo un elemento existente ya que estamos moviendo un elemento a una posición de índice existente en la lista.

El caso extremo al que @Oliver se refiere a continuación (mover un elemento al final de la lista) en realidad provocaría que las pruebas fallaran, pero esto es por diseño. Para insertar un nuevo elemento al final de la lista, simplemente llamaríamos List<T>.Add. list.Move(predicate, list.Count) debería fallar ya que esta posición de índice no existe antes del movimiento.

En cualquier caso, he creado dos métodos de extensión adicionales MoveToEndy MoveToBeginningla fuente de los cuales se puede encontrar aquí .

/// <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
fuente
¿Dónde se Ensure.Argumentdefine?
Oliver
1
En condiciones normales List<T>, puede llamar Insert(list.Count, element)para colocar algo al final de la lista. Entonces When_new_index_is_higherdebería llamar, lo list.Move(x => x == 3, 5)que realmente falla.
Oliver
3
@Oliver en una forma normal List<T>, simplemente llamaría .Addpara insertar un nuevo elemento al final de una lista. Al mover elementos individuales, nunca aumentamos el tamaño original del índice, ya que solo eliminamos un solo elemento y lo volvemos a insertar. Si hace clic en el enlace en mi respuesta, encontrará el código para Ensure.Argument.
Ben Foster, el
Su solución espera que el índice de destino sea una posición, no entre dos elementos. Si bien esto funciona bien para algunos casos de uso, no funciona para otros. Además, su movimiento no es compatible con el movimiento hasta el final (como señaló Oliver), pero en ningún lugar de su código indica esta restricción. También es contraintuitivo, si tengo una lista con 20 elementos y quiero mover el elemento 10 al final, esperaría que el método Move maneje esto, en lugar de tener que buscar para guardar la referencia del objeto, eliminar el objeto de la lista y agrega el objeto.
Trispedó
1
@Trisped en realidad si lees mi respuesta, se admite mover un elemento al final / principio de la lista . Puedes ver las especificaciones aquí . Sí, mi código espera que el índice sea una posición válida (existente) dentro de la lista. Estamos moviendo elementos, no insertándolos.
Ben Foster
1

Yo esperaría cualquiera:

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

... o:

// 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);

... haría el truco, pero no tengo VS en esta máquina para verificar.

Aaron Maenpaa
fuente
1
@GarryShutler Depende de la situación. Si su interfaz le permite al usuario especificar la posición en la lista por índice, se confundirán cuando le digan al elemento 15 que se mueva a 20, pero en su lugar se mueve a 19. Si su interfaz le permite al usuario arrastrar un elemento entre otros. en la lista, entonces tendría sentido disminuir newIndexsi es posterior oldIndex.
Trispedó
-1

La forma más simple:

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

EDITAR

La pregunta no está muy clara ... Como no nos importa a dónde list[newIndex]va el artículo, creo que la forma más sencilla de hacerlo es la siguiente (con o sin un método de extensión):

    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 solución es la más rápida porque no implica inserciones / eliminaciones de listas.

bruno conde
fuente
44
Eso sobrescribirá el elemento en newIndex, no insertará.
Garry Shutler el
@Garry ¿No será el resultado final el mismo?
Ozgur Ozcitak
44
No, terminará perdiendo el valor en newIndex, lo que no sucedería si insertara.
Garry Shutler el
-2

Es más simple, chicos, solo hagan esto

    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");
        }}

Haga lo mismo con MoveDown pero cambie if para "if (ind! = Concepts.Count ())" y Concepts.Insert (ind + 1, item.ToString ());

Richard Aguirre
fuente
-3

Así es como implementé un método de extensión de elemento de movimiento. Maneja el movimiento antes / después y hasta los extremos para elementos bastante bien.

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
fuente
2
Este es un duplicado de la respuesta de Francisco.
nivs1978