C#泛型、List和Dictionary泛型类

本文会讨论泛型接口、泛型类、泛型方法、泛型约束,以及两种常用的泛型集合类型List<T>和Dictionary<K, V>。

泛型方法

泛型(generic)是指通过相同的代码处理不同类型的数据,如交换两个对象(变量)的值,下面的代码(cfx/Comm.cs)演示了交换数据的泛型方法。

C#
using System;

namespace cfx
{
    public static class Comm
    {
        //
        public static void Swap<T>(ref T x, ref T y)
        {
            T tmp = x;
            x = y;
            y = tmp;
        }
    }
}

与普通方法不同的是,泛型方法会在方法名后面使用一对尖括号指定类型标识,多个类型时使用逗号(,)分隔。代码中定义的Swap()方法只使用一个泛型类型,类型标识为T,在参数、返回值和方法体中可以使用此类型标识定义参数和变量(对象)。Swap()方法的两个参数都定义为按引用传递,方法体中交换了两个参数的数据。下面的代码(Program.cs),在Main()方法中测试Swap()方法的使用。

C#
using System;
using cfx;

namespace csfx_demo
{
    class Program
    {
        static void Main(string[] args)
        {
            int x = 10, y = 99;
            Console.WriteLine("x={0}, y={1}", x, y);
            Comm.Swap(ref x, ref y);
            Console.WriteLine("x={0}, y={1}", x, y);
        }
    }
}

本例会使用Comm.Swap()方法交换x和y的值,执行结果如下图所示。

使用泛型方法

这里定义的Comm.Swap()泛型方法可交换两个相同类型的变量或对象的数据,代码中可以修改变量x和y的类型和数据,并观察代码执行结果。

泛型接口和泛型类

泛型接口需要泛型类来实现,与泛型方法的定义类似,在泛型接口和泛型类的名称后需要使用一对尖括号定义泛型的类型标识,多个类型标识使用逗号分隔。下面的代码演示了泛型接口和泛型类的基本应用。

C#
using System;

namespace csfx_demo
{
    //
    interface I1<T>
    {
        T Value { get; set; }
        string ToString();
    }
    //
    class C1<T> : I1<T>
    {
        public T Value { get; set; }
        //
        new public string ToString()
        {
            return Value.ToString();
        }
    }
    //
    class Program
    {
        static void Main(string[] args)
        {
            C1<string> s = new C1<string>();
            s.Value = "abc";
            Console.WriteLine(s.Value);
            //
            C1<int> n = new C1<int>();
            n.Value = 99;
            Console.WriteLine(n.ToString());
        }
    }
}

代码中,首先定义了泛型接口I1,使用了一个泛型类型标识T,接口中定义了Value属性和ToString()方法,其中,Value属性的类型为T,ToString()方法的返回值为string类型。接下来使用C1泛型类实现了I1接口,其中的ToString()使用new关键字覆盖了Object类中的ToString()方法,并返回Value属性的ToString()方法返回值。Main()方法中,首先使用C1<string>类型,即Value属性为string类型,然后使用C1<int>类型,即Value属性为int类型。执行代码会显示abc和99。

泛型约束

普通的泛型类型中,类型标识可以表示任何类型,但有时,可能需要对泛型的类型范围做一些限制,此时就需要使用泛型约束。定义泛型类型的约束时,应在泛型标识后使用where关键字指定泛型类型应实现的接口,如下面的代码。

C#
using System;

namespace csfx_demo
{
    //
    interface IFactory
    {
        string GetValue();
    }
    //
    class Factory1 : IFactory
    {
        public string GetValue()
        {
            return "工作A";
        }
    }
    //
    class Factory2 : IFactory
    {
        public string GetValue()
        {
            return "工作B";
        }
    }
    //
    class C1<T>  where T: IFactory
    {
        public T Factory { get; set; }
        //
        new public string ToString()
        {
            return Factory.GetValue();
        }
    }
    //
    class Program
    {
        static void Main(string[] args)
        {
            C1<Factory1> c1 = new C1<Factory1>();
            c1.Factory = new Factory1();
            Console.WriteLine(c1.ToString());
            //
            C1<Factory2> c2 = new C1<Factory2>();
            c2.Factory = new Factory2();
            Console.WriteLine(c2.ToString());
        }
    }
}

