Liste générique - déplacer un élément dans la liste

155

J'ai donc une liste générique et un oldIndex et unnewIndex valeur.

Je souhaite déplacer l'élément à oldIndex , versnewIndex ... aussi simplement que possible.

Aucune suggestion?

Remarque

L'élément doit se retrouver entre les éléments avant(newIndex - 1) et newIndex avant sa suppression.

Richard Ev
la source
1
Vous devriez changer la réponse que vous avez cochée. Celui avec newIndex--ne donne pas le comportement que vous avez dit vouloir.
Miral
1
@Miral - quelle réponse pensez-vous devrait être la réponse acceptée?
Richard Ev
4
jpierson's. Il en résulte que l'objet qui était à oldIndex avant le déplacement pour être à newIndex après le déplacement. C'est le comportement le moins surprenant (et c'est ce dont j'avais besoin lorsque j'écrivais du code de réorganisation drag'n'drop). Certes, il parle ObservableCollectionet non d'un générique List<T>, mais il est trivial de simplement échanger les appels de méthode pour obtenir le même résultat.
Miral
Le comportement demandé (et correctement implémenté dans cette réponse ) pour déplacer l'élément entre les éléments à [newIndex - 1]et [newIndex]n'est pas inversible. Move(1, 3); Move(3, 1);ne ramène pas la liste à l'état initial. Pendant ce temps, il y a un comportement différent fourni ObservableCollectionet mentionné dans cette réponse , qui est inversible .
Lightman

Réponses:

138

Je sais que vous avez dit "liste générique" mais vous n'avez pas précisé que vous deviez utiliser la liste (T) classe , alors voici un aperçu de quelque chose de différent.

La classe ObservableCollection (T) a une méthode Move qui fait exactement ce que vous voulez.

public void Move(int oldIndex, int newIndex)

En dessous, il est fondamentalement implémenté comme ça.

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

Ainsi, comme vous pouvez le voir, la méthode d'échange que d'autres ont suggérée est essentiellement ce que fait l' ObservableCollection dans sa propre méthode Move.

MISE À JOUR 2015-12-30: Vous pouvez voir le code source des méthodes Move et MoveItem dans corefx maintenant pour vous-même sans utiliser Reflector / ILSpy puisque .NET est open source.

