通用列表 - 在列表中移动项目

200

我有一个通用列表,还有一个旧索引oldIndex和一个新索引newIndex

我想尽可能简单地将位于oldIndex的项移动到newIndex

有什么建议吗?

注意

在被移除之前,该项应最终出现在索引为(newIndex - 1)newIndex的项之间。


2
你应该改变你选择的答案。那个带有newIndex--的选项不会产生你想要的行为。 - Miral
2
@Miral - 你认为哪个答案应该被接受? - Richard Ev
4
jpierson's意味着,在移动后,原来在oldIndex处的对象被移动到newIndex处。这是最不令人惊讶的行为(也是我编写一些拖放重新排序代码时所需要的)。尽管他谈论的是ObservableCollection而不是通用的List<T>,但只需简单地交换方法调用即可获得相同的结果。 - Miral
请求的行为(并且在此答案中正确实现)是将项目移动到[newIndex - 1][newIndex]之间的项目。这种行为不可逆。 Move(1, 3); Move(3, 1);无法将列表返回到初始状态。同时,ObservableCollection提供了不同的行为,并在此答案中提到,该行为是可逆的 - Lightman
10个回答

178
我知道你说过“通用列表”,但你没有明确说明需要使用 List(T) 类,所以这里提供另一种方法。 ObservableCollection(T) 类有一个 Move 方法,可以完全满足你的需求。
public void Move(int oldIndex, int newIndex)

在底层,它基本上是这样实现的。

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

因此,正如其他人建议的“swap”方法所示,ObservableCollection 在其自己的移动方法中实际上执行的是同样的操作。
更新于2015年12月30日:您现在可以在corefx中查看Move和MoveItem方法的源代码,而无需使用Reflector/ILSpy,因为.NET是开源的。链接如下:MoveMoveItem

38
我想知道为什么List<T>上没有实现这个功能,有人能够解释一下吗? - Andreas
2
泛型列表和List(T)类有什么区别?我以为它们是一样的 :( - BenKoshy
2
“通用列表”可以指任何类型的列表或集合,例如.NET中的数据结构,其中可能包括ObservableCollection(T)或其他实现了IList/ICollection/IEnumerable等接口的类。 - jpierson
8
请问为什么如果目标索引大于源索引时,就不进行目标索引的偏移呢?能否解释一下? - Vladius
1
@Jason,这不就是一个链表吗?当你删除和插入时,你并没有移动数据。我认为微软从来没有完善过任何东西...他们只是把它做到足够好,然后转向新产品。看看他们所有的企业解决方案...真是一团糟。 - visc
显示剩余3条评论

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

放到扩展方法中,它们看起来像:

    public static void Move<T>(this List<T> list, int oldIndex, int newIndex)
    {
        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);
    }

    public static void Move<T>(this List<T> list, T item, int newIndex)
    {
        if (item != null)
        {
            var oldIndex = list.IndexOf(item);
            if (oldIndex > -1)
            {
                list.RemoveAt(oldIndex);

                if (newIndex > oldIndex) newIndex--;
                // the actual index could have shifted due to the removal

                list.Insert(newIndex, item);
            }
        }

    }

10
如果列表中有两个项目的副本,并且其中一个在旧索引之前出现,那么你提出的解决方案将会失败。你应该使用RemoveAt来确保得到正确的那个。 - Aaron Maenpaa
1
@GarryShutler 我看不出来如果我们删除然后插入单个项目,索引如何会发生变化。减少 newIndex 实际上会破坏我的测试(请参见下面的答案)。 - Ben Foster
@Trisped,你说得对,这取决于如何计算新索引。我的行为没有考虑原始列表位置 - 如果你Move(0, 2),它将取list[0],删除它,然后插入到list[2]。看起来这不是OP需要的行为。 - Ben Foster
1
注意:如果线程安全很重要,这应该全部在 lock 语句内部。 - rory.ap
2
出于几个原因,我不会使用这个。在列表上定义一个方法Move(oldIndex,newIndex),然后调用Move(15,25),再调用Move(25,15)并不是一个恒等操作,而是交换操作。此外,Move(15,25)使项目移动到索引24而不是我期望的25。此外,交换可以通过temp=item[oldindex]; item[oldindex]=item[newindex]; item[newindex]=temp;来实现,这在大型数组上似乎更有效率。另外,Move(0,0)和Move(0,1)将是相同的,这也很奇怪。而且Move(0, Count -1)也不能将项目移动到末尾。 - Wouter
显示剩余3条评论

