C#开发训练营

第10课:数组与集合(1)

数组和集合是处理一组数据的强大工具,接下来两课将讨论C#和.NET Framework中关于数组和集合处理的常用资源,主要包括Array类、List、Dictionary和ConcurrentDictionary类等;此外,还封装了tPair和tPairList类,用于本书代码库中的数据传递操作。

C#数组和Array类

C#中的数组类型使用元素类型和一对方括号定义,如下面的代码定义了一个元素为int类型的数组。

C#
using System;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        int[] arrInt = new int[5];
        for(int i = 0; i < arrInt.Length; i++)
        {
            tWeb.WriteLine(arrInt[i]);
        }
    }
}

代码中定义了包含5个int元素的数组arrInt,但没有给元素赋值,此时,元素的默认值就是相应类型的默认值,本例中的int类型默认值为0,所以执行后页面会显示5个0。

访问数组的元素时使用了从0开始的索引值,即第一个元素索引为0、第二个元素索引为1、以此类推。

定义数组时,也可以直接指定数组的元素,如下面的代码。

C#
int[] seq = new int[] { 0, 1, 1, 2, 3, 5 };

这里,数组seq包含6个元素,分别是0、1、1、2、3、5,也就是斐波那契数列的前6个数字。

访问数组的全部元素时,除了使用for循环和数值索引,也可以使用foreach语句,通过遍历实现,如下面的代码。

C#
using System;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        int[] seq = new int[] { 0, 1, 1, 2, 3, 5 };
        foreach(int n in seq)
        {
            tWeb.WriteLine(n);
        }
    }
}

应用开发中,通过数组的数值索引访问元素的特性,还可以简化一些开发工作,如使用表驱动法表示一系列相关数据及描述。在源代码中的tLunar类中有一系列相关应用,这里简单了解基本的使用方法。在农历信息处理中,十二地支的数据从1到12,对应了十二时辰,也对应了十二生肖,获取生肖名称时,可以使用if或switch语句,如下面的代码。

C#
int dizhi = 1;
string result = "";
if(dizhi==1) result="鼠";
else if (dizhi==2) result="牛";
else if (dizhi==3) result="虎";
else if (dizhi==4) result="兔";
else if (dizhi==5) result="龙";
else if (dizhi==6) result="蛇";
else if (dizhi==7) result="马";
else if (dizhi==8) result="羊";
else if (dizhi==9) result="猴";
else if (dizhi==10) result="鸡";
else if (dizhi==11) result="狗";
else if (dizhi==12) result="猪";	

C#
int dizhi = 1;
string result = "";
switch(dizhi)
{
case 1: result="鼠";break;
case 2: result="牛"; break;
case 3: result="虎"; break;
case 4: result="兔"; break;
case 5: result="龙"; break;
case 6: result="蛇"; break;
case 7: result="马"; break;
case 8: result="羊"; break;
case 9: result="猴"; break;
case 10: result="鸡"; break;
case 11: result="狗"; break;
case 12: result="猪"; break;
}

可以看到,地支的数据是连续的整数,与数组的索引值比较接近,可以直接定义一个数组,并将数据作为元素的索引值,如下面的代码。

C#
using System;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string[] shengxiaoNames = {"",
            "鼠","牛","虎","兔","龙","蛇","马","羊","猴","鸡","狗","猪" };
        int shengxiao = 2;
        string result = shengxiaoNames[shengxiao];
        tWeb.WriteLine(result);
    }
}

由于地支数据是从1开始的,所以在索引0的位置使用了一个空字符串用于占位,然后,从索引1开始保存生肖名称,这样就可以直接使用生肖的数据作为索引直接获取数组元素的生肖名称。

使用for语句访问数组元素时,我们通过了数组的Length属性作为循环控制变量的上限(不包含);那么,这个属性是怎么来的呢?实际上,C#中的数组都会映射为System. Array类的子类,这样就可以使用Array类的成员进行数组操作,Length就是其中的一个属性;获取数组中元素的数量时,还可以使用LongLength属性,它定义为long类型,可以元素数量过多时使用。

