Lista ogólna - przenoszenie pozycji w ramach listy

155

Mam więc listę ogólną oraz wartość oldIndexi newIndex.

Chcę przenieść element o adres oldIndexdo newIndex... tak prosto, jak to możliwe.

Jakieś sugestie?

Uwaga

Przedmiot powinien znaleźć się między elementami na miejscu (newIndex - 1)i newIndex przed jego usunięciem.

Richard Ev
źródło
1
Powinieneś zmienić zaznaczoną odpowiedź. Ten z newIndex--nie skutkuje zachowaniem, o którym mówiłeś, że chciałeś.
Miral
1
@Miral - która Twoim zdaniem powinna być akceptowana?
Richard Ev
4
jpierson's. Powoduje to, że obiekt, który wcześniej znajdował się w oldIndex przed ruchem, był w newIndex po przeniesieniu. To najmniej zaskakujące zachowanie (i właśnie tego potrzebowałem, kiedy pisałem kod do zmiany kolejności drag'n'drop). To prawda, że ​​mówi o tym, ObservableCollectiona nie o typie List<T>, ale trywialne jest po prostu zamiana wywołań metod, aby uzyskać ten sam wynik.
Miral
Żądane (i poprawnie zaimplementowane w tej odpowiedzi ) zachowanie polegające na przeniesieniu elementu między elementami o godzinie [newIndex - 1]i [newIndex]nie jest odwracalne. Move(1, 3); Move(3, 1);nie przywraca listy do stanu początkowego. Tymczasem w tej odpowiedzi podanoObservableCollection i wspomniano inne zachowanie , które jest odwracalne .
Lightman

Odpowiedzi:

138

Wiem, że powiedziałeś „lista ogólna”, ale nie określiłeś, że musisz użyć klasy List (T), więc oto próbka na coś innego.

Klasa ObservableCollection (T) ma metodę Move, która robi dokładnie to, co chcesz.

public void Move(int oldIndex, int newIndex)

Pod spodem jest zasadniczo zaimplementowany w ten sposób.

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

Jak widać, metoda zamiany, którą sugerowali inni, jest zasadniczo tym, co robi ObservableCollection w swojej własnej metodzie Move.

AKTUALIZACJA 2015-12-30: Możesz teraz zobaczyć kod źródłowy metod Move i MoveItem w corefx teraz dla siebie bez używania Reflector / ILSpy, ponieważ .NET jest open source.