jpierson
la source
28
Je me demande pourquoi cela n'est pas implémenté sur la List <T> également, quelqu'un pour éclairer cela?
Andreas
Quelle est la différence entre une liste générique et une classe List (T)? Je pensais qu'ils étaient les mêmes :(
BKSpurgeon
Une «liste générique» pourrait signifier tout type de liste ou de collection comme la structure de données dans .NET qui pourrait inclure ObservableCollection (T) ou d'autres classes qui peuvent implémenter une interface listy telle que IList / ICollection / IEnumerable.
jpierson
6
Quelqu'un pourrait-il expliquer, s'il vous plaît, pourquoi il n'y a pas de décalage d'index de destination (au cas où il serait plus grand que l'index source)?
Vladius
@vladius Je crois que l'idée est que la valeur newIndex spécifiée doit simplement spécifier l'index souhaité que l'élément doit être après le déplacement et comme une insertion est utilisée, il n'y a aucune raison d'ajuster. Si newIndex était une position par rapport à l'index d'origine, ce serait une autre histoire, je suppose, mais ce n'est pas ainsi que cela fonctionne.
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
la source
9
Votre solution tombe en panne s'il y a deux copies de l'élément dans la liste, une se produisant avant oldIndex. Vous devez utiliser RemoveAt pour vous assurer que vous obtenez le bon.
Aaron Maenpaa le
6
En effet, cas de bord sournois
Garry Shutler
1
@GarryShutler Je ne vois pas comment l'index pourrait changer si nous supprimons puis insérons un seul élément. Décrémenter le newIndexcasse réellement mon test (voir ma réponse ci-dessous).
Ben Foster
1
Remarque: si la sécurité des threads est importante, tout doit être dans une lockinstruction.
rory.ap
1
Je n'utiliserais pas cela car il est déroutant pour plusieurs raisons. Définir une méthode Move (oldIndex, newIndex) sur une liste et appeler Move (15,25) puis Move (25,15) n'est pas une identité mais un swap. De plus, Move (15,25) fait passer l'élément à l'index 24 et non à 25, ce à quoi je m'attendrais. Outre l'échange peut être implémenté par temp = item [oldindex]; item [oldindex] = item [nouvelindex]; item [newindex] = temp; ce qui semble plus efficace sur les grands tableaux. De plus, Move (0,0) et Move (0,1) seraient les mêmes, ce qui est également étrange. Et aussi Move (0, Count -1) ne déplace pas l'élément vers la fin.
Wouter
12

Je sais que cette question est ancienne mais j'ai adapté CETTE réponse de code javascript en C #. J'espère que ça aide

 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
la source
9

List <T> .Remove () et List <T> .RemoveAt () ne renvoient pas l'élément en cours de suppression.

Par conséquent, vous devez utiliser ceci:

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

Insérez l'élément actuellement dans lequel oldIndexse trouver newIndex, puis supprimez l'instance d'origine.

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

Vous devez tenir compte du fait que l'index de l'élément que vous souhaitez supprimer peut changer en raison de l'insertion.

Megacan
la source
1
Vous devez supprimer avant d'insérer ... votre commande peut entraîner une attribution de la liste.
Jim Balter
4

J'ai créé une méthode d'extension pour déplacer des éléments dans une liste.

Un index ne doit pas changer si nous déplaçons un élément existant car nous déplaçons un élément vers une position d'index existante dans la liste.

Le cas de bord auquel @Oliver fait référence ci-dessous (déplacer un élément à la fin de la liste) entraînerait en fait l'échec des tests, mais c'est par conception. Pour insérer un nouvel élément à la fin de la liste, nous appellerions simplement List<T>.Add. list.Move(predicate, list.Count) devrait échouer car cette position d'index n'existe pas avant le déplacement.

Dans tous les cas, j'ai créé deux méthodes d'extension supplémentaires, MoveToEndet MoveToBeginningdont la source peut être trouvée ici .

/// <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
la source
Où est Ensure.Argumentdéfini?
Oliver
1
Dans une situation normale, List<T>vous pouvez appeler Insert(list.Count, element)pour placer quelque chose à la fin de la liste. Donc, vous When_new_index_is_higherdevriez appeler list.Move(x => x == 3, 5)qui échoue.
Oliver
3
@Oliver dans un normal, List<T>je voudrais simplement appeler .Addpour insérer un nouvel élément à la fin d'une liste. Lors du déplacement d' éléments individuels, nous n'augmentons jamais la taille d'origine de l'index car nous ne supprimons qu'un seul élément et le réinsérons. Si vous cliquez sur le lien dans ma réponse, vous trouverez le code pour Ensure.Argument.
Ben Foster
Votre solution s'attend à ce que l'index de destination soit une position, pas entre deux éléments. Bien que cela fonctionne bien pour certains cas d'utilisation, cela ne fonctionne pas pour d'autres. De plus, votre déménagement ne prend pas en charge le déplacement vers la fin (comme indiqué par Oliver), mais vous n'indiquez cette contrainte à aucun endroit de votre code. C'est également contre-intuitif, si j'ai une liste avec 20 éléments et que je veux déplacer l'élément 10 à la fin, je m'attendrais à ce que la méthode Move gère cela, plutôt que d'avoir à trouver pour enregistrer la référence de l'objet, supprimer l'objet de la liste et ajoutez l'objet.
Trisped
1
@Trisped en fait si vous lisez ma réponse, déplacer un élément à la fin / au début de la liste est pris en charge. Vous pouvez voir les spécifications ici . Oui, mon code s'attend à ce que l'index soit une position valide (existante) dans la liste. Nous déplaçons des articles, nous ne les insérons pas.
Ben Foster
1

Je m'attendrais soit:

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

... ferait l'affaire, mais je n'ai pas de VS sur cette machine à vérifier.

Aaron Maenpaa
la source
1
@GarryShutler Cela dépend de la situation. Si votre interface permet à l'utilisateur de spécifier la position dans la liste par index, il sera confus lorsqu'il dit à l'élément 15 de passer à 20, mais à la place, il se déplace vers 19. Si votre interface permet à l'utilisateur de faire glisser un élément entre les autres sur la liste, il serait logique de décrémenter newIndexsi c'est après oldIndex.
Trisped
-1

Manière la plus simple:

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

ÉDITER

La question n'est pas très claire ... Comme nous ne nous soucions pas de savoir où list[newIndex]va l' élément, je pense que la façon la plus simple de le faire est la suivante (avec ou sans méthode d'extension):

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

Cette solution est la plus rapide car elle n'implique pas d'insertion / suppression de liste.

bruno conde
la source
4
Cela écrasera l'élément à newIndex, pas l'insertion.
Garry Shutler
@Garry Le résultat final ne sera-t-il pas le même?
Ozgur Ozcitak
4
Non, vous finirez par perdre la valeur de newIndex, ce qui ne se produirait pas si vous insérez.
Garry Shutler
-2

Est-ce que les gars plus simples font juste ça

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

Faites la même chose avec MoveDown mais modifiez le if pour "if (ind! = Concepts.Count ())" et Concepts.Insert (ind + 1, item.ToString ());

Richard Aguirre
la source
-3

C'est ainsi que j'ai implémenté une méthode d'extension d'élément de déplacement. Il gère assez bien les mouvements avant / après et aux extrêmes pour les éléments.

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
la source
2
C'est un double de la réponse de Francisco.
nivs1978