需要注意的是,Length和LongLength属性获取的是数组中所有维度元素的总数量,在处理多维数组时,应使用GetLength()或GetLongLength()方法获取指定维度的元素数量。如下面的代码。

C#
using System;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        int[,] arr = new int[,] { { 1, 2, 3 }, { 4, 5, 6 } };
        tWeb.WriteLine(arr.GetLength(0));
        tWeb.WriteLine(arr.GetLength(1));
    }
}

代码中定义了一个二维数组,类型为int[,],如果是三维数组则定义为int[,,],以此类推。第一维(0维)中包含了两个元素,元素类型为int数组,第一个输出会显示2;第二维分别包含了三个元素,包含三个int数据,第二个输出会显示3。本例中,使用arr[1,1]读取的数据为5。

获取和设置多维数组元素的数据时,除了在一对方括号中使用对应的索引值,还可以分别使用GetValue()和SetValue()方法,通用的版本定义如下。

C#
public object? GetValue (params int[] indices);
public void SetValue (object? value, params int[] indices);

其中,indeces参数定义为参数数组,需要按维度顺序指定元素的索引值;SetValue()方法中的value参数指定元素的数据。下面的代码演示了这两个方法的使用。

C#
using System;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        int[,] arr = new int[,] { { 1, 2, 3 }, { 4, 5, 6 } };
        arr.SetValue(99, 1, 1);
        tWeb.WriteLine(arr.GetValue(1, 1));
    }
}

下面讨论一些Array类中的常用方法。

Clear()方法功能是数组中的指定范围的元素设置为元素类型的默认值,定义如下。

C#
public static void Clear (Array array, int index, int length);

下面的代码演示了Clear()方法的应用。

C#
using System;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        int[] arr = { 1, 2, 3, 4, 5, 6 };
        Array.Clear(arr, 1, 3);
        tWeb.WriteLine(string.Join(",", arr));
    }
}

代码中,会将数组arr第二个元素(索引1)开始的3个元素设置为int类型的默认值0,代码执行结果如图1。

图1

ConvertAll()方法的功能是转换元素类型并返回由转换类型后的元素组成的新数组,方法定义如下。

C#
public static TOutput[] ConvertAll<TInput,TOutput> (TInput[] array, 
    Converter<TInput,TOutput> converter);

下面的代码演示了ConvertAll()方法的应用。

C#
using System;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        double[] arrDbl = { 1.0, 2.1, 3.3 };
        int[] arrInt = Array.ConvertAll(arrDbl,
            (double n) => (int)n);
        tWeb.WriteLine(string.Join(",", arrInt));
    }
}

ConvertAll()方法方法中,参数一指定源数组对象。参数二是Converter委托类型,参数表示源数组的单个元素,返回值是新的类型的元素。代码执行会显示1,2,3。

Find()方法的功能是按条件查找元素并返回满足条件的第一个元素,如果没有找到满足条件的元素则元素类型的默认值,方法定义如下。

C#
public static T? Find<T> (T[] array, Predicate<T> match);

方法中,参数一指定源数组对象,参数二需要一个Predicate委托类型的实现,委托定义如下。

C#
public delegate bool Predicate<in T>(T obj);

委托的实现中需要指定匹配条件,当元素满足条件时返回true,否则返回false。Array.Find()方法会返回满足条件的第一个元素,如下面的代码。

C#
using System;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        int[] arrInt = { 1, 2, 3, 4, 5, 6 };
        int result = Array.Find(arrInt, (int n) => { return n % 2 == 0; });
        tWeb.WriteLine(result);
    }
}

代码中,Array.Find()方法的参数二实现的匹配条件是判断元素是否为偶数,运行代码会显示2,即arrInt数组中的第一个偶数。

根据匹配条件查询元素的相关方法还包括FindLast()方法和FindAll()方法,其中,FindLast()方法会返回满足条件的最后一个元素,FindAll()方法则返回满足条件的所有元素组成的新元素,下面的代码演示了这两个方法的应用。

