TOC

This article is currently in the process of being translated into Chinese (~98% done).

Collections:

Lists

C#有一系列用于处理列表的类。这些类实现了IList接口,其中最普遍使用的是通用列表(List),通常被称为List<T>。T 规定了列表(List)中的对象的类型,这有一个好处是编译器会检查并确保你只向列表中添加正确类型的对象——换句话说,List<T>是类型安全的。

List 与ArrayList类十分相似,在C#支持泛型列表前,ArrayList是声明列表的首选。因此 List 可以做很多与数组相同的事情(同样也包括实现了IList接口),但是在很多情况下,List 更简单、更容易操作。例如,创建List时不用定义大小,而是直接创建,并且在添加列表项时,.NET 会自动扩展它以适应列表项的数量。

如前所述,T代表类型,用于指定列表包含的对象的类型。在第一个例子中,我将会展示如何创建一个应该包含字符串(string)的列表:

List<string> listOfStrings = new List<string>();

这将创建一个空的列表,但之后用Add方法向其中添加项是非常简单的:

listOfStrings.Add("a string");

然而,如果你试图往列表中添加非字符串的对象,编译器会立即报错:

listOfStrings.Add(2);
Error   CS1503  Argument 1: cannot convert from 'int' to 'string'

带列表项的初始化

在上面的例子中,我们只是创建了一个列表,然后向其中添加了一项。然而,C#实际上允许你在声明中创建列表的同时向其添加项目,使用的方法称为集合的初始化函数。让我们看看它是如何实现的:

List<string> listOfNames = new List<string>()
{
    "John Doe",
    "Jane Doe",
    "Joe Doe"
};

它的语法很简单:在通常的结尾分号前有一组大括号,其中包括希望从开始就出现在列表中的值的列表。由于这是字符串的列表,我们提供的初始对象当然应该是字符串类型的。当然对于其他类型的列表也是同理,即使使用的是自己定义的类。我将在下一个例子中演示。

处理列表项

有几种方法可以处理通用列表的项,为了展示其中一些方法,我创建了一个更大的示例:

using System;
using System.Collections.Generic;

namespace Lists
{
    class Program
    {
static void Main(string[] args)
{
    List<User> listOfUsers = new List<User>()
    {
new User() { Name = "John Doe", Age = 42 },
new User() { Name = "Jane Doe", Age = 34 },
new User() { Name = "Joe Doe", Age = 8 },
    };

    for(int i = 0; i < listOfUsers.Count; i++)
    {
Console.WriteLine(listOfUsers[i].Name + " is " + listOfUsers[i].Age + " years old");
    }
    Console.ReadKey();
}
    }

    class User
    {
public string Name { get; set; }

public int Age { get; set; }
    }
}

让我们从最下方开始,我们定义一个简单的类来保存用户的信息:只有名字和年龄。回到例子的顶部,我改变了列表,使用了这个用户类而不是单纯的字符串。我使用一个集合初始化函数来填充列表中的用户,注意它的语法和以前一样,只是更复杂一点,因为我们处理的是比字符串更复杂的对象。

在准备好列表之后,我使用一个for循环来运行它。为了知道要做多少次迭代,我使用List的Count属性。在每次循环中,通过列表的索引器,使用方括号的语法(例如listOfUsers[i])来访问相应的用户。一旦找到用户,就输出姓名和年龄。

添加、插入、移除列表项

我们已经尝试过向列表中添加一个项,但还有更多的选择可以做到这点。首先,你可以插入项而不是添加它,区别在于添加(Add)方法总是添加到列表的末尾,而插入(Insert)方法允许你在一个特定的位置插入项。这里有一个例子:

List<string> listOfNames = new List<string>()
{
    "Joe Doe"
};
// Insert at the top (index 0)
listOfNames.Insert(0, "John Doe");
// Insert in the middle (index 1)
listOfNames.Insert(1, "Jane Doe");

开始时列表只有一项,但随后我们又插入了两项,先是在列表的顶部,然后在中间。Insert方法的第一个参数是要插入的项目的索引。不过要注意,如果你想在索引 3 处插入一项,但是列表总数小于3,就会抛出异常!

添加多个项

如同有添加和插入方法来添加单项一般,也有相应的方法来添加和插入多个项。它们被称为AddRange()InsertRange(),并接受任何实现IEnumerable接口的集合类型作为参数,它可以是你想将其添加或插入到当前列表的多个项,例如数组或其他列表。

作为Range方法的例子,让我们做一些有趣的事情,我们把AddRange方法和一个集合初始化函数结合起来,在一句中把几个新的名字添加到现有的列表中:

listOfNames.AddRange(new string[]
    {
"Jenna Doe",
"Another Doe"
    });

只需要临时创建一个字符串数组,并将其追加到前面例子中的名字列表中即可。

移除列表项

当你想从列表中删除一个或几个项时,目前有三种方法供你使用:Remove(), RemoveAt()RemoveAll()

Remove()方法只接受一个参数:你想删除的项。这对于例如字符串或整型的列表来说非常容易,因为你可以简单地写出你想删除的项目。但是如果你有一个复杂的对象列表,你必须先找到那个对象,以便有一个引用可以传递给Remove()方法,让我们稍后处理这个问题。这里有个非常基本的例子,说明如何用Remove()方法删除一个项:

List<string> listOfNames = new List<string>()
{
    "John Doe",
    "Jane Doe",
    "Joe Doe",
    "Another Doe"
};

listOfNames.Remove("Joe Doe");

Remove()方法简单地遍历列表,直到找到你指定要删除的对象的第一个实例,然后将其删除。它只删除一个实例,并且如果你在列表中指定了一个不存在的项,它不会抛出错误。如果该方法能删除一项,则返回true;如果不能,则返回false

RemoveAt()方法利用了通用列表是基于索引的这一事实,允许你根据某项在列表中的索引/位置来移除该项。例如,你可以这样删除列表中的第一项:

listOfNames.RemoveAt(0);

或者像这样删除最后一项:

listOfNames.RemoveAt(listOfNames.Count - 1);

同样,它只删除一个项。这个方法要注意:在提供要删除的项的索引时,如果你使用的索引超出了范围(小于0或大于等于列表项的总数),就会抛出异常。因此,除非你确信你在做什么,否则你可能要把RemoveAt()方法包裹在一个处理异常的try-catch块中(在本教程的其他部分有详细解释)。RemoveAt()方法不返回任何东西,所以你必须检查调用前后列表中的项数量,以判断它是否执行。另一方面,如果你知道你有个列表中的索引,你应该每次都确保这点,才能期望RemoveAt()会成功。

RemoveAll()是删除方法中最复杂的,但肯定也是最强大的。它以一个方法的委托作为参数,然后方法通过返回True或False来决定一项是否应该被删除。这允许你在移除项目时应用你自己的逻辑,也允许你一次移除一个以上的项目。委托将在本教程的其他部分讨论,因为它是一个庞大而复杂的主题,但我仍然希望你能感受到RemoveAll方法有多酷,所以这里有一个例子:

List<string> listOfNames = new List<string>()
{
    "John Doe",
    "Jane Doe",
    "Joe Doe",
    "Another Doe"
};

listOfNames.RemoveAll(name =>
{
    if (name.StartsWith("J"))
return true;
    else
return false;
});

在这个例子中,我们使用了一个匿名方法(在此解释太复杂,但将在另一章中阐述)作为 RemoveAll 方法的参数。我们的匿名方法非常简单:它将被列表中的每个项调用,并且有个name的参数,代表迭代循环中的当前项。方法查看name对应的值,如果它以 "J "开头,就会返回true,否则返回false。RemoveAll()方法根据这个返回值(True或False)来决定每项是否应该被删除。最后,这使得我们的列表只剩下一个Doe成员:Another Doe。

排序列表项

到目前为止,我们只是按照被添加到列表中的顺序来处理的列表中的项。然而,你可能希望以特定的方式对项目进行排序,例如,在姓名列表中按字母顺序排序。List<T>有一个Sort()方法,我们可以用它来实现这点:

List<string> listOfNames = new List<string>()
{
    "John Doe",
    "Jane Doe",
    "Joe Doe",
    "Another Doe"
};
listOfNames.Sort();
foreach (string name in listOfNames)
    Console.WriteLine(name);

正如你从输出结果中看到的,现在列表中的项目已经按字母顺序排序了,如果你想换成降序(从Z到A),只需在执行排序Sort后调用Reverse()方法:

listOfNames.Sort();
listOfNames.Reverse();

所以对一个列表进行排序很容易吗?这主要是因为我们用的是字符串的列表,并且.NET框架完全知道如何比较两个字符串。如果你有个数字列表,.NET当然也会知道如何排序。但是,你可能有个自定义对象的列表(因为List<T>可以包含任何对象),.NET没有机会知道如何比较。这个问题有几种解决方案,例如实现IComparable接口或使用LINQ(我们将在本教程的后面研究这两种方法),但作为一种快速解决方法,我们也可以为Sort()方法提供一个方法来调用,以了解两个项之间如何相互对比,像这样:

using System;
using System.Collections.Generic;

namespace ListSort
{
    class Program
    {
static void Main(string[] args)
{
    List<User> listOfUsers = new List<User>()
    {
new User() { Name = "John Doe", Age = 42 },
new User() { Name = "Jane Doe", Age = 39 },
new User() { Name = "Joe Doe", Age = 13 },
    };
    listOfUsers.Sort(CompareUsers);
    foreach (User user in listOfUsers)
Console.WriteLine(user.Name + ": " + user.Age + " years old");
}

public static int CompareUsers(User user1, User user2)
{
    return user1.Age.CompareTo(user2.Age);
}
    }

    public class User
    {
public string Name { get; set; }
public int Age { get; set; }
    }
}

这给我们的例子增加了不少代码,但实际上并不太复杂。从例子底部开始,创建了一个非常简单的用户类,由名字和年龄组成。在中间,我声明了一个名为CompareUsers()的方法:它接收两个用户作为参数,然后返回一个整数,这将表明一个项是 "小"、"相同 "或 "大"(-1、0或1)。这些值将被Sort()方法用来移动列表项,使项的排序符合我们的要求。在这种情况下,我只是使用Age属性进行比较,基本上实现了按年龄排序的用户列表。

摘要

这篇文章是本教程中较长的一篇,但希望你能学到很多关于列表的知识,因为你编程越多,就越能意识到列表(list )和字典(dictionary)是多么重要。说到字典,我们将在下一篇文章中讨论。


This article has been fully translated into the following languages: Is your preferred language not on the list? Click here to help us translate this article into your language!