11.3 列表

.NET Framework为动态列表提供了泛型类List<T>。这个类实现了IList、ICollection、IEnumerable、IList<T>、ICollection<T>和IEnumerable<T>接口。

下面的例子将Racer类中的成员用作要添加到集合中的元素,以表示一级方程式的一位赛车手。这个类有5个属性:Id、Firstname、Lastname、Country和Wins的次数。在该类的构造函数中,可以传递赛车手的姓名和获胜次数,以设置成员。重写ToString()方法是为了返回赛车手的姓名。Racer类也实现了泛型接口IComparable<T>,为Racer类中的元素排序,还实现了IFormattable接口(代码文件ListSamples/Racer.cs)。

        public class Racer: IComparable<Racer>, IFormattable
        {
          public int Id { get; }
          public string FirstName { get; set; }
          public string LastName { get; set; }
          public string Country { get; set; }
          public int Wins { get; set; }
          public Racer(int id, string firstName, string lastName, string country)
            :this(id, firstName, lastName, country, wins: 0)
          { }
          public Racer(int id, string firstName, string lastName, string country,
                        int wins)
          {
            Id = id;
            FirstName = firstName;
            LastName = lastName;
            Country = country;
            Wins = wins;
          }
          public override string ToString() => $"{FirstName} {LastName}";
          public string ToString(string format, IFormatProvider formatProvider)
          {
            if (format == null) format = "N";
            switch (format.ToUpper())
            {
              case "N": // name
                return ToString();
              case "F": // first name
                return FirstName;
              case "L": // last name
                return LastName;
              case "W": // Wins
                return $"{ToString()}, Wins: {Wins}";
              case "C": // Country
                return $"{ToString()}, Country: {Country}";
              case "A": // All
                return $"{ToString()}, Country: {Country} Wins: {Wins}";
              default:
                throw new FormatException(String.Format(formatProvider,
                                $"Format {format} is not supported"));
            }
          }
          public string ToString(string format) => ToString(format, null);
          public int CompareTo(Racer other)
          {
            int compare = LastName? .CompareTo(other? .LastName) ? ? -1;
            if (compare == 0)
            {
              return FirstName? .CompareTo(other? .FirstName) ? ? -1;
            }
            return compare;
          }
        }

11.3.1 创建列表

调用默认的构造函数,就可以创建列表对象。在泛型类List<T>中,必须为声明为列表的值指定类型。下面的代码说明了如何声明一个包含int的List<T>泛型类和一个包含Racer元素的列表。ArrayList是一个非泛型列表,它可以将任意Object类型作为其元素。

使用默认的构造函数创建一个空列表。元素添加到列表中后,列表的容量就会扩大为可接纳4个元素。如果添加了第5个元素,列表的大小就重新设置为包含8个元素。如果8个元素还不够,列表的大小就重新设置为包含16个元素。每次都会将列表的容量重新设置为原来的2倍。

        var intList = new List<int>();
        var racers = new List<Racer>();

如果列表的容量改变了,整个集合就要重新分配到一个新的内存块中。在List<T>泛型类的实现代码中,使用了一个T类型的数组。通过重新分配内存,创建一个新数组,Array.Copy()方法将旧数组中的元素复制到新数组中。为节省时间,如果事先知道列表中元素的个数,就可以用构造函数定义其容量。下面创建了一个容量为10个元素的集合。如果该容量不足以容纳要添加的元素,就把集合的大小重新设置为包含20或40个元素,每次都是原来的2倍。

        List<int> intList = new List<int>(10);

使用Capacity属性可以获取和设置集合的容量。

        intList.Capacity = 20;

容量与集合中元素的个数不同。集合中的元素个数可以用Count属性读取。当然,容量总是大于或等于元素个数。只要不把元素添加到列表中,元素个数就是0。

        WriteLine(intList.Count);

如果已经将元素添加到列表中,且不希望添加更多的元素,就可以调用TrimExcess()方法,去除不需要的容量。但是,因为重新定位需要时间,所以如果元素个数超过了容量的90%, TrimExcess()方法就什么也不做。

        intList.TrimExcess();

1.集合初始值设定项

还可以使用集合初始值设定项给集合赋值。使用集合初始值设定项,可以在初始化集合时,在花括号中给集合赋值:

          var intList = new List<int>() {1, 2};
          var stringList = new List<string>() {"one", "two"};

注意:集合初始值设定项没有反映在已编译的程序集的IL代码中。编译器会把集合初始值设定项转换成对初始值设定项列表中的每一项调用Add()方法。

2.添加元素