C#
using System;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        int[] arrInt = { 1, 2, 3, 4, 5, 6 };
        int lastResult = Array.FindLast(arrInt, IsEven);
        int[] allResult = Array.FindAll(arrInt, IsEven);
        tWeb.WriteLine(lastResult);
        tWeb.WriteLine(string.Join(",", allResult));
    }

    // 判断是否为偶数
    protected bool IsEven(int n)
    {
        return n % 2 == 0;
    }
}

代码中使用IsEven()方法判断int数据是否为偶数,然后在Array类的FindLast()和FindAll()方法中的第二个参数使用此方法作为Predicate委托的实现;代码执行结果如图2。

图2

FindIndex()方法的功能是查找匹配条件的元素并返回第一个满足条件元素的索引,没有找到时返回-1;FindLastIndex()方法同样根据条件查找元素,但它会返回最后一个匹配元素的索引,没有找到匹配元素时同样返回-1。下面的代码演示了这两个方法的应用。

C#
using System;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        int[] arrInt = { 1, 2, 3, 4, 5, 6 };
        int firstIndex = Array.FindIndex(arrInt, IsEven);
        int lastIndex = Array.FindLastIndex(arrInt, IsEven);
        tWeb.WriteLine(firstIndex);
        tWeb.WriteLine(lastIndex);
    }

    // 判断是否为偶数
    protected bool IsEven(int n)
    {
        return n % 2 == 0;
    }
}

执行代码会显示1和5。

FindIndex()和FindLastIndex()方法还有一些重载版本,可以指定查询范围,其中,使用参数二指定开始查询的位置索引;参数三指定搜索的元素数量,不指定时查询从参数二指定位置开始的所有元素。下面的代码演示了在FindIndex()方法中指定查询范围的操作。

C#
using System;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        int[] arrInt = { 1, 2, 3, 4, 5, 6 };
        int firstIndex = Array.FindIndex(arrInt, 2, IsEven);
        int lastIndex = Array.FindIndex(arrInt, 3, 2, IsEven);
        tWeb.WriteLine(firstIndex);
        tWeb.WriteLine(lastIndex);
    }

    // 判断是否为偶数
    protected bool IsEven(int n)
    {
        return n % 2 == 0;
    }
}

执行代码会显示两个3。

IndexOf()和LastIndexOf()方法用于返回指定元素的索引值,其中,IndexOf()方法返回第一个匹配的元素索引值,LastIndexOf()方法则返回最后一个匹配的元素索引值,没有找到指定的元素时,两个方法都会返回-1。下面的代码演示了这两个方法的应用。

C#
using System;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        int[] arrInt = { 1, 2, 3, 1, 5, 6 };
        tWeb.WriteLine(Array.IndexOf(arrInt, 1));  // 0
        tWeb.WriteLine(Array.LastIndexOf(arrInt, 1));  // 3
        tWeb.WriteLine(Array.IndexOf(arrInt, 9));  // -1
    }
}

执行代码会显示0、3、-1。

Resize()方法的功能是重新设置数组元素的数量,方法定义如下。

C#
public static void Resize<T> (ref T[]? array, int newSize);

当新的数量大于原数组元素数量时,多出的元素会使用元素类型的默认值;当新的数量小于原数组元素数量时,会直接截断。下面的代码演示了Resize()方法的应用。

C#
using System;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        int[] arrInt = { 1, 2, 3 };
        Array.Resize(ref arrInt, 6);
        tWeb.WriteLine(string.Join(",", arrInt));
        Array.Resize(ref arrInt, 2);
        tWeb.WriteLine(string.Join(",", arrInt));
    }
}

代码执行结果如图3。

图3

Reverse()方法,反转一维数组所有元素或部分元素的顺序,如下面的代码。

C#
using System;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        int[] arrInt = { 1, 2, 3, 1, 5, 6 };
        Array.Reverse(arrInt);
        tWeb.WriteLine(string.Join(",", arrInt));
    }
}

代码执行结果如图4。

图4

指定反转范围时,可以通过第二个参数指定开始反转的位置索引,第三个参数指定反转的元素数量,不指定时反转从参数二指定位置开始的所有元素,如下面的代码。