代码中,首先定义了IFactory接口,其中只包含GetValue()方法,然后,使用Factory1和Factory2两个类实现了此接口。接下来的C1类定义为泛型类,在约束中指定泛型类型必须实现IFactory接口,类中定义了Factory属性,在ToString()方法中则调用了IFactory接口中的GetValue()方法返回信息。Main()方法中,分别定义了C1<Factory1>和C1<Factory2>类型对象c1和c2,两个对象的Factory属性分别设置为Factory1和Factory2类型,然后显示对象的ToString()方法返回的信息,执行结果会显示“工作A”和“工作B”。

下面了解两种常用的泛型集合类型,List<T>和Dictionary<K, V>泛型类。

List泛型类

前面的文章中讨论过数组的应用,早期的.NET Framework类库中还定义了ArrayList类动态处理数组,其元素为object类型;当.NET Framework环境引入泛型支持后,使用List<T>泛型类可以更好地处理数据集合。下面的代码,先来看List<T>类的基本应用。

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

namespace csfx_demo
{
    class Program
    {
        static void Main(string[] args)
        {
            List<int> lst = new List<int>();
            lst.Add(1);
            lst.AddRange(new int[] { 1, 2, 3, 8 });
            lst.Insert(4, 5);
            Console.WriteLine(lst[2]); // 2
            Console.WriteLine(string.Join(",", lst));
        }
    }
}

代码中,首先创建了List<int>对象,其中,列表元素的类型为int,接下来使用三个方法添加列表元素,分别是:

  • Add()方法,将一个元素添加到列表的最后。
  • AddRange()方法,将一个集合的元素依次添加到列表中。代码中,将一个int[]数组成员添加到列表中。
  • Insert()方法,将一个元素插入到指定的位置,其中,参数一指定插入位置的索引,列表元素的索引同样从0开始,代码中的4表示第5个元素;参数二指定插入的元素。

接下来,第一个输出中使用索引显示第3个元素(索引2)。第二个输出使用string.Join()方法将列表元素转换为使用逗号连接的字符串,显示结果为“1,1,2,3,5,8”。

下面是List<T>泛型类的一些常用成员。

  • Count属性,返回列表中元素的数量。
  • Clear()方法,清除所有元素。
  • Contains(e)方法,查找指定的元素,存在时返回true,否则返回false。
  • GetRange(index, count)方法,返回指定范围的元素组成的新列表。参数一指定开始的位置索引,参数二指定读取的元素数量。
  • IndexOf(e)和LastIndexOf(e)方法,查找元素第一次和最后一次出现的位置,并返回索引值,没有找到时返回-1。可以使用第二和第三个参数指定开始查找的位置和查找的元素数量。
  • InsertRange(index, count)方法,将一个集合的元素插入到指定的位置。参数一指定插入的位置索引,参数二指定集合对象。
  • Remove(e)方法,删除第一个匹配的元素。
  • RemoveAt(index)方法,删除指定索引位置的元素。
  • RemoveRange(index, count)方法,删除指定索引范围的元素,参数一指定开始删除的索引位置,参数二指定删除的元素数量。
  • Reverse()方法,将列表元素反向排列。可以使用第二和第三个参数指定反向排列的范围,分别指定开始位置索引和元素数量。
  • ToArray()方法,将列表对象转换为T[]类型的数组。

下面是一些根据条件操作的方法。

  • Exists()方法,判断是否存在满足条件的元素,存在时返回true,否则返回false。
  • Find()方法,找到第一个满足条件的元素,没有时返回T类型的默认值。
  • FindLast()方法,找到最后一个满足条件的元素,没有时返回T类型的默认值。
  • FindAll()方法,找到所有满足条件元素,并返回由这些元素组成的新列表对象。没有找到时会返回完成0个元素的空列表对象。
  • FindIndex()方法,返回第一个满足条件的元素的索引,没有时返回-1。
  • FindLastIndex()方法,返回最后一个满足条件的元素的索引,没有时返回-1。
  • TrueForAll()方法,所有元素都满足条件时返回true,否则返回false。如果列表没有元素同样返回true。

下面的代码会使用FindAll()方法查找列表元素中的所有偶数。

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

namespace csfx_demo
{
    class Program
    {
        static void Main(string[] args)
        {
            List<int> lst = new List<int>(
                new int[] { 1, 2, 3, 5, 6, 7, 8 });
            List<int> lstEven = lst.FindAll(x => x % 2 == 0);
            foreach (int n in lstEven)
            {
                Console.WriteLine(n);
            }
        }
    }
}

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

ForEach()方法通过委托对所有元素执行相同的操作,如下面的代码会显示所有元素的平方。

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

