Elenco generico: spostamento di un elemento nell'elenco


155

Quindi ho un elenco generico e un oldIndexe un newIndexvalore.

Voglio spostare l'oggetto in oldIndex, a newIndex... il più semplicemente possibile.

Eventuali suggerimenti?

Nota

L'articolo dovrebbe trovarsi tra gli elementi in (newIndex - 1)e newIndex prima che fosse rimosso.


1
Dovresti cambiare la risposta che hai spuntato. Quello con newIndex--non comporta il comportamento che hai detto che volevi.
Miral

1
@Miral - quale risposta pensi dovrebbe essere quella accettata?
Richard Ev,

4
jpierson di. Ne risulta che l'oggetto che si trovava in oldIndex prima dello spostamento si trova in newIndex dopo lo spostamento. Questo è il comportamento meno sorprendente (ed è quello di cui avevo bisogno quando stavo scrivendo un codice di riordino drag'n'drop). Concesso che sta parlando ObservableCollectione non un generico List<T>, ma è banale scambiare semplicemente le chiamate del metodo per ottenere lo stesso risultato.
Miral

Il comportamento richiesto (e correttamente implementato in questa risposta ) di spostare l'articolo tra gli articoli in [newIndex - 1]e [newIndex]non è invertibile. Move(1, 3); Move(3, 1);non riporta l'elenco allo stato iniziale. Nel frattempo c'è un comportamento diverso fornito ObservableCollectione menzionato in questa risposta , che è invertibile .
Lightman,

Risposte:


138

So che hai detto "elenco generico" ma non hai specificato che dovevi usare la classe List (T) , quindi ecco una panoramica di qualcosa di diverso.

La classe ObservableCollection (T) ha un metodo Move che fa esattamente quello che vuoi.

public void Move(int oldIndex, int newIndex)

Sotto è praticamente implementato in questo modo.

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

Quindi, come puoi vedere, il metodo di scambio suggerito da altri è essenzialmente quello che fa l' ObservableCollection nel suo metodo Move.

AGGIORNAMENTO 2015-12-30: Puoi vedere il codice sorgente per i metodi Move e MoveItem in corefx ora senza usare Reflector / ILSpy poiché .NET è open source.


28
Mi chiedo perché questo non sia implementato anche nella Lista <T>, qualcuno per far luce su questo?
Andreas,

Qual è la differenza tra un elenco generico e una classe List (T)? Pensavo fossero uguali :(
BKSpurgeon,

Un "elenco generico" potrebbe significare qualsiasi tipo di elenco o raccolta come la struttura di dati in .NET che potrebbe includere ObservableCollection (T) o altre classi che potrebbero implementare un'interfaccia listy come IList / ICollection / IEnumerable.
jpierson,

6
Qualcuno potrebbe spiegare, per favore, perché non esiste uno spostamento dell'indice di destinazione (nel caso in cui sia maggiore dell'indice di origine)?
Vladius,

@vladius Credo che l'idea sia che il valore newIndex specificato dovrebbe semplicemente specificare l'indice desiderato che l'elemento dovrebbe essere dopo lo spostamento e poiché viene utilizzato un inserto, non vi è motivo di regolazione. Se newIndex fosse una posizione relativa all'indice originale sarebbe un'altra storia, ma non è così che funziona.
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);

9
La soluzione si interrompe se nell'elenco sono presenti due copie dell'articolo, di cui una prima di oldIndex. Dovresti usare RemoveAt per assicurarti di ottenere quello giusto.
Aaron Maenpaa,

6
Anzi, subdolo caso
Garry Shutler,

1
@GarryShutler Non riesco a vedere come l'indice potrebbe spostarsi se stiamo rimuovendo e quindi inserendo un singolo elemento. Decrementare newIndexeffettivamente interrompe il mio test (vedi la mia risposta di seguito).
Ben Foster,

1
Nota: se la sicurezza del thread è importante, dovrebbe essere tutto all'interno di lockun'istruzione.
rory.ap,

1
Non lo userei perché è confuso per diversi motivi. Definire un metodo Move (oldIndex, newIndex) in un elenco e chiamare Move (15,25) e quindi Move (25,15) non è un'identità ma uno swap. Anche Move (15,25) fa spostare l'oggetto sull'indice 24 e non su 25, come mi aspetterei. Inoltre lo scambio può essere implementato da temp = item [oldindex]; item [oldindex] = item [NewIndex]; item [NewIndex] = temperatura; che sembra più efficiente su array di grandi dimensioni. Anche Move (0,0) e Move (0,1) sarebbero gli stessi, il che è anche dispari. E anche Move (0, Count -1) non sposta l'elemento alla fine.
Wouter

12

So che questa domanda è vecchia ma ho adattato QUESTA risposta del codice javascript a C #. Spero che sia d'aiuto

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

