Общий список - перемещение элемента в списке

155

Итак, у меня есть общий список, а также oldIndexи newIndexзначение.

Я хочу переместить элемент в oldIndex, чтобы newIndex... как можно проще.

Какие-либо предложения?

Заметка

Предмет должен находиться между предметами до(newIndex - 1) и newIndex до его удаления.

Ричард Эв
источник
1
Вы должны изменить ответ, который вы отметили. Тот, с которым newIndex--не приводит к поведению, которое вы сказали, что вы хотели.
Мирал
1
@Miral - какой ответ, по вашему мнению, должен быть принят?
Ричард Эв
4
jpierson годов. Это приводит к тому, что объект, который раньше находился в oldIndex до перемещения, будет в newIndex после перемещения. Это наименее удивительное поведение (и это то, что мне было нужно, когда я писал код переупорядочивания drag'n'drop). Конечно, он говорит ObservableCollectionи не является универсальным List<T>, но тривиально просто поменять местами вызовы методов, чтобы получить тот же результат.
Мирал
Запрошенное (и правильно реализованное в этом ответе ) поведение для перемещения элемента между элементами в [newIndex - 1]и [newIndex]не является обратимым. Move(1, 3); Move(3, 1);не возвращает список в исходное состояние. В то же время существует другое поведение предусмотрено ObservableCollectionи упоминается в этом ответе , который является обратимым .
Лайтман

Ответы:

138

Я знаю, что вы сказали «универсальный список», но вы не указали, что вам нужно использовать класс List (T), так что здесь вы видите что-то другое.

Класс ObservableCollection (T) имеет метод Move, который делает именно то, что вы хотите.

public void Move(int oldIndex, int newIndex)

Под ним в основном реализовано так.

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

Итак, как вы можете видеть, метод подкачки, предложенный другими, по сути, является тем, что ObservableCollection делает в своем собственном методе Move.

ОБНОВЛЕНИЕ 2015-12-30: Вы можете увидеть исходный код для методов Move и MoveItem в corefx прямо сейчас, не используя Reflector / ILSpy, поскольку .NET является открытым исходным кодом.