C#
using System;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        int[] arrInt = { 1, 2, 3, 1, 5, 6 };
        tWeb.WriteLine(string.Join(",", arrInt));
        Array.Reverse(arrInt, 2, 3);
        tWeb.WriteLine(string.Join(",", arrInt));
    }
}

代码执行结果如图5。

图5

Sort()方法可以对数组成员进行快速排序,下面的代码显示了Sort()方法的基本应用。

C#
using System;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        int[] arrInt = { 1, 2, 3, 1, 5, 6 };
        tWeb.WriteLine(string.Join(",", arrInt));
        Array.Sort(arrInt);
        tWeb.WriteLine(string.Join(",", arrInt));
    }
}

代码执行结果如图6。

图6

在Sort()方法中,还可以指定排序的范围和排序规则。下面的代码演示了如何将数组中的部分元素进行排序。

C#
using System;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        int[] arrInt = { 1, 2, 3, 1, 5, 6 };
        tWeb.WriteLine(string.Join(",", arrInt));
        Array.Sort(arrInt, 1, 3);
        tWeb.WriteLine(string.Join(",", arrInt));
    }
}

代码中,从数组中第二个元素(索引1)开始的3个元素进行排序,运行结果如图7。

图7

当预到特殊情况时,还可以自定义排序规则,此时,可以使用Sort()方法的如下版本。

C#
public static void Sort<T> (T[] array, Comparison<T> comparison);

这里,第二参数使用了Comparison委托类型,定义如下。

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

Comparison委托的实现中,当x小于y返回负数,x等于y返回0,x大于y返回正数。

下面的代码,我们先来看字符串中的数字通过默认方式排序的结果。

C#
using System;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string[] arrStr = { "1", "2", "3", "11", "5" };
        tWeb.WriteLine(string.Join(",", arrStr));
        Array.Sort(arrStr);
        tWeb.WriteLine(string.Join(",", arrStr));
    }
}

代码执行结果如图8。

图8

示例中,由于是按文本进行排序,11排到了2的前面;下面的代码,我们通过实现Comparison委托改变元素比较的规则。

C#
using System;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string[] arrStr = { "1", "2", "3", "11", "5" };
        tWeb.WriteLine(string.Join(",", arrStr));
        Array.Sort(arrStr,(string x,string y) => {
            int a = tInt.Parse(x);
            int b = tInt.Parse(y);
            return a - b;
        });
        tWeb.WriteLine(string.Join(",", arrStr));
    }
}

本例,在Comparison委托的实现中,先将string类型的元素强制转换为int类型,请注意转换规则,如果字符串不能成功转换为int数据,则tInt.Parse()方法返回0;转换完成后,会返回整数相减的差以确定比较规则。代码执行结果如图9。

图9

TrueForAll()方法的功能是判断数组的所有元素是否都匹配指定的条件,方法宝义如下。

C#
public static bool TrueForAll<T> (T[] array, Predicate<T> match);

如下面的代码,其功能是判断数组中的整数是否都是偶数。

C#
using System;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        int[] arrInt = { 1, 2, 3, 4, 5, 6 };
        bool result = Array.TrueForAll(arrInt, 
            (int n) => { return n % 2 == 0; });
        if (result) tWeb.WriteLine("数组元素均为偶数");
        else tWeb.WriteLine("数组元素不完全是偶数");
    }
}

List泛型类

在.NET Framework平台支持泛型之前,可以使用ArrayList对象来处理动态数组,数组元素的类型为object,在大量元素的处理过程中,数据类型的转换工作是无法避免的,这也是影响代码性能的主要问题。

使用List泛型类,对于不同的元素类型,可以在编程时使用相同的代码,在运行时绑定集合中的元素类型,无论是开发效率和运行效率都得到了极大的提高。定义一下List对象时,需要指定元素的类型,格式如下。

C#
List<元素类型> lst = new List<元素类型>();

添加List对象的元素时,可以使用Add()和AddRange()方法,如下面的代码,演示了List泛型类的基本应用。