9

Elenco <T> .Remove () e Elenco <T> .RemoveAt () non restituiscono l'elemento che viene rimosso.

Pertanto devi usare questo:

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

5

Inserisci l'elemento attualmente in oldIndexessere in newIndexe quindi rimuovere l'istanza originale.

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

Devi tenere conto del fatto che l'indice dell'articolo che desideri rimuovere potrebbe cambiare a causa dell'inserimento.


1
È necessario rimuovere prima di inserire ... il tuo ordine potrebbe causare un allocazione dell'elenco.
Jim Balter,

4

Ho creato un metodo di estensione per spostare elementi in un elenco.

Un indice non dovrebbe spostarsi se stiamo spostando un articolo esistente poiché stiamo spostando un articolo in una posizione di indice esistente nell'elenco.

Il caso limite a cui fa riferimento @Oliver di seguito (spostando un elemento alla fine dell'elenco) provocherebbe effettivamente l'esito negativo dei test, ma questo è in base alla progettazione. Per inserire un nuovo elemento alla fine dell'elenco, chiameremmo semplicemente List<T>.Add. list.Move(predicate, list.Count) dovrebbe fallire poiché questa posizione dell'indice non esiste prima dello spostamento.

In ogni caso, ho creato due metodi di estensione aggiuntivi MoveToEnde MoveToBeginningla cui fonte è disponibile qui .

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

Dove è Ensure.Argumentdefinito?
Oliver,

1
In un normale List<T>è possibile chiamare Insert(list.Count, element)per posizionare qualcosa alla fine dell'elenco. Quindi When_new_index_is_higherdovresti chiamare list.Move(x => x == 3, 5)che in realtà fallisce.
Oliver,

3
@Oliver in un normale List<T>vorrei solo chiamare .Addper inserire un nuovo elemento alla fine di un elenco. Quando spostiamo singoli articoli non aumentiamo mai le dimensioni originali dell'indice poiché rimuoviamo solo un singolo articolo e lo reinseriamo. Se fai clic sul link nella mia risposta troverai il codice per Ensure.Argument.
Ben Foster

La soluzione prevede che l'indice di destinazione sia una posizione, non tra due elementi. Mentre questo funziona bene per alcuni casi d'uso, non funziona per altri. Inoltre, la tua mossa non supporta lo spostamento alla fine (come notato da Oliver) ma nessuna posizione nel tuo codice indica questo vincolo. È anche controintuitivo, se ho un elenco con 20 elementi e voglio spostare l'elemento 10 alla fine, mi aspetterei che il metodo Move lo gestisca, piuttosto che dover trovare per salvare il riferimento all'oggetto, rimuovere l'oggetto dall'elenco e aggiungi l'oggetto.
Trisped

1
@Trisped in realtà se leggi la mia risposta, è supportato lo spostamento di un elemento alla fine / all'inizio dell'elenco . Puoi vedere le specifiche qui . Sì, il mio codice prevede che l'indice sia una posizione (esistente) valida all'interno dell'elenco. Stiamo spostando oggetti, non inserendoli.
Ben Foster,

1

Mi aspetterei neanche:

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

... farebbe il trucco, ma non ho VS su questa macchina da controllare.


1
@GarryShutler Dipende dalla situazione. Se la tua interfaccia consente all'utente di specificare la posizione nell'elenco per indice, saranno confusi quando dicono all'elemento 15 di spostarsi su 20, ma invece si sposta su 19. Se l'interfaccia consente all'utente di trascinare un elemento tra gli altri nell'elenco allora avrebbe senso diminuire newIndexse è dopo oldIndex.
Trisped

-1

Il modo più semplice:

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

MODIFICARE

La domanda non è molto chiara ... Dal momento che non ci interessa dove list[newIndex]va l' articolo, penso che il modo più semplice per farlo sia il seguente (con o senza un metodo di estensione):

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

Questa soluzione è la più veloce perché non prevede inserimenti / rimozioni di elenchi.


4
Ciò sovrascriverà l'elemento in newIndex, non l'inserimento.
Garry Shutler,

@Garry Il risultato finale non sarà lo stesso?
Ozgur Ozcitak,

4
No, finirai per perdere il valore in newIndex che non accadrebbe se lo avessi inserito.
Garry Shutler,

-2

I ragazzi più semplici lo fanno e basta

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

Fai lo stesso con MoveDown ma modifica if per "if (ind! = Concepts.Count ())" e Concepts.Insert (ind + 1, item.ToString ());


-3

Ecco come ho implementato un metodo di estensione degli elementi di spostamento. Gestisce lo spostamento prima / dopo e agli estremi per elementi abbastanza bene.

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

2
Questo è un duplicato della risposta di Francisco.
nivs1978,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.