Skip to content

Files

Latest commit

 

History

History
372 lines (271 loc) · 15.1 KB

6.md

File metadata and controls

372 lines (271 loc) · 15.1 KB

六、使用集合和泛型

您已经在前面的章节中看到了数组,它们在需要固定大小、强类型对象列表的场景中非常有用。但是,很多时候需要将对象组织成不同类型的数据结构,如列表、队列、栈和字典。控件中的集合类,C# 开发人员可以使用这些功能。NET 框架。

使用集合的核心部分是泛型的使用,它允许您使用参数化代码。这允许您强有力地键入您的收藏。您甚至可以编写自己的使用泛型的代码,允许您创建强类型的可重用库。

| | 注意:的第一个版本。NET 提供了一个基于非强类型对象的集合库。毕竟。NET 类型可分配给对象,这起作用了。但是,您必须编写大量代码,使用强制转换运算符将对象转换回添加到集合中的类型。泛型解决了这个问题,使用泛型集合是。NET。 |

使用集合

。NET 集合类允许您以多种不同的方式处理数据。可以用List代替数组。如果需要一套先进先出的物品,可以使用Queue。如果你需要处理具有唯一标识的物品,你可以使用Dictionary。使用泛型,您可以构建自己的集合,以任何需要的方式管理数据。

| | 提示:查看系统。集合。在编写自己的集合之前使用通用命名空间;你可能会发现你需要的东西已经写好了。 |

一个常见的集合是List,这是一个数组的很好的替代。下面的清单提供了一个例子。

    using System;
    using System.Collections.Generic;

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

    public class Program
    {
        public static void Main()
        {
            List<string> names = new List<string>();
            names.Add("Joe");
            names.Insert(0, "Car");
            names.Add("Jill");
            names[0] = "Building";
            names.RemoveAt(0);
            Console.WriteLine($"First name: {names[0]}");

            IList<Company> companies = new List<Company>
            {
                new Company { Name = "Syncfusion" },
                new Company { Name = "Microsoft" },
                new Company { Name = "Acme" }
            };

            foreach (Company cmp in companies)
                Console.WriteLine(cmp.Name);

            Console.ReadKey();
        }
    }

代码清单 85

之前的程序展示了集合的多功能性。你有一个stringList,它只保存string类型的物体。这是一个泛型集合,这意味着它的类型参数在<>中指定该集合使用的对象类型。添加的每一项都会附加到列表中,列表会动态增长。Insert操作在列表的第一个位置添加一个新的string,并将第一个"Joe"下推到索引 1 的第二个位置。第二个Add"Jill"置于指数 2。请注意如何使用索引器(类似数组)语法来访问列表元素。RemoveAt删除了集合第一个索引处的字符串,将"Joe"移动到 0,"Jill"移动到 1。

Main中的第二个List展示了如何使用自定义类型。由于List是从IList派生的,所以您可以将实例分配给该接口。这很方便,因为这意味着你可以创建在IList上运行的代码;无论调用者传入的是List<T>还是从IList派生的任何其他集合类型,您的代码都仍然可以工作。

该示例还使用集合初始化语法,您可以实例化填充List的集合类型的逗号分隔列表。foreach语句遍历集合,打印每个项目。

前面的例子使用了foreach循环,但是你也可以使用ListForEach方法,如下例所示。

            List<Company> companyList = companies as List<Company>;
            companyList.ForEach(cmp => Console.WriteLine(cmp.Name));

代码清单 86

第一行使用as运算符将IList<Company>转换为List<Company>。有了List<T>的实例,你可以调用ForEach方法,它接受一个λ参数。这个λ对List<T>中的每个项目执行,λ参数cmp包含当前项目。

这应该会让你了解List是如何工作的。通过阅读List课程的文档,您可以了解更多可用的方法。

另一个有用的收藏是Dictionary。它的工作方式类似于哈希表,您可以通过索引存储和检索对象,如下例所示。

    using System;
    using System.Collections.Generic;

    public class Customer
    {
        public int ID { get; set; }
        public string Name { get; set; }
    }

    public class Program
    {
        public static void Main()
        {
            Dictionary<int, Customer> customers = new Dictionary<int, Customer>();
            Customer jane = new Customer { ID = 0, Name = "Jane" };
            Customer joe = new Customer { ID = 1, Name = "Joe" };
            customers.Add(jane.ID, jane);
            customers[joe.ID] = joe;

            foreach (int key in customers.Keys)
                Console.WriteLine(customers[key].Name);

            Dictionary<int, Customer> customers2 = new Dictionary<int, Customer>
            {
                [0] = new Customer { ID = 0, Name = "Chris" },
                [1] = new Customer { ID = 1, Name = "Alex" }
            };

            Console.ReadKey();
        }
    }