使用Add()方法可以给列表添加元素,如下所示。实例化的泛型类型定义了Add()方法的参数类型:

        var intList = new List<int>();
        intList.Add(1);
        intList.Add(2);
        var stringList = new List<string>();
        stringList.Add("one");
        stringList.Add("two");

把racers变量定义为List<Racer>类型。使用new运算符创建相同类型的一个新对象。因为类List<T>用具体类Racer来实例化,所以现在只有Racer对象可以用Add()方法添加。在下面的示例代码中,创建了5个一级方程式赛车手,并把它们添加到集合中。前3个用集合初始值设定项添加,后两个通过显式调用Add()方法来添加(代码文件ListSamples/Program.cs)。

        var graham = new Racer(7, "Graham", "Hill", "UK", 14);
        var emerson = new Racer(13, "Emerson", "Fittipaldi", "Brazil", 14);
        var mario = new Racer(16, "Mario", "Andretti", "USA", 12);
        var racers = new List<Racer>(20) {graham, emerson, mario};
        racers.Add(new Racer(24, "Michael", "Schumacher", "Germany", 91));
        racers.Add(new Racer(27, "Mika", "Hakkinen", "Finland", 20));

使用List<T>类的AddRange()方法,可以一次给集合添加多个元素。因为AddRange()方法的参数是IEnumerable<T>类型的对象,所以也可以传递一个数组,如下所示:

        racers.AddRange(new Racer[] {
            new Racer(14, "Niki", "Lauda", "Austria", 25),
            new Racer(21, "Alain", "Prost", "France", 51)});

注意:集合初始值设定项只能在声明集合时使用。AddRange()方法则可以在初始化集合后调用。如果在创建集合后动态获取数据,就需要调用AddRange()。

如果在实例化列表时知道集合的元素个数,就也可以将实现IEnumerable<T>类型的任意对象传递给类的构造函数。这非常类似于AddRange()方法:

        var racers = new List<Racer>(
            new Racer[] {
                new Racer(12, "Jochen", "Rindt", "Austria", 6),
                new Racer(22, "Ayrton", "Senna", "Brazil", 41) });

3.插入元素

使用Insert()方法可以在指定位置插入元素:

        racers.Insert(3, new Racer(6, "Phil", "Hill", "USA", 3));

方法InsertRange()提供了插入大量元素的功能,类似于前面的AddRange()方法。

如果索引集大于集合中的元素个数,就抛出ArgumentOutOfRangeException类型的异常。

4.访问元素

实现了IList和IList<T>接口的所有类都提供了一个索引器,所以可以使用索引器,通过传递元素号来访问元素。第一个元素可以用索引值0来访问。指定racers[3],可以访问列表中的第4个元素:

        Racer r1 = racers[3];

可以使用Count属性确定元素个数,再使用for循环遍历集合中的每个元素,并使用索引器访问每一项:

        for (int i = 0; i < racers.Count; i++)
        {
          WriteLine(racers[i]);
        }

注意:可以通过索引访问的集合类有ArrayList、StringCollection和List<T>。

因为List<T>集合类实现了IEnumerable接口,所以也可以使用foreach语句遍历集合中的元素。

        foreach (var r in racers)
        {
          WriteLine(r);
        }

注意:编译器解析foreach语句时,利用了IEnumerable和IEnumerator接口,参见第7章。

5.删除元素

删除元素时,可以利用索引,也可以传递要删除的元素。下面的代码把3传递给RemoveAt()方法,删除第4个元素:

        racers.RemoveAt(3);

也可以直接将Racer对象传送给Remove()方法,来删除这个元素。按索引删除比较快,因为必须在集合中搜索要删除的元素。Remove()方法先在集合中搜索,用IndexOf()方法获取元素的索引,再使用该索引删除元素。IndexOf()方法先检查元素类型是否实现了IEquatable<T>接口。如果是,就调用这个接口的Equals()方法,确定集合中的元素是否等于传递给Equals()方法的元素。如果没有实现这个接口,就使用Object类的Equals()方法比较这些元素。Object类中Equals()方法的默认实现代码对值类型进行按位比较,对引用类型只比较其引用。

注意:第8章介绍了如何重写Equals()方法。

这里从集合中删除了变量graham引用的赛车手。变量graham是前面在填充集合时创建的。因为IEquatable<T>接口和Object.Equals()方法都没有在Racer类中重写,所以不能用要删除元素的相同内容创建一个新对象,再把它传递给Remove()方法。

        if (! racers.Remove(graham))
        {
          WriteLine("object not found in collection");
        }

RemoveRange()方法可以从集合中删除许多元素。它的第一个参数指定了开始删除的元素索引,第二个参数指定了要删除的元素个数。

        int index = 3;
        int count = 5;
        racers.RemoveRange(index, count);