16

我知道这个问题很久了,但我将JavaScript代码的响应改编成C#。希望能有所帮助。

public static void Move<T>(this List<T> list, int oldIndex, int newIndex)
{
    // exit if positions 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;
}

2023年工作顺利。 - TheLegendaryCopyCoder

15

List<T>.Remove()和List<T>.RemoveAt()不会返回被删除的元素。

因此,您需要使用以下代码:

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

8

我创建了一个扩展方法来移动列表中的项目。

如果我们正在移动一个现有项目,那么索引不应该移动,因为我们正在将项目移动到列表中的现有索引位置。

@Oliver在下面提到的边缘情况(将项目移动到列表末尾)实际上会导致测试失败,但这是有意设计的。要在列表末尾插入项目,我们只需调用List<T>.Addlist.Move(predicate, list.Count)应该失败,因为在移动之前该索引位置不存在。

无论如何,我创建了两个额外的扩展方法MoveToEndMoveToBeginning,源代码可以在这里找到。

/// <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 定义在哪里? - Oliver
1
在普通的 List<T> 中,您可以调用 Insert(list.Count, element) 将某些内容放置在列表末尾。因此,您的 When_new_index_is_higher 应该调用 list.Move(x => x == 3, 5),但实际上会失败。 - Oliver
3
在正常的List<T>中,我只需调用.Add将一个项插入列表末尾。当移动单个项时,我们从未增加索引的原始大小,因为我们只是删除单个项并重新插入它。如果您点击我回答中的链接,您将找到Ensure.Argument的代码。 - Ben Foster
你的解决方案期望目标索引是一个位置,而不是两个元素之间的位置。虽然这对某些用例很有效,但对其他用例却无效。此外,你的移动操作不支持移动到末尾(正如Oliver所指出的),但在你的代码中没有表明这个限制。这也是违反直觉的,如果我有一个包含20个元素的列表,并且想要将第10个元素移动到末尾,我希望Move方法能处理这个操作,而不是需要找到保存对象引用、从列表中删除对象并添加对象的方式来实现。 - Trisped
1
@Trisped,如果您仔细阅读我的回答,您会发现将项目移动到列表的末尾/开头是受支持的。您可以在此处查看规格[https://github.com/benfoster/Fabrik.Common/blob/master/src/Specs/Fabrik.Common.Specs/ListExtensionSpecs.cs]。 是的,我的代码期望索引是列表中有效(已存在)的位置。我们正在移动项目,而不是插入它们。 - Ben Foster

6
将当前位于oldIndex的项目插入到newIndex位置,然后删除原始实例。
list.Insert(newIndex, list[oldIndex]);
if (newIndex <= oldIndex) ++oldIndex;
list.RemoveAt(oldIndex);

需要注意的是,由于插入操作,你想要移除的项目的索引可能会发生改变。


4
在插入之前应该进行删除...您的操作可能会导致列表执行分配。 - Jim Balter

2
我会期望得到以下之一:
// 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,这会让他们感到困惑。如果您的界面允许用户在列表中的两个其他项目之间拖动一个项目,则如果newIndexoldIndex之后,将其减量化是有意义的。 - 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;
    }

这个解决方案是最快的,因为它不涉及列表的插入/删除。

5
这将覆盖newIndex处的项目,而不是插入。 - Garry Shutler
@Garry 最终的结果不是会一样吗? - Ozgur Ozcitak
5
不,如果你只是移动而不插入,你会失去 newindex 上的值。如果插入,则不会出现这种情况。 - Garry Shutler

-4
这是我实现移动元素扩展方法的方式。它很好地处理了元素的前后和极限移动。
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);
  }
}

3
这是Francisco的答案的副本。 - nivs1978

-4

更简单的方法就是这样做

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接