代码清单 87

上例中的Dictionary分别为键和值取了两个类型参数。第一个示例实例化字典以获取一个int键和Customer值。Customer类有两个属性,其中ID将用作字典的关键字。请注意,您可以通过Add方法或索引器分配向字典添加值的两种不同方式。Add的第一个参数是索引,第二个参数是值。使用索引器时,将索引放在括号中并赋值。就像在其他集合中一样,有许多可用的方法,您应该查看该集合的文档。

foreach循环展示了如何迭代Dictionary项。一个Dictionary有一个Keys属性,它是键的集合,还有一个Values属性,它是值的集合(前面例子中的Customer实例)。注意循环如何使用索引器customers[key]访问与每个键相关的值。

示例中的第二个Dictionary展示了如何使用字典初始化器语法。只需将该值分配给与该值的键匹配的索引。

编写通用代码

泛型的主要应用之一是支持集合。在前一节中,您看到了如何使用集合。你也可以写自己的收藏类。如果您编写了一个通用链表,您将需要一个Node类来保存一个对象并引用列表中的下一个对象,还需要一个LinkedList集合类来执行列表操作。下面清单中的Node类包含一个对象实例。

    class Node<T>
    {
        public T Item { get; set; }
        public Node<T> Next;

        public Node(T item)
        {
            Item = item;
        }
    }

代码清单 88

<T>语法使Node<T>类通用。每当代码实例化一个Node,它就指定一个替换T的类型。在任何使用该类型对象的地方,指定TNode<T>没有访问修饰符,因为它只用于这个程序集中的代码,默认的内部可访问性是合适的。以下示例显示了如何实例化Node<T>

    Node<string> name = new Node<string>("May");

代码清单 89

在这里,你看到的Node<string>是类型,意思是你看到的Node类里面的所有地方都是现在的string。你不会将intdecimal或任何其他类型传递给这个类的构造函数,因为它只会持有string。它是强类型的。

接下来,您需要一个集合,将Node<T>实例保存为一个链表,如下面的列表所示。

    Using System;
    using System.Collections;
    using System.Collections.Generic;

    public class LinkedList<T> : IList<T>
    {
        Node<T> head;
        Node<T> tail;

        public void Add(T item)
        {
            var node = new Node<T>(item);

            if (head == null)
                head = node;
            else
                tail.Next = node;

            tail = node;
        }

        // Other IList members...
    }

代码清单 90

LinkedList类是通用的,并保存它被实例化为的类型的项目。IList<T>界面属于 FCL,便于创建收藏。正如您对接口的期望,为IList接口编写代码的开发人员也可以使用这个集合。LinkedList类实现了IList<T>接口的所有成员,这是必须的。

Add是一个最小的实现,但是说明了使用泛型的一些概念。即使代码实例化了一个新的Node<T>,实际类型将与LinkedList定义的类型相同。同样的概念也适用于IList<T>变成与LinkedList相同类型的界面。以下示例实例化了一个LinkedList<T>

    public class Program
    {
        public static void Main()
        {
            var llist = new LinkedList<string>();
            llist.Add("Jamie");
            llist.Add("Ron");
            //...

            Node<string> name = new Node<string>("May");
        }
    }

代码清单 91

这表明您像任何其他集合一样实例化和使用您的泛型集合。只需在实例化过程中提供类型,集合就可以处理该类型的对象。

您在任何地方看到正在使用的object类型都是创建泛型类型的潜在候选。所有类型都继承了object类型,这就是为什么您会看到 FCL 和其他地方的类型使用对象类型值。

您也可以创建泛型方法。以下示例显示了几个工厂方法,其中一个是类型object,另一个是泛型。

    using System;

    public class CustomerReport
    {
        public DateTime Date { get; set; }
    }

    public class OrdersReport
    {
        public DateTime Date { get; set; }
    }

    public class ReportFactory
    {
        public static object Create(Type reportType)
        {
            switch (reportType.ToString())
            {
                case "CustomerReport":
                    var custRpt = new CustomerReport();
                    custRpt.Date = DateTime.Now;
                    return custRpt;
                default:
                case "OrdersReport":
                    var ordsRpt = new OrdersReport();
                    ordsRpt.Date = DateTime.Now;
                    return ordsRpt;
            }
        }
    }

    public class Program
    {
        public static void Main()
        {
            var rpt = (CustomerReport)ReportFactory.Create(typeof(CustomerReport));
            Console.ReadKey();
        }
    }