要从集合中删除有指定特性的所有元素,可以使用RemoveAll()方法。这个方法在搜索元素时使用下面将讨论的Predicate<T>参数。要删除集合中的所有元素,可以使用ICollection<T>接口定义的Clear()方法。

6.搜索

有不同的方式在集合中搜索元素。可以获得要查找的元素的索引,或者搜索元素本身。可以使用的方法有IndexOf()、LastIndexOf()、FindIndex()、FindLastIndex()、Find()和FindLast()。如果只检查元素是否存在,List<T>类就提供了Exists()方法。

IndexOf()方法需要将一个对象作为参数,如果在集合中找到该元素,这个方法就返回该元素的索引。如果没有找到该元素,就返回-1。IndexOf()方法使用IEquatable<T>接口来比较元素(代码文件ListSamples/Program.cs)。

 
        int index1 = racers.IndexOf(mario);

使用IndexOf()方法,还可以指定不需要搜索整个集合,但必须指定从哪个索引开始搜索以及比较时要迭代的元素个数。

除了使用IndexOf()方法搜索指定的元素之外,还可以搜索有某个特性的元素,该特性可以用FindIndex()方法来定义。FindIndex()方法需要一个Predicate类型的参数:

        public int FindIndex(Predicate<T> match);

Predicate<T>类型是一个委托,该委托返回一个布尔值,并且需要把类型T作为参数。如果Predicate<T>委托返回true,就表示有一个匹配元素,并且找到了相应的元素。如果它返回false,就表示没有找到元素,搜索将继续。

        public delegate bool Predicate<T>(T obj);

在List<T>类中,把Racer对象作为类型T,所以可以将一个方法(该方法将类型Racer定义为一个参数且返回一个布尔值)的地址传递给FindIndex()方法。查找指定国家的第一个赛车手时,可以创建如下所示的FindCountry类。FindCountryPredicate()方法的签名和返回类型通过Predicate<T>委托定义。Find()方法使用变量country搜索用FindCountry类的构造函数定义的某个国家(代码文件ListSamplesFindCountry.cs)。

        public class FindCountry
        {
          public FindCountry(string country)
          {
            _country = country;
          }
          private string _country;
          public bool FindCountryPredicate(Racer racer) =>
            racer? .Country == _country;
        }

使用FindIndex()方法可以创建FindCountry类的一个新实例,把表示一个国家的字符串传递给构造函数,再传递Find()方法的地址。在下面的示例中,FindIndex()方法成功完成后,index2就包含集合中赛车手的Country属性设置为Finland的第一项的索引(代码文件ListSamples/Program.cs)。

        int index2 = racers.FindIndex(new FindCountry("Finland").
                                          FindCountryPredicate);

除了用处理程序方法创建类之外,还可以在这里创建lambda表达式。结果与前面完全相同。现在lambda表达式定义了实现代码,来搜索Country属性设置为Finland的元素。

        int index3 = racers.FindIndex(r => r.Country == "Finland");

与IndexOf()方法类似,使用FindIndex()方法也可以指定搜索开始的索引和要遍历的元素个数。为了从集合中的最后一个元素开始向前搜索某个索引,可以使用FindLastIndex()方法。

FindIndex()方法返回所查找元素的索引。除了获得索引之外,还可以直接获得集合中的元素。Find()方法需要一个Predicate<T>类型的参数,这与FindIndex()方法类似。下面的Find()方法搜索列表中FirstName属性设置为Niki的第一个赛车手。当然,也可以实现FindLast()方法,查找与Predicate<T>类型匹配的最后一项。

        Racer racer = racers.Find(r => r.FirstName == "Niki");

要获得与Predicate<T>类型匹配的所有项,而不是一项,可以使用FindAll()方法。FindAll()方法使用的Predicate<T>委托与Find()和FindIndex()方法相同。FindAll()方法在找到第一项后,不会停止搜索,而是继续迭代集合中的每一项,并返回Predicate<T>类型是true的所有项。

这里调用了FindAll()方法,返回Wins属性设置为大于20的整数的所有racer项。从bigWinners列表中引用所有赢得超过20场比赛的赛车手。

        List<Racer> bigWinners = racers.FindAll(r => r.Wins > 20);

用foreach语句遍历bigWinners变量,结果如下:

        foreach (Racer r in bigWinners)
        {
          WriteLine($"{r:A}");
        }
        Michael Schumacher, Germany Wins: 91
        Niki Lauda, Austria Wins: 25
        Alain Prost, France Wins: 51