jpierson
źródło
28
Zastanawiam się, dlaczego nie jest to również zaimplementowane w List <T>, ktoś, kto rzuci trochę światła na to?
Andreas
Jaka jest różnica między listą ogólną a klasą List (T)? Myślałem, że są takie same :(
BKSpurgeon
„Lista ogólna” może oznaczać dowolny typ listy lub kolekcji, na przykład strukturę danych w .NET, która może zawierać ObservableCollection (T) lub inne klasy, które mogą implementować interfejs listy , taki jak IList / ICollection / IEnumerable.
jpierson
6
Czy ktoś mógłby wyjaśnić, proszę, dlaczego rzeczywiście nie ma przesunięcia indeksu docelowego (w przypadku, gdy jest większy niż indeks źródłowy)?
Vladius
@vladius Myślę, że idea polega na tym, że określona wartość newIndex powinna po prostu określać żądany indeks, który powinien znajdować się element po przeniesieniu, a ponieważ używana jest wkładka, nie ma powodu do dostosowywania. Gdyby newIndex był pozycją względem oryginalnego indeksu, byłaby to inna historia, ale myślę, że tak nie działa.
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
źródło
9
Twoje rozwiązanie nie działa, jeśli na liście znajdują się dwie kopie elementu, z których jedna występuje przed oldIndex. Powinieneś użyć RemoveAt, aby upewnić się, że masz właściwy.
Aaron Maenpaa
6
Rzeczywiście, podstępna sprawa
Garry Shutler
1
@GarryShutler Nie widzę, jak indeks mógłby się przesunąć, jeśli usuwamy, a następnie wstawiamy pojedynczy element. Zmniejszenie wartości newIndexfaktycznie łamie mój test (zobacz moją odpowiedź poniżej).
Ben Foster
1
Uwaga: jeśli bezpieczeństwo wątków jest ważne, wszystko to powinno znajdować się w lockinstrukcji.
rory.ap
1
Nie użyłbym tego, ponieważ jest to mylące z kilku powodów. Zdefiniowanie metody Move (oldIndex, newIndex) na liście i wywołanie Move (15,25), a następnie Move (25,15) nie jest tożsamością, ale zamianą. Również Move (15,25) powoduje, że pozycja przesuwa się do indeksu 24, a nie 25, czego bym się spodziewał. Poza tym zamiana może zostać zaimplementowana przez temp = item [oldindex]; item [oldindex] = item [newindex]; item [newindex] = temp; co wydaje się bardziej wydajne w przypadku dużych tablic. Również Ruch (0,0) i Ruch (0,1) byłyby takie same, co również jest nieparzyste. A także Move (0, Count -1) nie przenosi elementu na koniec.
Wouter
12

Wiem, że to pytanie jest stare, ale dostosowałem odpowiedź kodu javascript do C #. Mam nadzieję, że to pomoże

 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
źródło
9

List <T> .Remove () i List <T> .RemoveAt () nie zwracają usuwanego elementu.

Dlatego musisz tego użyć:

var item = list[oldIndex];
list.RemoveAt(oldIndex);
list.Insert(newIndex, item);
M4N
źródło
5

Wstaw element, w którym aktualnie oldIndexma być, newIndexa następnie usuń oryginalne wystąpienie.

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

Musisz wziąć pod uwagę, że indeks elementu, który chcesz usunąć, może ulec zmianie w wyniku wstawienia.

Megacan
źródło
1
Należy usunąć przed wstawieniem ... Twoje zamówienie może spowodować alokację na liście.
Jim Balter
4

Stworzyłem metodę rozszerzenia do przenoszenia elementów na liście.

Indeks nie powinien się przesuwać, jeśli przenosimy istniejący element, ponieważ przenosimy element do istniejącej pozycji indeksu na liście.

Przypadek skrajny, do którego odnosi się @Oliver poniżej (przeniesienie elementu na koniec listy) w rzeczywistości spowodowałby niepowodzenie testów, ale jest to zgodne z projektem. Aby wstawić nową pozycję na końcu listy, po prostu zadzwonilibyśmy List<T>.Add. list.Move(predicate, list.Count) powinien nie powieść, ponieważ ta pozycja indeksu nie istnieje przed ruchem.

W każdym razie, mam utworzone dwa dodatkowe metody rozszerzenie, MoveToEndi MoveToBeginning, których źródłem mogą być znalezione tutaj .

/// <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
źródło
Gdzie jest Ensure.Argumentzdefiniowane?
Oliver
1
Normalnie List<T>możesz zadzwonić, Insert(list.Count, element)aby umieścić coś na końcu listy. Więc When_new_index_is_higherpowinieneś sprawdzić, list.Move(x => x == 3, 5)co faktycznie się nie udaje.
Oliver
3
@Oliver w normalnym List<T>, po prostu zadzwoniłbym, .Addaby wstawić nowy element na koniec listy. Podczas przenoszenia pojedynczych elementów nigdy nie zwiększamy oryginalnego rozmiaru indeksu, ponieważ usuwamy tylko jeden element i wkładamy go ponownie. Jeśli klikniesz link w mojej odpowiedzi, znajdziesz kod Ensure.Argument.
Ben Foster
Twoje rozwiązanie oczekuje, że indeks docelowy jest pozycją, a nie między dwoma elementami. Chociaż działa to dobrze w niektórych przypadkach użycia, nie działa w innych. Ponadto Twój ruch nie obsługuje przejścia do końca (jak zauważył Oliver), ale nigdzie w kodzie nie wskazałeś tego ograniczenia. Jest to również sprzeczne z intuicją, jeśli mam listę z 20 elementami i chcę przesunąć element 10 na koniec, spodziewałbym się, że metoda Move sobie z tym poradzi, zamiast konieczności znalezienia zapisania odniesienia do obiektu, usunięcia obiektu z listy i dodaj obiekt.
Trisped
1
@Trisped właściwie, jeśli przeczytasz moją odpowiedź, obsługiwane jest przeniesienie elementu na koniec / początek listy . Możesz zobaczyć specyfikację tutaj . Tak, mój kod oczekuje, że indeks będzie prawidłową (istniejącą) pozycją na liście. Jesteśmy ruchomych elementów, a nie ich wstawienie.
Ben Foster
1

Spodziewałbym się:

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

... lub:

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

... by załatwił sprawę, ale nie mam VS na tym komputerze do sprawdzenia.

Aaron Maenpaa
źródło
1
@GarryShutler To zależy od sytuacji. Jeśli twój interfejs pozwala użytkownikowi określić pozycję na liście według indeksu, będą zdezorientowani, gdy powiedzą elementowi 15, aby przesunął się do 20, ale zamiast tego przesuwa się do 19. Jeśli twój interfejs pozwala użytkownikowi przeciągać element między innymi na liście, sensowne byłoby zmniejszenie wartości, newIndexjeśli jest po oldIndex.
Trisped
-1

Najprostszy sposób:

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

EDYTOWAĆ

Pytanie nie jest zbyt jasne ... Ponieważ nie obchodzi nas, dokąd list[newIndex]trafia element, myślę, że najprostszy sposób na zrobienie tego jest następujący (z metodą rozszerzającą lub bez):

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

To rozwiązanie jest najszybsze, ponieważ nie obejmuje wstawiania / usuwania list.

bruno conde
źródło
4
Spowoduje to nadpisanie elementu w newIndex, a nie wstawienie.
Garry Shutler
@Garry Czy efekt końcowy nie będzie taki sam?
Ozgur Ozcitak
4
Nie, w końcu stracisz wartość newIndex, co nie nastąpiłoby, gdybyś wstawił.
Garry Shutler
-2

Czy prostsze faceci po prostu to robią

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

Zrób to samo z MoveDown, ale zmień if dla "if (ind! = Concepts.Count ())" i Concepts.Insert (ind + 1, item.ToString ());

Richard Aguirre
źródło
-3

W ten sposób zaimplementowałem metodę rozszerzenia elementu Move. Całkiem dobrze radzi sobie z przesuwaniem się przed / po i do skrajności dla elementów.

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
źródło
2
To jest duplikat odpowiedzi Francisco.
nivs1978