代码清单 92

您应该从之前的ReportFactory实现中得到的是,代码中有很多重复,并且在Main方法中使用了强制转换和typeof操作符,包括了比必要更多的语法。您可能会看到这些代码变得越来越复杂,越来越难以维护。以下示例显示了如何将Create方法重构为泛型方法。

    using System;

    public abstract class Report { }

    public class CustomerReport : Report
    {
        public DateTime Date { get; set; }
    }

    public class OrdersReport : Report
    {
        public DateTime Date { get; set; }
    }

    public class ReportFactory
    {
        public static TReport Create<TReport>()
            where TReport : Report
        {
            switch (typeof(TReport).Name)
            {
                case "CustomerReport":
                    var custRpt = new CustomerReport();
                    custRpt.Date = DateTime.Now;
                    return (TReport)(Report)custRpt;
                default:
                case "OrdersReport":
                    var ordsRpt = new OrdersReport();
                    ordsRpt.Date = DateTime.Now;
                    return (TReport)(Report)ordsRpt;
            }
        }
    }

    public class Program
    {
        public static void Main()
        {
            var rpt2 = ReportFactory.Create<CustomerReport>();

            Console.ReadKey();
        }
    }

代码清单 93

Create方法有一个新的类型参数TReport。在前面的例子中,您已经看到了仅使用T的情况,但是有时候——就像在Dictionary<TKeyTValue>中一样——您必须区分多个类型参数,或者使代码更加自文档化。返回类型现在也是强类型的。代码能够从派生类型转换到Report,然后转换到TReport以返回正确的类型。这是允许的,因为通用约束条件TReport : Report规定TReport必须从Report派生。调用代码要简单得多。

Create<TReport>方法仍然比它必须的要长,并且包含了太多的重复。我们可以用一般的约束来解决这个问题。约束顾名思义就是:它限制了类型的泛型程度。您在前面的代码中看到了对Report的基类约束。下表描述了所有可用的约束。

表 3:泛型类型约束

限制 描述
连接 类型必须实现指定的接口。
基本类 类型必须从指定的基类派生。
班级 类型必须是引用类型。
结构体 类型必须是值类型。
新的 类型必须有默认(无参数)构造函数。

我们需要两个约束来简化我们的代码:interfacenew。以下示例显示了如何使用它们。

    using System;

    public interface IReport
    {
        DateTime Date { get; set; }
    }

    public class CustomerReport : IReport
    {
        public DateTime Date { get; set; }
    }

    public class OrdersReport : Report, IReport
    {
        public DateTime Date { get; set; }
    }

    public class ReportFactory
    {
        public static TReport Create<TReport>()
            where TReport : IReport, new()
        {
            return new TReport() { Date = DateTime.Now };
        }
    }

    public class Program
    {
        public static void Main()
        {
            var rpt2 = ReportFactory.Create<CustomerReport>();

            Console.ReadKey();
        }
    }

代码清单 94

在这个演示中,有一个新的界面IReport,它是CustomerReportOrdersReport派生出来的。因为我们知道我们期望的类是IReport,我们可以对类型做出假设,并编写在任何IReport上运行的代码。

Create<TReport>方法在方法签名后有额外的语法。要指定一个约束,在要约束的类型后面跟随where关键字,附加一个分号,然后从上一个表中添加一个逗号分隔的约束列表。这个例子使用了一个接口和new()约束。new()约束意味着我们可以创建一个新的类型实例,new TReport()。此外,由于类型是一个IReport,我们知道它有一个Date属性,可以填充它的Date属性。重复和过多的代码已经不复存在,在实现和使用上都被通用代码简化了。

| | 提示:您也可以创建通用委托。像往常一样,您应该寻求重用已经存在于 FCL 的类型。中流行的可重用委托。NET 框架是事件处理程序。事实上,您可以将第 5 章中对 ClickHandler 的所有引用替换为事件处理程序< ClickEventArgs >,您的代码仍然可以工作。 |

总结

您已经看到了如何使用泛型,并且它们允许您编写可重用的代码。那个。NET 集合类比数组更通用,允许您以更有助于应用程序设计的方式管理对象。