这个结果没有排序,但这是下一步要做的工作。

注意:格式修饰符和IFormattable接口参见第10章。

7.排序

List<T>类可以使用Sort()方法对元素排序。Sort()方法使用快速排序算法,比较所有的元素,直到整个列表排好序为止。

Sort()方法使用了几个重载的方法。可以传递给它的参数有泛型委托Comparison<T>和泛型接口IComparer<T>,以及一个范围值和泛型接口IComparer<T>。

        public void List<T>.Sort();
        public void List<T>.Sort(Comparison<T>);
        public void List<T>.Sort(IComparer<T>);
        public void List<T>.Sort(Int32, Int32, IComparer<T>);

只有集合中的元素实现了IComparable接口,才能使用不带参数的Sort()方法。

Racer类实现了IComparable<T>接口,可以按姓氏对赛车手排序:

        racers.Sort();

如果需要按照元素类型不默认支持的方式排序,就应使用其他技术,例如,传递一个实现了IComparer<T>接口的对象。

RacerComparer类为Racer类型实现了接口IComparer<T>。这个类允许按名字、姓氏、国籍或获胜次数排序。排序的种类用内部枚举类型CompareType定义。CompareType枚举类型用RacerComparer类的构造函数设置。IComparer<Racer>接口定义了排序所需的Compare()方法。在这个方法的实现代码中,使用了string和int类型的CompareTo()方法(代码文件ListSamples/Racer-Comparer.cs)。

        public class RacerComparer : IComparer<Racer>
        {
          public enum CompareType
          {
            FirstName,
            LastName,
            Country,
            Wins
          }
          private CompareType _compareType;
          public RacerComparer(CompareType compareType)
          {
            _compareType = compareType;
          }
          public int Compare(Racer x, Racer y)
          {
            if (x == null && y == null) return 0;
            if (x == null) return -1;
            if (y == null) return 1;
            int result;
            switch (_compareType)
            {
              case CompareType.FirstName:
                return string.Compare(x.FirstName, y.FirstName);
              case CompareType.LastName:
                return string.Compare(x.LastName, y.LastName);
              case CompareType.Country:
                result = string.Compare(x.Country, y.Country);
                if (result == 0)
                  return string.Compare(x.LastName, y.LastName);
                else
                  return result;
              case CompareType.Wins:
                return x.Wins.CompareTo(y.Wins);
              default:
                throw new ArgumentException("Invalid Compare Type");
            }
          }
        }

注意:如果传递给Compare方法的两个元素的顺序相同,该方法则返回0。如果返回值小于0,说明第一个参数小于第二个参数;如果返回值大于0,则第一个参数大于第二个参数。传递null作为参数时,Compare方法并不会抛出一个NullReferenceException异常。相反,因为null的位置在其他任何元素之前,所以如果第一个参数为null,该方法返回-1,如果第二个参数为null,则返回+1。

现在,可以对RacerComparer类的一个实例使用Sort()方法。传递枚举RacerComparer. CompareType.Country,按属性Country对集合排序:

        racers.Sort(new RacerComparer(RacerComparer.CompareType.Country));

排序的另一种方式是使用重载的Sort()方法,该方法需要一个Comparison<T>委托:

        public void List<T>.Sort(Comparison<T>);

Comparison<T>是一个方法的委托,该方法有两个T类型的参数,返回类型为int。如果参数值相等,该方法就必须返回0。如果第一个参数比第二个小,它就必须返回一个小于0的值;否则,必须返回一个大于0的值。

        public delegate int Comparison<T>(T x, T y);

现在可以把一个lambda表达式传递给Sort()方法,按获胜次数排序。两个参数的类型是Racer,在其实现代码中,使用int类型的CompareTo()方法比较Wins属性。在实现代码中,因为以逆序方式使用r2和r1,所以获胜次数以降序方式排序。调用方法之后,完整的赛车手列表就按赛车手的获胜次数排序。

        racers.Sort((r1, r2) => r2.Wins.CompareTo(r1.Wins));

也可以调用Reverse()方法,逆转整个集合的顺序。

11.3.2 只读集合

创建集合后,它们就是可读写的,否则就不能给它们填充值了。但是,在填充完集合后,可以创建只读集合。List<T>集合的AsReadOnly()方法返回ReadOnlyCollection<T>类型的对象。ReadOnlyCollection<T>类实现的接口与List<T>集合相同,但所有修改集合的方法和属性都抛出NotSupportedException异常。除了List<T>的接口之外,ReadOnlyCollection<T>还实现了IReadOnlyCollection<T>和IReadOnlyList<T>接口。因为这些接口的成员,集合不能修改。