C#
using System;
using System.Collections.Generic;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        List<string> lst = new List<string>();
        lst.Add("Tom");
        lst.Add("Jerry");
        string[] arr = new string[] {"张三","李四"};
        lst.AddRange(arr);
        tWeb.WriteLine(lst.Count);
    }
}

代码中,首先使用Add()方法向lst对象添加了两个元素,然后定义了一个string数组,包含两个成员,并使用AddRange()方法将数组元素全部添加到lst对象;最后,使用lst对象的Count属性显示了元素数据,页面会显示4。

List泛型类可以对元素进行动态操作,下面了解一些常用的操作方法。

  • Clear()方法,删除所有元素。
  • Contains(item)方法,判断列表中是否存在item元素。元素存在时返回true,否则返回false。
  • IndexOf(item)方法,在列表中查找元素item,并返回第一次出现的索引;还可以通过参数二指定开始查找的索引位置,默认为0;参数三指定查询的元素数量,默认为指定位置开始的所有元素。查询元素最后一次出现的位置索引时可以使用LastIndexOf(item)方法。没有找到指定的元素时,方法会返回-1。
  • Insert(index,item)方法,在索引index位置插入元素item。
  • InsertRange(index,items)方法,在索引index位置插入一系列的元素,如一个数组或一个集合等。
  • Remove(item)方法,删除指定的元素,操作成功返回true,否则返回false。
  • RemoveAt(index)方法,删除索引index位置的元素。
  • RemoveRange(index,count)方法,删除从index开始的count个元素。
  • Reverse()方法,将列表中的元素顺序反向排列。如果只需要反向排列其中一部分元素,可以使用Reverse(index,count)方法,将index索引位置开始的count个元素顺序进行反向排列。
  • Sort()方法,使用默认的快速排列算法对列表中的元素进行排序。
  • ToArray()方法,将列表对象转换为数组,如List<string>对象会转换为string[]数组类型。

在List泛型类中还定义一些方法,这法方法的具体操作可以使用委托类型实现,首先来ConvertAll()方法的应用,它的功能是转换所有元素的类型,然后返回相应的List<T>对象。方法的参数需要一个委托方法,定义如下:

C#
public delegate TOutput Converter<in TInput, out TOutput>(TInput input)

如下面的代码就是将List<double>转换为List<long>对象。

C#
using System;
using System.Collections.Generic;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        List<double> lstDbl = new List<double>();
        lstDbl.Add(1.2);
        lstDbl.Add(2.5);
        lstDbl.Add(1.9);
        List<long> lstLng = lstDbl.ConvertAll(
            new Converter<double, long>((n) =>
            {
                return (long)Math.Floor(n);
            }));
        //
        for (int i = 0; i < lstLng.Count; i++)
            tWeb.WriteLine(lstLng[i]);
    }
}

执行代码会显示1、2、1。在ConvertAll()的参数中,创建了一个委托对象,类型为Converter<double,long>,实际的功能是将参数中的n由double类型转换为long类型,这里,我们使用了Math.Floor()方法对数据进行向下取整,也就是取小于浮点数的最大整数。

对于List对象中的元素,如果需要挑选满足的条件的元素,可以使用FindAll()方法,它的参数委托类型定义如下。

C#
public delegate bool Predicate<in T>(T obj)

委托方法中,对于元素obj的操作结果为true时,将添加到新的列表对象中,如下面的代码会返回List<int>对象中的所有偶数。

C#
using System;
using System.Collections.Generic;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        List<int> lst1 = new List<int>();
        lst1.Add(1);
        lst1.Add(2);
        lst1.Add(5);
        lst1.Add(6);
        lst1.Add(8);
        List<int> lst2 = lst1.FindAll(
            new Predicate<int>((n) =>
            {
                return n % 2 == 0;
            }));
        //
        for (int i = 0; i < lst2.Count; i++)
            tWeb.WriteLine(lst2[i]);
    }
}

执行代码会显示2、6、8。

List对象中更多通过委托操作的方法应用可以参考以上示例。