jpierson
источник
28
Интересно, почему это не реализовано и в List <T>, кто-нибудь, чтобы пролить свет на это?
Андреас
В чем разница между универсальным списком и классом List (T)? Я думал, что они были одинаковыми :(
BKSpurgeon
А «общий список» может означать любой тип списка или коллекцию , как структура данных в .NET , которые могут включать в себя ObservableCollection (T) или другие классы , которые могут реализовать Listy интерфейса , такие как IList / ICollection / IEnumerable.
jpierson
6
Может ли кто-нибудь объяснить, почему на самом деле не происходит смещение индекса назначения (если оно больше исходного индекса)?
Влади
@ vladius Я считаю, что идея заключается в том, что указанное значение newIndex должно просто указывать желаемый индекс, которым должен быть элемент после перемещения, и поскольку вставка используется, нет причин для корректировки. Если бы newIndex был позицией относительно исходного индекса, это была бы другая история, я думаю, но это не так.
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
Ваше решение выходит из строя, если в списке есть две копии элемента, причем одна происходит до oldIndex. Вы должны использовать RemoveAt, чтобы убедиться, что вы получите правильный.
Аарон Маэнпаа,
6
Действительно, подлый край дела
Гарри Шатлер
1
@GarryShutler Я не вижу, как индекс может сместиться, если мы удаляем, а затем вставляем один элемент. Уменьшение newIndexфактически нарушает мой тест (см. Мой ответ ниже).
Бен Фостер
1
Примечание: если важна безопасность потоков, все это должно быть внутри lockоператора.
rory.ap
1
Я бы не использовал это, поскольку это сбивает с толку по нескольким причинам. Определение метода Move (oldIndex, newIndex) в списке и вызов Move (15,25), а затем Move (25,15) - это не тождество, а своп. Также Move (15,25) заставляет предмет двигаться в индекс 24, а не в 25, как я ожидал. Кроме того, обмен может быть реализован с помощью temp = item [oldindex]; Пункт [oldindex] = пункт [NewIndex]; Пункт [NewIndex] = темп; который кажется более эффективным на больших массивах. Также Move (0,0) и Move (0,1) будут одинаковыми, что также нечетно. А также Move (0, Count -1) не перемещает элемент до конца.
Wouter
12

Я знаю, что этот вопрос старый, но я адаптировал ЭТОТ ответ кода JavaScript на C #. Надеюсь, поможет

 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

List <T> .Remove () и List <T> .RemoveAt () не возвращают удаляемый элемент.

Поэтому вы должны использовать это:

var item = list[oldIndex];
list.RemoveAt(oldIndex);
list.Insert(newIndex, item);
M4N
источник
5

Вставьте элемент, который в данный момент находится в oldIndexточке, newIndexа затем удалите исходный экземпляр.

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

Вы должны принять во внимание, что индекс элемента, который вы хотите удалить, может измениться из-за вставки.

Megacan
источник
1
Вы должны удалить перед вставкой ... ваш заказ может привести к тому, что список будет выделен.
Джим Балтер
4

Я создал метод расширения для перемещения элементов в списке.

Индекс не должен сдвигаться, если мы перемещаем существующий элемент, так как мы перемещаем элемент в существующую позицию индекса в списке.

Крайний случай, на который @Oliver ссылается ниже (перемещение элемента в конец списка), фактически приведет к сбою тестов, но это сделано специально. Чтобы вставить новый элемент в конец списка, мы просто позвоним List<T>.Add. list.Move(predicate, list.Count) должен потерпеть неудачу, так как эта позиция индекса не существует до движения.

В любом случае, я создал два дополнительных метода расширения, MoveToEndи MoveToBeginning, источник которого можно найти здесь .

/// <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);
        };
    }
}
Бен Фостер
источник
Где Ensure.Argumentопределяется?
Оливер
1
В обычном режиме List<T>вы можете позвонить, Insert(list.Count, element)чтобы поместить что-то в конец списка. Так что вам When_new_index_is_higherследует позвонить, list.Move(x => x == 3, 5)который на самом деле не удается.
Оливер
3
@ Оливер в обычном режиме, List<T>я бы просто позвонил, .Addчтобы вставить новый элемент в конец списка. При перемещении отдельных элементов мы никогда не увеличиваем исходный размер индекса, поскольку удаляем только один элемент и вставляем его заново. Если вы нажмете на ссылку в моем ответе, вы найдете код для Ensure.Argument.
Бен Фостер
Ваше решение ожидает, что целевой индекс является позицией, а не между двумя элементами. Хотя это хорошо работает для некоторых случаев использования, оно не работает для других. Кроме того, ваш переход не поддерживает переход в конец (как отметил Оливер), но нигде в вашем коде вы не указываете это ограничение. Это также нелогично, если у меня есть список с 20 элементами и я хочу переместить элемент 10 в конец, я бы ожидал, что метод Move справится с этим, вместо того, чтобы искать ссылку на объект, удалить объект из списка. и добавьте объект.
Trisped
1
@Trisped на самом деле , если вы читали мой ответ, перемещение элемента в конец / начало списка будет поддерживаться. Вы можете увидеть спецификации здесь . Да, мой код ожидает, что индекс будет действительной (существующей) позицией в списке. Мы перемещаем предметы, а не вставляем их.
Бен Фостер
1

Я бы ожидал либо:

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

... или:

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

... сделал бы уловку, но у меня нет VS на этой машине, чтобы проверить.

Аарон Маенпаа
источник
1
@GarryShutler Это зависит от ситуации. Если ваш интерфейс позволяет пользователю указывать позицию в списке по индексу, он будет сбит с толку, когда скажет элементу 15 перейти на 20, а вместо этого переместится на 19. Если ваш интерфейс позволяет пользователю перетаскивать элемент между другими в списке, то имеет смысл уменьшить, newIndexесли это после oldIndex.
Trisped
-1

Самый простой способ:

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

РЕДАКТИРОВАТЬ

Вопрос не очень ясен ... Так как нас не волнует, куда list[newIndex]идет элемент, я думаю, что самый простой способ сделать это заключается в следующем (с или без метода расширения):

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

Это решение является самым быстрым, потому что оно не включает в себя вставку / удаление списка.

Бруно Конде
источник
4
Это перезапишет элемент в newIndex, а не вставит.
Гарри Шатлер
@ Гарри Разве конечный результат не будет таким же?
Озгур Озцитак
4
Нет, вы в конечном итоге потеряете значение в newIndex, что не произойдет, если вы вставите.
Гарри Шатлер
-2

Есть более простые ребята, просто сделайте это

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

Сделайте то же самое с MoveDown, но измените if для «if (ind! = Concepts.Count ())» и Concepts.Insert (ind + 1, item.ToString ());

Ричард Агирре
источник
-3

Вот как я реализовал метод расширения элемента move. Он неплохо справляется с перемещением до / после и в крайности для элементов.

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
Это дубликат ответа от Франциско.
nivs1978