namespace csfx_demo
{
    class Program
    {
        static void Main(string[] args)
        {
            List<int> lst = new List<int>(new int[] { 1, 2, 3, 4, 5 });
            lst.ForEach(x => Console.WriteLine(x * x));
        }
    }
}

执行代码会分行显示1、4、9、16、25。

ConvertAll()方法,将列表中所有元素转换类型,并返回新类型元素组成的列表对象,参数需要指定转换方法,如下面的代码。

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

namespace csfx_demo
{
    class Program
    {
        static void Main(string[] args)
        {
            List<string> lstStr = new List<string>(
                new string[] { "1", "2", "3", "4", "5" });
            List<int> lstInt = lstStr.ConvertAll((s) => int.Parse(s));
            foreach(int n in lstInt)
            {
                Console.WriteLine(n);
            }
        }
    }
}

代码中,使用int.Parse()方法将lstStr列表中的所有元素转换为int类型,并返回由新类型元素组成的lstInt列表对象,最后,使用foreach语句访问lstInt对象的元素,会分行显示元素1、2、3、4、5。

Dictionary泛型类

字典是一种“键/值”结构的集合类型,在.NET Framework中可以使用Dictionary<K,V>泛型类处理。在字典中,每个元素都由“键”和“值”组成,其中,“键”是元素的名称,在一个字典中不能重复,也不能为null;“值”则是对应的元素数据。下面的代码演示了字典的基本操作。

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

namespace csfx_demo
{
    class Program
    {
        static void Main(string[] args)
        {
            Dictionary<string, string> d = new Dictionary<string, string>();
            d.Add("earth", "地球");
            d["mars"] = "火";
            d["mars"] = "火星";
            Console.WriteLine(d["earth"]);  // 地球
            foreach (string k in d.Keys)
            {
                Console.WriteLine("{0} : {1}", k, d[k]);
            }
        }
    }
}

代码执行结果如下图所示。

字典的应用

代码中,首先定义了Dictionary<string,string>对象,其中,第一个string为“键”的类型,第二个string是“值”的类型,也就是说,字典元素的名称和数据都是字符串类型。

接下来,使用Add()方法添加字典元素,参数一指定元素名称,参数二指定元素数据。使用索引形式指定元素数据时,当“键”不存在时会添加元素,当“键”存在时会修改元素的数据。

第一个输出中,通过“键”读取了“earth”对应的数据,显示为“地球”。需要注意的是,使用索引形式读取元素数据时,如果“键”不存在则会产生KeyNotFoundException异常。

最后,使用foreach语句结构依次访问“键”,并通过"键 : 值"格式显示元素。

下面是Dictionary泛型类的常用成员。

  • Count属性,返回字典中的元素数量。
  • Keys属性,返回字典中“键”的集合。
  • Values属性,返回字典中“值”的集合。
  • Add(key,value)方法,添加元素。参数分别指定“键”和“值”。
  • Clear()方法,删除所有元素。
  • ContainsKey(key)方法,判断“键”是否存在,存在时返回true,否则返回false。
  • ContinasValue(value)方法,判断“值”是否存在,存在时返回true,否则返回false。
  • Remove(key)方法,删除指定“值”的元素,当“值”存在并成功删除元素时返回true,“键”不存在时返回false。
  • TryGetValue(key, out value)方法,尝试读取“键”为key的数据,并通过输出参数value输出;成功读取时方法返回true,key不存在时返回false。

下面的代码演示了字典的一些操作。

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

namespace csfx_demo
{
    class Program
    {
        static void Main(string[] args)
        {
            Dictionary<string, string> d = new Dictionary<string, string>
            {
                { "earth", "地球"},
                { "mars", "火星" },
                { "jupiter", "木星" }
            };
            Console.WriteLine(d.ContainsKey("mars"));  // True
            Console.WriteLine(d.ContainsValue("木星"));  // True
            Console.WriteLine(d.ContainsKey("saturn"));  // False
            Console.WriteLine(d.ContainsValue("土星"));  // False
            //
            string key = "earth";
            string value;
            if (d.TryGetValue("earth", out value))
            {
                Console.WriteLine("{0} : {1}", key, value);
            }
            else
            {
                Console.WriteLine("{0}键不存在", key);
            }
        }
    }
}

代码中,首先定义了字典对象d,并直接通过字典结构指定元素名称和数据,格式为{{key1,value1},{key2,value2},{key3,value3},...}。

接下来,测试了不同的“键”和“值”是否存在。最后,通过TryGetValue()方法读取元素的数据。代码执行结果如下图所示,这里,可以修改变量key的值来观察执行结果。

字典的应用