C--面向对象编程入门指南-全-
C# 面向对象编程入门指南(全)
零、简介
作为一名. NET 培训师和首席程序员,我的经验是,大多数人在掌握 C# 语言的语法方面没有问题。困扰和挫败许多人的是面向对象编程方法和设计的更高层次的概念。更糟糕的是,大多数介绍性的编程书籍和培训课程都忽略了这些概念,或者更糟糕的是,根本没有涉及到它们。我希望这本书能填补这一空白。我写这本书的目的有两个。我的第一个目标是为您提供理解 C# 编程基础所需的信息。更重要的是,我的第二个目标是为您提供掌握面向对象编程方法和设计的高级概念所需的信息。
这本书提供了你需要的知识来构建一个面向对象的编程解决方案,旨在解决一个业务问题。当您阅读本书时,您将首先学习如何分析应用的业务需求。接下来,您将对解决方案设计中涉及的对象和关系进行建模。最后,您将使用 C# 实现该解决方案。在这个过程中,您将学习软件设计的基础知识、统一建模语言(UML)、面向对象编程、C# 和 .NET 框架。
因为这是一本介绍性的书,所以它是你学习它所呈现的主题的起点。因此,这本书不是为了让你成为面向对象编程和 UML 的专家;也不是对 C# 和 .NET 框架;也不是对 Visual Studio 的深入研究。精通这些领域中的任何一个都需要相当多的时间和努力。我希望通过阅读这本书,你在面向对象编程方面的第一次经历将会是愉快的和容易理解的——并且这些经历将会灌输进一步学习的愿望。
目标受众
本书的目标读者是初学 C# 的程序员,他们希望在掌握 C# 语言基础的同时,掌握面向对象编程的基础。从面向过程编程模型过渡到面向对象模型的程序员也将从本书中受益。另外,还有很多 Visual Basic (VB)程序员想过渡到 C#。在过渡到 C# 之前,您必须理解面向对象编程的基础。
因为“初学者”的经验水平可能会有很大的差异,所以我在附录 A 中包含了一本讨论一些基本编程概念以及如何用 C# 实现它们的入门书。如果你是编程新手,我建议你复习一下这些概念。
活动和软件要求
学习最重要的一个方面是做。不骑自行车就学不会骑自行车,不出代码就学不会编程。任何成功的培训计划都需要包括理论和实践两个部分。
我在本书中包含了这两个部分。我希望你会认真对待我在每一章中添加的活动,并彻底完成它们——甚至是反复完成。与一些学生认为这些活动是“打字练习”相反,在这些活动中,你有机会将理论具体化,并对概念进行真正的模拟。我也鼓励你在一项活动中边工作边玩耍。不要害怕修改一些代码,看看会发生什么。一些最好的学习经历发生在学生“超越界限”的时候
前面章节中的 UML 建模活动是为使用 UMLet 的人设计的。我选择这个程序是因为它是一个很好的学习绘图工具。它允许您创建 UML 图,而无需添加许多与高端 CASE 工具相关的高级特性。UMLet 是一个免费的开源工具,可以从www.umlet.com
下载。您也可以使用 Visio 等其他工具来完成这些活动。然而,你不需要一个工具来完成这些活动;纸和铅笔就可以了。
一旦你开始编码,你将需要安装 C# 的 Visual Studio 2012。我鼓励您安装帮助文件,并在完成活动时充分利用它们。第十三章讲述了如何创建 Windows 应用商店应用,并要求在 Windows 8 操作系统上安装 Visual Studio。后面的章节要求安装了 Pubs 和 Northwind 数据库的 Microsoft SQL Server 2008 或更高版本。附录 C 包括下载和安装示例数据库的说明。你可以在www.msdn.microsoft.com
找到 Visual Studio 和 SQL Server 的免费速成版和试用版,以及 Windows 8 的试用版。
一、面向对象编程概述
为了给你学习面向对象编程(OOP)和 C# 打下基础,本章将简要介绍面向对象编程的历史和面向对象编程语言的特点。您将看到为什么面向对象编程在工业级分布式软件系统的开发中变得如此重要。您还将研究 C# 是如何发展成为主流应用编程语言之一的。
阅读本章后,您将熟悉以下内容:
- 什么是面向对象编程
- 为什么面向对象编程在工业级应用的开发中变得如此重要
- 使编程语言面向对象的特征
- C# 的历史和演变
OOP 是什么?
面向对象编程是一种软件开发方法,在这种方法中,软件的结构基于对象之间的交互来完成任务。这种交互采取在对象之间来回传递消息的形式。作为对消息的响应,对象可以执行一个动作。
如果你看看你是如何在你周围的世界中完成任务的,你可以看到你在一个面向对象的世界中互动。例如,如果你想去商店,你可以与一个汽车对象进行交互。汽车对象由其他对象组成,这些对象相互交互以完成将您带到商店的任务。你把钥匙放在点火物体里,转动它。这反过来向启动器对象发送一条消息(通过电信号),启动器对象与引擎对象交互以启动汽车。作为一名司机,你与系统的对象如何一起工作来启动汽车的逻辑是隔离的。您只需通过用钥匙执行点火对象的 start 方法来启动事件序列。然后,您等待成功或失败的响应(消息)。
类似地,软件程序的用户与完成任务所需的逻辑相隔离。例如,当您在文字处理器中打印页面时,您可以通过单击打印按钮来启动该操作。你被从需要发生的内部处理中隔离出来;你只需等待一个告诉你是否打印的回复。在软件程序中,按钮对象与打印机对象交互,打印机对象与实际打印机交互以完成打印页面的任务。
面向对象的历史
OOP 概念在 20 世纪 60 年代中期随着一种叫做 Simula 的编程语言出现,并在 20 世纪 70 年代随着 Smalltalk 的出现而进一步发展。尽管软件开发人员并没有完全接受 OOP 语言的这些早期进步,但是面向对象的方法继续发展。在 20 世纪 80 年代中期,人们对面向对象的方法又有了新的兴趣。具体来说,C++和 Eiffel 等 OOP 语言开始受到主流计算机程序员的欢迎。OOP 在 20 世纪 90 年代继续流行,最显著的是 Java 的出现和它吸引的大量追随者。在 2002 年,随着。微软引入了一种新的面向对象语言 C#(发音为 C-sharp ),并改进了他们广泛流行的现有语言 Visual Basic,使其现在真正面向对象。今天,OOP 语言继续蓬勃发展,是现代编程的中流砥柱。
为什么要用 OOP?
为什么 OOP 已经发展成为当今解决业务问题的广泛使用的范例?在 20 世纪 70 年代和 80 年代,面向过程的编程语言如 C、Pascal 和 Fortran 被广泛用于开发面向商业的软件系统。过程语言以线性方式组织程序,它们从上到下运行。换句话说,程序是一系列一个接一个运行的步骤。这种类型的编程对于由几百行代码组成的小程序来说很好,但是随着程序变大,它们变得难以管理和调试。
为了管理不断增长的程序规模,引入了结构化编程来将代码分解成可管理的称为函数或过程的片段。这是一个进步,但是随着程序执行更复杂的业务功能并与其他系统交互,结构化编程的以下缺点开始显现:
- 程序变得更难维护。
- 在不对系统的所有功能产生负面影响的情况下,很难改变现有的功能。
- 新程序基本上是从零开始构建的。因此,以前的投资回报很少。
- 编程不利于团队发展。程序员必须知道一个程序如何工作的每一个方面,不能把他们的努力孤立在系统的一个方面。
- 很难将业务模型转化为编程模型。
- 结构化编程在孤立的情况下工作得很好,但是不能很好地与其他系统集成。
除了这些缺点之外,计算系统的一些发展对结构化程序方法造成了进一步的压力,例如:
- 非程序员通过图形用户界面和他们的台式计算机的结合,要求并获得了对程序的直接访问。
- 用户需要一种更直观、更少结构化的方法来与程序交互。
- 计算机系统发展成一种分布式模型,其中业务逻辑、用户界面和后端数据库是松散耦合的,并通过 Internet 和 intranets 进行访问。
因此,许多商业软件开发人员转向了面向对象的方法和编程语言。这些好处包括如下:
- 从业务分析模型到软件实现模型的更直观的转变
- 更有效、更快速地维护和实施计划变更的能力
- 使用团队过程更有效地创建软件系统的能力,允许专家处理系统的各个部分
- 能够在其他程序中重用代码组件,并购买由第三方开发人员编写的组件,从而毫不费力地增加现有程序的功能
- 与松散耦合的分布式计算系统更好地集成
- 改进了与现代操作系统的集成
- 为用户创建更直观的图形用户界面的能力
面向对象的特点
在这一节中,你将研究一些所有面向对象语言共有的基本概念和术语。不要担心这些概念是如何在任何特定的编程语言中实现的;那是以后的事。我的目标是让你熟悉这些概念,并把它们与你的日常经验联系起来,这样当你以后看 OOP 设计和实现时,它们就更有意义了。
目标
正如我前面提到的,我们生活在一个面向对象的世界里。你是一件物品。你与其他物体互动。其实你就是一个物体,有你的身高,发色等数据。你也有你执行的方法或在你身上执行的方法,例如吃和走。
那么什么是对象?用面向对象的术语来说,对象是一个用来合并数据和处理数据的过程的结构。例如,如果您对跟踪与产品库存相关的数据感兴趣,您可以创建一个 product 对象,负责维护和使用与产品相关的数据。如果您想在应用中拥有打印功能,您可以使用一个打印机对象,该对象负责用于与打印机交互的数据和方法。
抽象
当您与世界上的对象交互时,您通常只关心它们属性的子集。如果没有这种提取或过滤物体无关属性的能力,你会发现很难处理轰炸你的大量信息并专注于手头的任务。
作为抽象的结果,当两个不同的人与同一个对象交互时,他们经常处理不同的属性子集。例如,当我开车时,我需要知道汽车的速度和方向。因为汽车使用自动变速器,所以我不需要知道发动机的每分钟转数,所以我过滤掉这些信息。另一方面,这些信息对于赛车手来说至关重要,他们不会过滤掉这些信息。
当在 OOP 应用中构造对象时,结合这个抽象概念是很重要的。这些对象只包括与应用上下文相关的信息。如果您正在构建一个运输应用,您将构建一个具有诸如尺寸和重量等属性的产品对象。该项目的颜色是无关的信息,将被忽略。另一方面,在构建订单输入应用时,颜色可能很重要,并且会作为产品对象的属性包含在内。
包装
OOP 的另一个重要特征是封装。封装是不允许对数据进行直接访问的过程;反而是隐藏的。如果您想要访问数据,您必须与负责数据的对象进行交互。在前面的库存示例中,如果您想要查看或更新产品信息,您必须处理产品对象。要读取数据,您需要向产品对象发送一条消息。然后,产品对象将读取该值并发回一条消息,告诉您该值是多少。产品对象定义了可以对产品数据执行的操作。如果您发送一条消息来修改数据,并且产品对象确定它是一个有效的请求,它将为您执行操作并发送一条消息返回结果。
你在日常生活中无时无刻不在体验着被封闭。想想人力资源部门。它们封装(隐藏)了员工的信息。他们决定如何使用和操作这些数据。任何对员工数据的请求或更新数据的请求都必须通过他们。再比如网络安全。任何对安全信息的请求或对安全策略的更改都必须通过网络安全管理员进行。对网络用户来说,安全数据是封装的。
通过封装数据,您可以使系统的数据更加安全可靠。您知道数据是如何被访问的,以及对数据执行了什么操作。这使得程序维护更加容易,也大大简化了调试过程。您还可以修改用于处理数据的方法,并且,如果您不改变请求方法的方式和发送回的响应类型,您就不必改变使用该方法的其他对象。想想当你在邮件中寄信的时候。你向邮局请求投递这封信。邮局是如何做到这一点的,你无从知晓。如果它更改了用于邮寄信件的路线,它不会影响您开始发送信件的方式。你不必知道邮局传递信件的内部程序。
多态
多态是两个不同的对象以他们自己独特的方式响应同一请求消息的能力。例如,我可以训练我的狗回应命令吠叫,训练我的鸟回应命令唧唧喳喳。另一方面,我可以训练它们既响应命令又说话。通过多态,我知道狗会用叫声回应,而鸟会用唧唧声回应。
这和 OOP 有什么关系?您可以创建对象,这些对象以自己独特的实现方式响应相同的消息。例如,您可以向在打印机上打印文本的打印机对象发送打印消息,也可以向将文本打印到计算机屏幕上的窗口的屏幕对象发送相同的消息。
多态的另一个好例子是英语中单词的使用。单词有许多不同的意思,但通过句子的上下文,你可以推断出哪个意思是想要的。你知道有人会说“饶了我吧!”不是让你打断他的腿!
在 OOP 中,你通过一个叫做重载的过程来实现这种类型的多态。您可以实现同名对象的不同方法。然后,该对象可以根据消息的上下文(换句话说,传递的参数的数量和类型)来判断要实现哪个方法。例如,您可以创建 inventory 对象的两种方法来查找产品的价格。这两种方法都将被命名为 getPrice。另一个对象可以调用这个方法并传递产品名称或产品 ID。inventory 对象可以通过请求传递的是字符串值还是整数值来判断运行哪个 getPrice 方法。
遗产
大多数现实生活中的对象都可以分为不同的层次。例如,您可以将所有狗归为具有某些共同特征的一类,如有四条腿和皮毛。他们的品种进一步将他们分为具有共同属性的亚组,如体型和神态。你也可以根据物体的功能对其进行分类。比如有商务车,也有休闲车。有卡车和客车。你根据汽车的品牌和型号来分类。为了理解这个世界,您需要使用对象层次和分类。
你在 OOP 中使用继承来根据共同的特征和功能对程序中的对象进行分类。这使得处理对象更加容易和直观。它还使编程更容易,因为它使您能够将一般特征组合到父对象中,并在子对象中继承这些特征。例如,您可以定义一个 employee 对象,该对象定义公司中雇员的所有一般特征。然后,您可以定义一个经理对象,该对象继承 employee 对象的特征,但也添加了贵公司经理特有的特征。由于继承关系,经理对象将自动反映雇员对象特征的任何变化。
聚合
聚合是指一个对象由一起工作的其他对象组成。例如,您的割草机对象是轮子对象、引擎对象、刀片对象等的组合。事实上,引擎对象是许多其他对象的组合。在我们周围的世界里有许多聚合的例子。在 OOP 中使用聚合的能力是一个强大的特性,它使您能够在程序中准确地建模和实现业务流程。
c# 的历史
在 20 世纪 80 年代,大多数运行在 Windows 操作系统上的应用都是用 C++编写的。尽管 C++是一种面向对象的语言,但可以说它是一种很难掌握的语言,程序员负责处理诸如内存管理和安全性等日常事务。这些内务处理任务很难实现,并且经常被忽略,这导致了充满 bug 的应用很难测试和维护。
在 20 世纪 90 年代,Java 编程语言开始流行。因为它是一种托管编程语言,所以它运行在一组统一的类库之上,这些类库负责低级编程任务,如类型安全检查、内存管理和销毁不需要的对象。这使得程序员可以专注于业务逻辑,而不必担心容易出错的内务代码。因此,程序更紧凑、更可靠、更易于调试。
看到 Java 的成功和互联网的日益普及,微软开发了自己的一套托管编程语言。微软想让开发基于 Windows 和基于 Web 的应用变得更容易。这些托管语言依赖于 .NET Framework 提供了许多功能来执行所有应用中所需的内务处理代码。在开发过程中 .NET 框架,类库是用一种叫做 C# 的新语言编写的。C# 的主要设计者和首席架构师是安德斯·海尔斯伯格。Hejlsberg 之前参与了 Turbo Pascal 和 Delphi 的设计。他利用以前的经验设计了一种 OOP 语言,这种语言建立在这些语言的成功之上,并改进了它们的缺点。Hejlsberg 还将类似于 C 的语法结合到语言中,以吸引 C++和 Java 开发人员。创建 .NET 框架和 C# 语言将现代概念,如面向对象、类型安全、垃圾收集和结构化异常处理直接引入平台。
自从发布 C# 以来,微软一直在寻求为该语言添加额外的功能和增强。例如,2.0 版本增加了对泛型的支持(泛型在第九章中有所涉及),3.0 版本增加了 LINQ (更多信息请见第十章)以减少编程语言和用于检索和处理数据的数据库语言之间的阻抗不匹配。如今,C# 5.0 支持让并行和异步编程更易于开发人员实现(参见第八章)。随着微软对 C# 不断改进和发展的承诺,它将继续成为世界上使用最广泛的编程语言之一。
微软也致力于提供 .NET 开发人员拥有必要的工具,以获得高效和直观的编程体验。虽然您可以使用文本编辑器创建 C# 程序,但大多数专业程序员发现集成开发环境(IDE)在易用性和提高生产率方面非常有价值。微软在 Visual Studio (VS)中开发了一个出色的 IDE。集成到 VS 中的许多特性使得 .NET 框架更直观(这些特性在第五章中有所介绍)。随着 Visual Studio 2012 的最新发布,微软继续增强设计时开发体验。VS 2012 包含了一些新特性,比如对并行编程的更好的调试支持和改进的代码测试体验。当你读完这本书时,我想你会逐渐体会到 Visual Studio 和 C# 语言所提供的强大功能和高效率。
摘要
在这一章中,你已经了解了面向对象的程序设计和 C# 的简史。既然您已经了解了 OOP 语言的组成以及 OOP 语言对于企业级应用开发如此重要的原因,那么您的下一步就是熟悉 OOP 应用是如何设计的。
为了满足用户的需求,必须仔细规划和开发成功的应用。下一章是三章系列的第一章,旨在向您介绍设计面向对象应用时使用的一些技术。您将看到决定应用中需要包含哪些对象的过程,以及这些对象的哪些属性对该应用的功能很重要。
二、设计 OOP 解决方案:识别类结构
作为商业软件开发人员,你将参与的大多数软件项目都是团队工作。作为团队中的程序员,您将被要求将设计文档转换成实际的应用代码。此外,因为面向对象程序的设计是一个迭代过程,设计者依赖软件开发者的反馈来提炼和修改程序设计。随着您在开发面向对象软件系统方面获得经验,您甚至可能会被邀请参加设计会议,并对设计过程做出贡献。因此,作为软件开发人员,您应该熟悉各种设计文档的目的和结构,并对这些文档的开发方式有所了解。
本章向您介绍一些用于设计系统静态方面的常用文档。(你将在下一章了解系统的动态方面是如何建模的。)为了帮助您理解这些文档,本章包括一些基于有限案例研究的实践活动。你会在本书的大部分章节中找到与讨论主题相对应的类似活动。
阅读本章后,您将熟悉以下内容:
- 软件设计的目标
- 统一建模语言的基础
- 软件需求规格的目的
- 用例图如何建模系统将提供的服务
- 类图如何对需要开发的对象类建模
软件设计的目标
在开发现代企业级面向对象程序时,组织良好的系统设计方法是必不可少的。设计阶段是软件开发周期中最重要的阶段之一。您可以将许多与失败的软件项目相关的问题追溯到糟糕的前期设计以及系统开发人员和系统消费者之间不充分的沟通。不幸的是,许多程序员和项目经理不喜欢参与系统的设计。他们认为任何不花在生产代码上的时间都是无效的。
更糟糕的是,随着“互联网时代”的到来,消费者期望开发周期越来越短。因此,为了满足不切实际的时间表和项目范围,开发人员倾向于放弃或缩短开发的系统设计阶段。这对系统的成功起反作用。在设计过程中投入时间将实现以下目标:
- 提供一个机会来审查当前的业务流程,并修复任何发现的低效或缺陷
- 教育客户软件开发过程是如何发生的,并让他们作为合作伙伴参与到这个过程中
- 创建现实的项目范围和完成时间表
- 为确定软件测试需求提供基础
- 降低实施软件解决方案所需的成本和时间
软件设计的一个很好的类比是建造一个家的过程。没有建筑师提供的详细计划(蓝图),你不会想到建筑商会开始建造房子。你会期望建筑师在设计蓝图之前和你讨论房子的设计。建筑师的工作就是倾听你的要求,并把它们转化成建筑者可以用来建造房屋的平面图。一个好的架构师还会告诉你哪些特性对你的预算和预计的时间表是合理的。
理解统一建模语言
为了成功地设计面向对象的软件,您需要遵循一个经过验证的设计方法。如今,OOP 中使用的一种经过验证的设计方法是统一建模语言(UML)。UML 开发于 20 世纪 80 年代早期,作为对面向对象软件设计建模的标准、系统方法的需求的回应。该行业标准由对象管理小组(OMG)创建和管理。UML 已经随着软件行业的发展而成熟,当前版本 2.4.1 于 2011 年正式发布。
在设计过程中使用 UML 有很多好处。当正确实现时,UML 允许您在不同的细节层次上可视化软件系统。您可以与用户一起验证项目的需求和范围。它也可以作为测试系统的基础。UML 非常适合增量开发过程。UML 建模对于大型系统的并行开发也非常有益。使用该模型,每个团队都知道他们的部分如何适应系统,并可以传达可能影响其他团队的变化。
UML 由所提议的解决方案的一系列文本和图形模型组成。这些模型定义了系统范围、系统组件、用户与系统的交互,以及系统组件如何相互交互以实现系统功能。
UML 中使用的一些常见模型如下:
- 软件需求说明书 (SRS) : 对系统整体职责和范围的文字描述。
- 用例 : 从用户的角度对系统行为的文本/图形描述。用户可以是人类或其他系统。
- 类图 : 将用于构建系统的对象的可视化蓝图。
- 序列图 : 程序执行时对象交互序列的模型。重点放在交互的顺序以及它们如何随时间进行。
- 协作图 : 程序执行时,对象如何组织在一起工作的视图。重点放在对象之间的通信上。
- 活动图 : 流程或操作执行流程的可视化表示。
在这一章中,你将会看到 SRS 的开发,用例,以及类图。第三章涵盖了顺序图、协作图和活动图。
制定安全气囊系统
SRS 的目的是完成以下工作:
- 定义系统的功能需求。
- 确定系统的边界。
- 识别系统的用户。
- 描述系统和外部用户之间的交互。
- 在客户和程序团队之间建立一种描述系统的通用语言。
- 为用例建模提供基础。
要制作 SRS,您需要采访业务所有者和系统的最终用户。这些访谈的目标是清楚地记录所涉及的业务流程,并确定系统的范围。该过程的结果是一份详细说明系统功能要求的正式文件(SRS)。正式文档有助于确保客户和软件开发人员之间达成一致。随着开发的进行,SRS 还为解决任何关于感知系统范围的分歧提供了基础。
例如,假设一家小型通勤航空公司的所有者希望客户能够使用 Web 注册系统查看航班信息并预订机票。与业务经理和票务代理面谈后,软件设计师起草一份 SRS 文档,列出系统的功能需求。以下是其中的一些要求:
- 未注册的网络用户可以浏览网站查看航班信息,但他们不能预订航班。
- 想要预订航班的新客户必须填写登记表,提供他们的姓名、地址、公司名称、电话号码、传真号码和电子邮件地址。
- 客户分为公司客户和零售客户。
- 客户可以根据目的地和出发时间搜索航班。
- 客户可以在预订航班时注明航班号和所需座位数。
- 预订航班后,系统会通过电子邮件向客户发送确认信息。
- 当公司客户的员工预订航班时,他们会获得常旅客里程。
- 常旅客里程用来对未来的购买打折。
- 机票预订可提前一周取消,并可获得 80%的退款。
- 票务代理可以查看和更新航班信息。
在这个部分 SRS 文档中,您可以看到几个简洁的语句定义了系统范围。它们从系统用户的角度描述系统的功能,并确定将使用它的外部实体。需要注意的是,SRS 不包含对系统技术要求的参考。
一旦 SRS 被开发出来,它所包含的功能需求就被转化为一系列的用例图。
引入用例
用例描述了外部实体将如何使用系统。这些外部实体可以是人或者其他系统(在 UML 术语中称为参与者)。描述强调用户对系统的看法以及用户和系统之间的交互。用例有助于进一步定义系统范围和边界。它们通常以图表的形式,伴随着对正在发生的交互的文本描述。图 2-1 显示了一个通用图,它由两个参与者组成,用简笔画表示,用矩形表示系统,用椭圆表示系统边界内的用例。
图 2-1 。有两个参与者和三个用例的通用用例图
用例是根据 SRS 文档开发的。参与者是与系统交互的任何外部实体。参与者可以是一个人类用户(例如,一个租赁代理),另一个软件系统(例如,一个软件计费系统),或者一个接口设备(例如,一个温度探测器)。参与者和系统之间发生的每个交互都被建模为一个用例。
图 2-2 中显示的示例用例是为上一节中介绍的机票预订应用开发的。它显示了需求“客户可以根据目的地和出发时间搜索航班”的用例图。
图 2-2 。查看航班信息用例
除了用例的图形化描述,许多设计人员和软件开发人员发现提供用例的文本描述很有帮助。文字描述应该简洁明了,集中在正在发生的事情上,而不是它是如何发生的。有时,与用例相关的任何先决条件或后置条件也被识别。下文进一步描述了图 2-2 中所示的用例图。
- 描述:顾客查看航班信息页面。客户输入航班搜索信息。提交搜索请求后,客户查看符合搜索条件的航班列表。
- 前提条件:无。
- 后置条件:客户有机会登录并进入机票预订页面。
再举一个例子,看看图 2-3 中显示的预留座位用例。
图 2-3 。预订座位用例图
下文进一步描述了图 2-3 中所示的用例图。
- 描述:顾客输入航班号,并指出所要求的座位。客户提交请求后,会显示一些确认信息。
- 前提条件:客户已经查询了航班信息。客户已经登录并正在查看航班预订屏幕。
- 后置条件:向客户发送一封确认电子邮件,概述航班详情和取消政策。
正如你从图 2-3 中看到的,在用例之间可以存在某些关系。预订座位用例包括查看航班信息用例。这种关系非常有用,因为您可以独立于预订航班用例使用查看航班信息用例。这叫包容。然而,您不能将预订座位用例与查看航班信息用例分开使用。这是将影响您如何对解决方案建模的重要信息。
用例相互关联的另一种方式是通过扩展。你可能有一个通用用例,它是其他用例的基础。其他用例扩展了基本用例。例如,您可能有一个注册客户用例,它描述了注册客户的核心过程。然后,您可以开发注册公司客户和注册零售客户用例,扩展基本用例。扩展和包含之间的区别在于,在扩展中,被扩展的基本用例不会单独使用。图 2-4 展示了你如何在用例图中建模。
图 2-4 。扩展用例
开发用例时的一个常见错误是包含由系统本身发起的动作。用例的重点是外部实体和系统之间的交互。另一个常见的错误是包含系统技术需求的描述。请记住,用例并不关注系统将如何执行功能,而是关注从用户的角度来看,什么功能需要被整合到系统中。
在您开发了系统的用例之后,您可以开始识别将执行系统功能需求的内部系统对象。您可以使用类图来实现这一点。
活动 2-1。创建用例图
完成本活动后,您应该熟悉以下内容:
- 如何生成用例图来定义系统的范围
- 如何使用 UML 建模工具创建并记录用例图
检查安全气囊
你所属的软件用户组决定集中资源,创建一个借阅图书馆。借出的物品包括书籍、电影和电子游戏。您的任务是开发一个应用,该应用将跟踪借出物品的库存以及向小组成员借出物品的情况。在与小组成员和官员面谈后,您制定了一份 SRS 文件,其中包括以下功能要求:
- 只有用户组的成员才能借阅物品。
- 书可以借四周。
- 电影和游戏可以借一个星期。
- 如果没有人在等着借阅,可以续借。
- 会员一次最多只能借四件物品。
- 当项目过期时,会通过电子邮件向成员发送提醒。
- 过期的项目要罚款。
- 有未偿还的过期项目或罚款的成员不能借新项目。
- 秘书负责维护项目库存和采购项目以添加到库存中。
- 已经任命了一名图书管理员来跟踪借阅情况并发送过期通知。
- 图书管理员还负责收集罚款和更新罚款信息。
接下来的步骤是分析 SRS 以识别参与者和用例。
- 通过检查 SRS 文档,确定以下哪些是与系统交互的主要参与者:
- A.成员
- B.图书管理员
- C.书
- D.出纳员
- E.库存
- F.电子邮件
- G.秘书
- 一旦您确定了主要参与者,您就需要确定参与者的用例。确定与以下用例相关的参与者:
- A.请求项目
- B.目录项目
- C.借出项目
- D.加工精细
活动 2-1 答案见本章末尾。
创建用例图
尽管手工或在白板上创建 UML 图是可能的,但大多数程序员最终会求助于图表工具或计算机辅助软件工程(CASE)工具。 CASE 工具帮助您构建专业质量的图表,并使团队成员能够轻松地共享和扩充图表。市场上有许多 CASE 工具,包括 Microsoft Visio。在选择 CASE 工具之前,您应该彻底评估它是否满足您的需求,是否足够灵活。许多与高端 CASE 工具相关的高级特性很难使用,所以你花更多的时间去弄清楚 CASE 工具是如何工作的,而不是记录你的设计。
一个很好的学习绘图工具是 UMLet。UML let 使您能够创建 UML 图,而无需添加许多与高端 CASE 工具相关的高级特性。最棒的是,UMLet 是一个免费的开源工具,可以从www.umlet.com
下载。
注意这些活动使用的是 UMLet 11.5.1 单机版。这也需要在www.java.com
可用的 Java 1.6。
在下载并安装了 UMLet 之后,您可以完成以下步骤(如果您不想使用工具,您可以手工创建用例图):
-
双击 UMLet.jar 文件启动 UMLet。您将看到三个窗口。主窗口是设计图面,右上角的窗口包含 UML 对象模板,右下角的窗口是您更改或添加对象属性的地方。
-
Locate the actor template in the upper right window (see Figure 2-5). Double click the actor template. An actor will appear in the upper left corner of the design surface.
图 2-5 。定位演员模板
-
如果尚未选择,请在设计图面上选择参与者形状。在右下窗口中,将执行元形状的名称更改为 Member。
-
重复添加秘书和图书管理员执行元的过程。
-
从“模板”窗口中,双击“用例 1”形状,将其添加到设计图面。将用例的名称更改为 Request Item。
-
对另外两个用例重复步骤 5。包括当秘书向图书馆目录数据库添加新项目时将发生的目录项目用例。添加将在图书管理员处理项目请求时发生的借出项目用例。
-
From the Template window, double click the Empty Package shape and change the name to Library Loan System. Right click on the shape in the design surface and change the background color to white. Move the use case shapes inside the Library Loan System shape (see Figure 2-6).
图 2-6 。将用例放入系统边界
-
From the Template window, double click on the Communications Link shape. It is the line with no arrow heads (see Figure 2-7). On the design surface, attach one end to the Member shape and the other end to the Request Item shape.
图 2-7 。定位通信链路形状
-
重复步骤 8 两次,在“图书管理员”和“出借”项目形状之间创建一个“通信链接”形状,并在“秘书”和“目录项目”形状之间创建一个“通信链接”形状。
-
在模板窗口中,双击扩展关系箭头。将扩展箭头的尾端连接到借出项目用例,并将箭头的头部连接到请求项目用例。
-
Your completed diagram should be similar to the one shown in Figure 2-8. Save the file as UMLAct2_1 and exit UMLet.
![9781430249351_Fig02-08.jpg](https://gitee.com/OpenDocCN/vkdoc-csharp-zh/raw/master/docs/begin-cs-oop/img/9781430249351_Fig02-08.jpg)
图 2-8 。完整的用例图
理解类图
类和对象的概念是面向对象的基础。对象是用于合并数据和处理数据的过程的结构。这些对象实现了面向对象程序的功能。把一个类想象成一个对象的蓝图,把一个对象想象成一个类的实例。类定义了基于类类型的对象将包含的结构和方法。
设计者从 SRS 和用例图中识别出他们需要开发的类的潜在列表。识别类的一种方法是查看 SRS 文档中的名词短语和用例描述。如果您查看到目前为止为机票预订应用开发的文档,您可以开始确定组成系统的类。例如,您可以开发一个处理客户数据的Customer
类和一个处理航班数据的Flight
类。
一个类负责管理数据。在定义类结构时,你必须确定类负责维护什么数据。类属性定义了这些信息。例如,Flight
类将具有标识航班号、出发时间和日期、飞行持续时间、目的地、容量和可用座位的属性。类结构还必须定义将对数据执行的任何操作。Flight
类负责的操作的一个例子是当座位被预订时更新可用的座位。
类图可以帮助您可视化类的属性和操作。 图 2-9 是航班预订系统示例中使用的Flight
类的类图示例。分成三部分的矩形代表该类。矩形的上半部分显示了类的名称,中间部分列出了类的属性,下半部分列出了类执行的操作。
图 2-9 。飞行等级图
建模对象关系
在面向对象程序设计中,当程序执行时,各种对象协同工作来完成编程任务。例如,在机票预订应用中,为了预订航班座位,一个Reservation
对象必须与Flight
对象进行交互。两个对象之间存在关系,这种关系必须在程序的类结构中建模。组成程序的类之间的关系在类图中建模。分析 SRS 中的动词短语通常会揭示这些关系(这将在第三章的中详细讨论)。接下来的部分研究了类之间可能出现的一些常见关系,以及类图如何表示它们。
联合
当一个类引用或使用另一个类时,这些类就形成了一个关联。在两个类之间画一条线来表示关联,并添加一个标签来表示关联的名称。例如,在机票预订应用中,一个座位与一个航班相关联,如图图 2-10 所示。
图 2-10 。类别关联
有时一个类的单个实例与另一个类的多个实例相关联。这在连接两个类的线上有所表示。例如,当客户进行预订时,Customer
类和Reservation
类之间存在关联。一个Customer
类的实例可能与多个Reservation
类的实例相关联。放置在Reservation
类附近的 n 表示这种多重性,如图图 2-11 所示。
图 2-11 。在类图中指示多重性
还可能存在一种情况,其中一个类的实例可能与同一类的多个实例相关联。例如,Pilot
类的一个实例代表机长,而Pilot
类的另一个实例代表副驾驶。飞行员管理副驾驶。这种场景被称为自关联,通过绘制从类到自身的关联线来建模,如图图 2-12 所示。
图 2-12 。一个自我联想的班级
遗产
当多个类共享一些相同的操作和属性时,基类可以封装共性。然后子类从基类继承。这在类图中用一条实线表示,该实线带有一个指向基类的开放箭头。例如,CorporateCustomer
类和RetailCustomer
类可以从基础Customer
类中继承公共属性和操作,如图图 2-13 所示。
图 2-13 。记录继承
聚合
当一个类由其他类组合而成时,它们被归类为一个集合。这用连接分层结构中的类的实线来表示。在图中的类旁边的线上放置一个菱形表示层次结构的顶层。例如,一个为飞机维修部门设计的跟踪飞机零件的库存应用可以包含一个由各种零件类组成的Plane
类,如图图 2-14 所示。
图 2-14 。描绘聚合
关联类
随着程序的类和关联的发展,可能会出现这样的情况,一个属性不能被分配给任何一个类,而是类之间关联的结果。例如,前面提到的零件库存应用可能有一个Part
类和一个Supplier
类。因为一个零件可以有多个供应商,并且供应商供应多个零件,那么价格属性应该位于哪里呢?它不适合作为任何一个类的属性,也不应该在两个类中重复。解决方案是开发一个关联类,它管理作为关联产品的数据。在这种情况下,你可以开发一个PartPrice
类。关联和关联类之间用虚线建模,如图图 2-15 所示。
图 2-15 。联想类
图 2-16 显示了机票预订应用的演化类图。它包括已经为系统确定的类、属性和关系。与这些类相关的操作将在第三章中展开。
图 2-16 。机票预订类图
活动 2-2。创建类图
完成本活动后,您应该熟悉以下内容:
- 如何通过检查用例以及系统范围文档来确定需要构建的类
- 如何使用 UML 建模工具创建类图
识别类别和属性
检查为用户组库应用的用例开发的以下场景:
查看可用借出项目列表后,成员请求借出一个项目。图书管理员输入会员号,并检索有关未偿还贷款和任何未付罚款的信息。如果成员的未偿还贷款少于四笔,并且没有任何未偿还的罚款,则处理贷款。图书管理员检索关于借出项目的信息,以确定它当前是否被借出。如果项目可用,它将被签出给成员。
- 通过识别用例场景中的名词和名词短语,您可以了解为了执行任务,您必须在系统中包含什么类。以下哪一项将成为系统的良好候选类别?
- A.
Member
- B.
Item
- C.
Librarian
- D.
Number
- E.
Fine
- F.
Checkout
- G.
Loan
- A.
- 此时,您可以开始识别与正在开发的类相关联的属性。将开发一个
Loan
类来封装与借出项目相关的数据。下面哪个可能是Loan
类的属性?- A.
MemberNumber
- B.
MemberPhone
- C.
ItemNumber
- D.
ReturnDate
- E.
ItemCost
- F.
ItemType
- A.
活动 2-2 答案见本章末尾。
创建类图
要使用 UML Modeler 创建一个类图,请遵循以下步骤(您也可以手工创建):
-
启动 UMLet。您将看到三个窗口。主窗口是设计图面,右上角的窗口包含 UML 对象模板,右下角的窗口是您更改或添加对象属性的地方。
-
Locate the SimpleClass template in the upper right window (see Figure 2-17). Double click the SimpleClass template. A SimpleClass will appear in the upper left corner of the design surface.
图 2-17 。添加类形状
-
在右下方的属性窗口中,将类名更改为
Member
。 -
对
Loan
、Item
、Book
和Movie
级重复该程序。 -
Locate the association template in the upper right window (see Figure 2-18). Double click the association template. An association will appear in the upper left corner of the design surface.
图 2-18 。添加关联形状
-
Attach the left end of the association shape to the
Member
class and the right end to theLoan
class shape. Select the association shape and update the properties in the properties window so that they match Figure 2-19.图 2-19 。更新关联属性
-
重复步骤 5 和 6,在
Loan
类和Item
类之间创建一个“包含一个”关联形状。这应该是一对一的关联。 -
Locate the generalization arrow template in the upper right window (see Figure 2-20). Double click the generalization shape. A generalization shape will appear in the upper left corner of the design surface.
图 2-20 。添加一个概括箭头
-
Attach the tail end of the generalization arrow to the
Book
class and the head end to theItem
class shape. Select the generalization arrow and update the properties in the properties widow so that they match Figure 2-21.图 2-21 。更新综合属性
-
重复第 8 步和第 9 步,显示
Movie
类继承自Item
类。 -
Click on the
Member
class in the design window. In the properties window, add thememberNumber
,firstName
,lastName
, andeMail
attributes as shown in Figure 2-22.
![9781430249351_Fig02-22.jpg](https://gitee.com/OpenDocCN/vkdoc-csharp-zh/raw/master/docs/begin-cs-oop/img/9781430249351_Fig02-22.jpg)
图 2-22 。添加类属性
- Your completed diagram should be similar to Figure 2-23. Save the file as UMLAct2_2.
![9781430249351_Fig02-23.jpg](https://gitee.com/OpenDocCN/vkdoc-csharp-zh/raw/master/docs/begin-cs-oop/img/9781430249351_Fig02-23.jpg)
图 2-23 。完成的类图
摘要
在这一章中,你已经了解了面向对象设计过程和 UML 的目标。您学习了一些使用 UML 生成的设计文档和图表。这些包括 SRS,它定义了系统的范围;用例图,它定义了系统边界,并确定了将使用该系统的外部实体;和类图,它们对将要开发来实现系统的类的结构进行建模。
您看到了如何对应用的类结构进行建模,包括识别必要的类,识别这些类的属性,以及建立这些类之间所需的结构关系。在第三章中,你将继续学习面向对象设计。特别是,您将看到应用中的对象如何协作来实现应用的功能。
活动答案
活动 2–1 答案
- 演员是会员、图书管理员和秘书。
- A.会员,b 秘书,c 图书管理员,d 图书管理员。请求项用例与成员一起使用,目录项用例与秘书一起使用,借出项用例与图书管理员一起使用,流程精细用例与图书管理员一起使用。
活动 2–2 答案
- a、B、C、E、g,候选班有
Member
、Item
、Librarian
、Fine
、Loan
。 - 与
Loan
类相关的属性有MemberNumber
、ItemNumber
和ReturnDate
。
三、设计 OOP 解决方案:建模对象交互
前一章关注于 OOP 解决方案的静态(组织)方面的建模。它介绍并讨论了 UML 的方法论。它还研究了用例图和类图的目的和结构。这一章继续讨论 UML 建模技术,并重点关注 OOP 解决方案的动态(行为)方面的建模。本章的重点是系统中的对象必须如何相互作用,以及必须发生什么活动来实现解决方案。这是建模过程的一个重要方面。这些模型将作为对构成软件应用的类的各种方法(在第二章中确定)进行编码的基础。
阅读本章后,您应该熟悉以下内容:
- 场景的目的以及它们如何扩展用例模型
- 序列图如何对系统中对象的时间相关交互进行建模
- 活动图如何映射应用处理期间的活动流
- 图形用户界面设计的重要性以及它如何适应面向对象的设计过程
了解场景
场景帮助确定系统的对象(类实例)之间将要发生的动态交互。场景是对实现用例所记录的功能所需的内部处理的文本描述。记住用例从系统外部用户的角度描述系统的功能。一个场景详述了用例的执行。换句话说,它的目的是描述组成系统的对象必须在内部执行的步骤。
图 3-1 显示了一个视频租赁应用的流程电影租赁用例 。以下文本描述了使用案例:
- 前提条件:客户向租赁人员请求租赁一部电影。该客户是视频俱乐部的会员,并向租赁店员提供了她的会员卡和个人身份号码(p in)。验证客户的会员资格。显示客户信息,并验证客户的帐户信誉良好。
- 描述:电影确认有货。记录租赁信息,并通知客户到期日期。
- 岗位条件:无。
图 3-1 。处理电影租赁用例
以下场景描述了流程电影租赁用例的内部处理:
- 电影经核实有现货。
- 库存中的可用份数减少。
- 到期日已确定。
- 租赁信息被记录。这些信息包括电影标题、拷贝数、当前日期和到期日期。
- 客户被告知租赁信息。
这个场景描述了用例的最佳可能执行。因为异常可能发生,一个用例可以产生多个场景。例如,为流程电影租赁用例创建的另一个场景可以描述当电影没有库存时会发生什么。
在您为一个用例规划出各种场景之后,您可以创建交互图来确定哪些类的对象将参与执行场景的功能。交互图还揭示了这些对象类需要什么操作。交互图有两种风格:序列图和协作图。
序列图介绍
一个序列图模拟了当系统运行时,对象类如何随着时间的推移而相互作用。序列图是正在发生的交互的可视化二维模型,并且基于一个场景。图 3-2 显示了一个通用顺序图。
图 3-2 。通用序列图
如图 3-2 所示,从一个对象到另一个对象的消息流是水平的。交互发生的时间流程是垂直描述的,从顶部开始向下进行。根据调用顺序,对象从左到右并排放置。虚线从它们中的每一个向下延伸。这条虚线代表对象的生命线。生命线上的矩形代表对象的激活。矩形的高度表示对象激活的持续时间。
在 OOP 中,对象通过相互传递消息来进行交互。从发起对象开始到接收对象结束的箭头描述了交互。拉回到发起对象的虚线箭头表示返回消息。序列图中描述的消息将形成系统的类的方法的基础。图 3-3 显示了上一节中介绍的流程电影租赁场景的示例序列图。在这一点上,该图只模拟了电影有库存的情况。
图 3-3 。流程电影租赁顺序图
当你分析序列图时,你会对执行程序处理所涉及的对象类别有所了解;您还将了解需要创建哪些方法并附加到这些类。您还应该在类图中对序列图中描述的类和方法进行建模。这些设计文件必须不断交叉引用,并在必要时进行修订。
图 3-3 中的序列图揭示了执行流程电影租赁场景将涉及四个对象。
Customer
对象是Customer
类的一个实例,负责封装和维护关于客户的信息。RentalClerk
对象是RentalClerk
类的一个实例,负责管理租借电影所涉及的处理。RentalItem
对象是RentalItem
类的一个实例,负责封装和维护与出租视频相关的信息。Rental
对象是Rental
类的一个实例,负责封装和维护与当前租用的视频相关的信息。
消息类型
通过分析序列图,您可以确定哪些消息必须在处理中涉及的对象之间传递。在 OOP 中,消息是同步或异步传递的。
当消息以同步方式传递时,发送对象会暂停处理并等待响应,然后再继续。序列图中用闭合箭头画的线代表同步消息传递。
当对象发送异步消息时,该对象继续处理,并且不期望接收对象立即响应。序列图中用空心箭头画的线代表异步消息传递。虚线箭头通常表示响应消息。这些线如图 3-4 所示。
图 3-4 。不同类型的消息
通过研究图 3-3 中所示的流程电影租赁场景的序列图,您可以看到必须传递的消息类型。例如,RentalClerk
对象发起与RentalItem
对象的同步消息,请求关于电影拷贝是否有货的信息。然后,RentalItem
对象向RentalClerk
对象发回一个响应,表明有一份拷贝。这需要同步,因为RentalClerk
正在等待响应来处理请求。异步消息的一个例子是,当电影返回时,有人注册了电子邮件提醒。在这种情况下,将发送一条消息来启动电子邮件,但不需要响应。
递归消息
在 OOP 中,一个对象拥有一个调用它自己的另一个对象实例的操作并不罕见。这被称为递归。在序列图中,返回调用对象的消息箭头代表递归。箭头的末端指向一个更小的激活矩形,代表在原始激活矩形上绘制的第二个对象激活(见图 3-5 )。例如,Account
对象计算逾期付款的复利。为了计算几个复利周期的利息,它需要调用自己几次。
图 3-5 。绘制递归消息的图表
消息迭代
有时,会重复消息调用,直到满足某个条件。例如,在合计租金时,会重复调用和Add
方法,直到向客户收取的所有租金都被加到总数中。在编程术语中,这就是迭代。在迭代消息周围画出的矩形代表序列图中的一次迭代。迭代的绑定条件显示在矩形的左上角。图 3-6 显示了序列图中描述的一个迭代的例子。
图 3-6 。描绘了一个迭代的信息
消息约束
对象之间的消息调用可能有附加的条件约束。例如,顾客必须有良好的信誉才能被允许租借电影。将约束条件放在序列中的方括号([])内。仅当条件评估为真时,才会发送消息(参见图 3-7 )。
图 3-7 。识别条件约束
消息分支
当条件约束被绑定到消息调用时,您经常会遇到分支情况,在这种情况下,根据条件的不同,可能会调用不同的消息。图 3-8 表示请求电影租赁时的条件约束。如果租赁项目的状态是“库存中”,则向Rental
对象发送一条消息来创建租赁。如果租赁物品的状态是“缺货”,那么会向Reservation
对象发送一条消息来创建预订。围绕信息画出的矩形(图 3-8 中标为 alt)显示了根据条件可能出现的替代路径。
图 3-8 。序列图中的分支消息
活动 3-1。创建序列图
完成本活动后,您应该熟悉以下内容:
- 生成序列图来模拟对象交互
- 使用 UML 建模工具创建序列图
- 向类图中添加方法
检查场景
以下场景是为活动 2-1 中介绍的用户组库应用中的一个用例创建的。它描述了当成员从图书馆借书时所涉及的处理过程。
当一名会员提出借阅请求时,图书管理员会检查该会员的记录,以确保不存在未支付的罚款。一旦成员通过了这些检查,就会检查该物品是否可用。一旦确认了物品的可用性,就会创建一个记录物品号、会员号、结帐日期和归还日期的借出。
- 通过检查场景中的名词短语,您可以确定哪些对象将参与执行处理。被识别的对象也应该有一个在类图中描述的相应的类,这个类图已经被创建了。从描述的场景中,找出五个将执行处理的对象。
- 在对象被识别并与类图交叉引用之后,下一步是识别这些对象之间必须发生的消息传递以执行任务。你可以看看场景中的动词短语来帮助识别这些信息。例如,“请求借阅项目”短语表示成员对象和图书管理员对象之间的消息交互。场景中描述的其他交互是什么?
活动 3-1 答案见本章末尾。
创建顺序图
按照以下步骤,使用 UMLet 创建一个序列图:
-
Start UMLet. Locate the drop-down list at the top of the template window. Change the template type to Sequence (see Figure 3-9).
图 3-9 。更改模板形状类型
-
双击模板窗口中的实例形状。一个实例形状将出现在设计图面的左上角。在“属性”窗口中,将形状的名称更改为
Member
。 -
From the shapes window, locate the lifeline and activation shapes (see Figure 3-10) and add them to the
Member
instance, as shown in Figure 3-11.图 3-10 。定位生命线和激活形状
图 3-11 。向序列图添加形状
-
Repeat steps 2 and 3 to add a
Librarian
,LoanHistory
,Item
, andLoan
object to the diagram. Lay them out from left to right as shown in Figure 3-12.图 3-12 。序列图中的对象布局
-
在形状模板窗口中,双击序列消息箭头形状。将箭头的尾端连接到
Member
对象的生命线,并将箭头的头部连接到Librarian
对象的生命线。在“属性”窗口中,将消息的名称更改为“请求项” -
To create a return arrow, double-click on the solid arrow with the open arrow head in the shapes template window. In the properties window, change the first line to
lt=.<
This should change the arrow from solid to dash. Attach the tail end to theLibrarian
object and the head end to theMember
object. Change the name to “Return Loan Info.” Your diagram should look similar to Figure 3-13.图 3-13 。序列图中的消息布局
-
重复步骤 5 和 6,创建一条从
Librarian
对象到LoanHistory
对象的消息。将调用消息(实线)命名为“检查历史”将返回消息(虚线)命名为“返回历史” -
创建一条从
Librarian
对象到Item
对象的消息。将呼叫消息命名为“检查可用性”将返回消息命名为“返回可用性信息” -
创建一条从
Librarian
对象到Item
对象的消息。将调用消息命名为“更新状态”将返回消息命名为“返回更新确认” -
创建一条从
Librarian
对象到Loan
对象的消息。将调用消息命名为“创建贷款”将返回消息命名为“归还贷款确认” -
Rearrange the shapes so that your diagram looks similar to Figure 3-14. Save the diagram as UML_Act3_1.
![9781430249351_Fig03-14.jpg](https://gitee.com/OpenDocCN/vkdoc-csharp-zh/raw/master/docs/begin-cs-oop/img/9781430249351_Fig03-14.jpg)
图 3-14 。完整的序列图
向类图添加方法
在您开发了序列图之后,您开始了解必须包含在应用的各个类中的方法。通过从发起对象(客户机)到接收对象(服务器)的方法调用,可以实现序列图中描述的消息交互。被调用的方法是在服务器对象被实例化为的类中定义的。例如,序列图中的“检查可用性”消息表明 Item 类需要一个方法来处理这个消息调用。
按照以下步骤添加方法:
-
在 UMLet 中,选择 File New 来创建一个新的图表。找到模板窗口顶部的下拉列表。将模板类型更改为 Class。
-
双击简单的类形状模板。在设计窗口中选择形状。
-
在“属性”窗口中,将类名更改为
Item
。在“属性”窗口中的名称下,输入两个破折号。这将在类形状中创建新的部分。这一部分是您输入类属性的地方。 -
在“属性”窗口中,将
ItemNumber
属性添加到该类中,后跟两个破折号。这将在类形状中创建第三个部分,用于添加该类的方法。 -
Add a
CheckAvailability
and anUpdateStatus
method to the class as shown in Figure 3-15.图 3-15 。向类中添加方法
-
将图表另存为 UML_Act3_1b。
理解活动图
活动图说明了在操作或流程中需要发生的活动的流程。您可以构建活动图来查看不同关注级别的工作流。
- 高度的、系统级的关注将每个用例表示为一个活动,并在不同的用例之间绘制工作流程图。
- 中级焦点图示了特定用例中发生的工作流。
- 低层次的焦点图示了在系统的一个类的特定操作中发生的工作流。
活动图由实心圆表示的流程起点和表示从一个活动到下一个活动的流程或转换的转换箭头组成。圆角矩形代表活动,牛眼圆形代表流程的终点。例如,图 3-16 显示了一个通用的活动图,它代表了一个从活动 A 开始,进行到活动 B,然后结束的过程。
图 3-16 。通用活动图
决策点和警戒条件
通常,一个活动会有条件地跟随另一个活动。例如,为了租借视频,PIN 验证成员资格。活动图通过流程线旁边的括号(见图 3-17 )用警戒条件(必须满足的条件)表示决策点的制约性。
图 3-17 。指示决策点和保护条件
并行处理
在某些情况下,两个或多个活动可以并行运行,而不是顺序运行。垂直于过渡箭头绘制的粗实线表示路径的分割。拆分后,第二条粗实线代表合并。图 3-18 显示了电影退货处理的活动图。增量库存和移除租赁活动的发生顺序无关紧要。图中的并行路径代表了这种并行处理。
图 3-18 。活动图中描述的并行处理
活动所有权
活动图的目的是在程序进行的过程中,对活动之间的控制流进行建模。迄今为止显示的图表并没有指出哪些对象负责这些活动。为了表示活动的对象所有权,您将活动图分割成一系列垂直分区(也称为泳道)。分区顶部的对象角色负责该分区中的活动。图 3-19 显示了处理电影租赁的活动图,包括泳道。
图 3-19 。活动图中的泳道
活动 3-2。创建活动图
完成本活动后,您应该熟悉以下内容:
- 使用活动图来模拟程序完成活动时的控制流
- 使用 UML 建模工具创建活动类图
识别对象和活动
检查为用户组库应用的用例开发的以下场景:
查看可用借出项目列表后,成员请求借出一个项目。图书管理员输入会员号,并检索有关未偿还贷款和任何未付罚款的信息。如果成员的未偿还贷款少于四笔,并且没有任何未偿还的罚款,则处理贷款。图书管理员检索关于借出项目的信息,以确定它当前是否被借出。如果项目可用,它将被签出给成员。
通过识别用例场景中的名词和名词短语,您可以了解在执行活动时什么对象将执行任务。请记住,这些对象是类图中标识的类的实例。活动的开展将涉及到以下对象:Member
、Librarian
、LoanHistory
、Item
、Loan
。
动词短语有助于识别对象执行的活动。这些活动应该对应于系统中类的方法。将下列活动与适当的物体配对:
- A.请求电影
- B.过程租赁
- C.检查可用性
- D.检查会员的贷款状况
- E.更新项目状态
- F.计算到期日
- G.记录租赁信息
- H.确认租赁
活动 3-2 答案见本章末尾。
创建活动图
按照以下步骤,使用 UMLet: 创建一个活动图
-
启动 UMLet。找到模板窗口顶部的下拉列表。将模板类型更改为活动。
-
双击模板窗口中的系统框形状。系统框形状将出现在设计图面的左上角。在“属性”窗口中,将该形状的名称更改为 Member,以表示成员分区。
-
Repeat step 2 to add a partition for the
Librarian
,LoanHistory
,Item
, andLoan
objects. Align the partitions from left to right as in Figure 3-20.图 3-20 。创建活动图分区
-
从“形状”窗口中,双击“初始状态”形状,并将其添加到成员分区中。在成员分区的初始状态下,添加一个状态形状。将状态重命名为“请求项目”添加从初始状态到请求项目操作状态的转换形状(箭头)。
-
在 Librarian 分区下,添加一个“处理借出”状态和一个从请求项目状态到处理借出状态的转换形状。
-
在 LoanHistory 分区下,添加“检查成员状态”操作状态和从“处理贷款”操作到“检查成员状态”操作状态的转换形状。
-
From the Shapes window, double-click the Conditional Branch shape (diamond) and add it to the LoanHistory partition below the Check Member Status action state. Add a Transition shape from the Check Member Status state to the Conditional Branch. From the Conditional Branch, add a Transition shape to a Deny Loan state under the Librarian partition. Add a label to the Transition shape with a condition of “fail.” Also add a Transition shape to a Check Item Status action state under the Item partition with a label condition of “pass.” Your diagram should be similar to Figure 3-21.
图 3-21 。添加分支条件
-
重复步骤 7,从检查项目状态创建条件分支。如果物品有库存,请在物品分区下将转换形状添加到“更新物品状态”状态。如果该项目缺货,请在 Librarian 分区下添加一个“拒绝借出”状态的转换形状。
-
从“更新项目状态”状态,向“贷款”分区下的“记录贷款信息”状态添加一个转换形状。
-
从“记录借出信息”状态,向“图书管理员”分区下的“确认借出”状态添加一个转换形状。
-
在“形状”窗口中,单击“最终状态”形状,并将其添加到成员分区的底部。添加从拒绝贷款状态到最终操作状态的转换形状。添加另一个从“确认贷款”状态到“最终操作”状态的转换形状。
您完成的图表应该类似于图 3-22 中所示的图表。将图表保存为 UMLAct3_2 并退出 UMLet。
图 3-22 。完成的活动图
探索 GUI 设计
到目前为止,关于面向对象的分析和设计的讨论集中在应用的功能设计和内部处理的建模上。成功的现代软件应用依赖于一组丰富的图形用户界面(GUI)来向应用的用户展示该功能。
在现代软件系统中,应用最重要的一个方面是它与用户的交互有多好。用户通过在 DOS 提示符下键入神秘命令来与应用进行交互的日子已经一去不复返了。现代操作系统使用的图形用户界面在很大程度上是直观的。用户也已经习惯了商业办公应用的精致界面。用户已经开始期望 IT 部门内部开发的定制应用具有同样的易用性和直观性。
用户界面的设计不应该随意进行;相反,它应该与业务逻辑设计一起计划。大多数应用的成功是由用户对应用的反应来判断的。如果用户在与应用交互时不舒服,并且应用没有提高用户的生产力,那么它注定是失败的。对于用户来说,界面就是应用。业务逻辑代码有多原始和聪明并不重要;如果用户界面设计和实现得不好,应用将不会被用户接受。开发人员最好记住是用户驱动软件开发。
虽然 UML 不是专门为 GUI 设计而设计的,但是许多软件架构师和程序员使用 UML 图来帮助对应用的用户界面建模。
GUI 活动图
开发用户界面设计的第一步是进行任务分析,以发现用户将如何与系统交互。任务分析基于之前已经建模的用例及场景。然后,您可以开发活动图来模拟用户和系统之间的交互是如何发生的。使用前面的电影租赁应用示例,图 3-23 显示了一个活动图,该图模拟了租赁人员记录租赁信息所经历的活动。
图 3-23 。用活动图进行 GUI 建模
界面原型
在您确定了必要的任务并对其进行了优先级排序之后,您就可以开发出构成用户界面的各种屏幕的原型草图了。图 3-24 显示了客户信息屏幕的原型草图。虽然你可以用纸和笔来开发你的图表,但是有一些很好的 GUI 原型工具可以提供通用的 GUI 设计模板和链接屏幕的能力,以及其他有用的特性。一些流行的商业原型工具包括微软的 SketchFlow、Balsamiq Mockups 和 AppMockupTools。一些流行的免费工具是铅笔项目,知更鸟和原型作曲家。图 3-24 所示的原型是使用铅笔投影工具创建的。
图 3-24 。GUI 原型草图
界面流程图
一旦你原型化了不同的屏幕,你就可以使用界面流程图来建模组成用户界面的屏幕之间的关系和流程模式。图 3-25 显示了视频租赁应用的部分界面流程图。
图 3-25 。界面流程图
应用原型
一旦你完成了屏幕布局和用户界面的设计,你就可以开发一个简单的原型。原型应该包含模拟系统功能的框架代码。在这一点上,您不应该花大力气将用户界面前端与应用的业务功能集成在一起。这个想法是让用户与一个工作原型交互,在用户界面上产生反馈。重要的是用户对界面设计感到舒适,并且使用起来直观。如果界面对用户来说很麻烦,不能提高他们的生产力,那么应用注定会失败。
改进和测试用户界面的过程将是反复的,很可能会持续几个周期。一旦应用的用户界面设计和内部功能设计已经完成并原型化,应用开发周期的下一步就是开始编码应用。
摘要
本章介绍了场景、序列图、协作图和活动图。您看到了如何使用这些图来建模对象交互。此外,您还了解了如何使用一些 UML 图来帮助对应用的用户界面进行建模。
这一章和前几章的目标是向你介绍一些软件设计和 UML 中涉及到的通用建模图和概念。在第四章的中,您将获得目前为止开发的概念,并使用它们来实现一个示例案例研究的解决方案设计。
活动答案
活动 3-1 答案
Member
、Librarian
、Item
、Loan
、LoanHistory
。这五个对象包含在场景中描述的处理中。
该场景中描述的其他消息传递交互如下:
Librarian
对象用LoanHistory
对象检查Member
对象的借出历史。Librarian
对象通过Item
对象检查物品的可用性。Librarian
对象通过Item
对象更新物品的可用性。Librarian
创建一个包含贷款信息的Loan
对象。Librarian
将贷款信息返回给Member
对象。
活动 3-2 答案
A.Member
、B. Librarian
、C. Item
、D. LoanHistory
、E. Item
、F. Loan
、G. Loan
、H. Librarian
。
Member
对象负责请求电影活动。Librarian
对象负责处理租赁和确认租赁活动。LoanHistory
对象负责检查成员的贷款状态活动。Item
对象负责检查可用性和更新物品状态活动。Loan
对象负责计算到期日和记录租赁信息活动。
四、设计面向对象的解决方案:案例研究
为应用设计解决方案不是一件容易的事情。成为一名有成就的设计师需要时间和有意识的努力,这解释了为什么许多开发人员像躲避瘟疫一样躲避它。你可以研究理论,知道术语,但是真正发展你的建模技能的唯一方法是卷起袖子,弄脏你的手,开始建模。在本章中,您将了解办公用品订购系统的建模过程。虽然这不是一个复杂的应用,但是它包含了几个很好的用例,并且将由多个具有丰富的类交互的类组成。通过分析案例研究,您将更好地理解模型是如何开发的,以及各个部分是如何组合在一起的。
阅读本章后,您应该熟悉以下内容:
- 如何使用 UML 为 OOP 解决方案建模
- 要避免的一些常见 OOP 设计陷阱
开发面向对象的解决方案
在案例研究场景中,您的公司目前没有标准的部门订购办公用品的方式。每个部门分别实施自己的订购流程。因此,几乎不可能跟踪公司范围内的供应支出,这会影响预测预算和识别滥用的能力。当前系统的另一个问题是,它不允许单个联系人可以与各种供应商协商更好的交易。
因此,您被要求帮助开发一个公司范围的办公用品订购(OSO)应用。要对此系统进行建模,您需要完成以下步骤:
- 创建软件需求规范(SRS)
- 开发用例
- 绘制用例图表
- 给班级建模
- 为用户界面设计建模
创建系统需求规格
在与提议系统的各种客户面谈之后,您开发 SRS。请记住第二章中的内容,SRS 确定了系统需求的范围,定义了系统边界,并确定了系统的用户。
您已经确定了以下系统用户:
- 购买者:发起供货请求
- 部门经理:跟踪并批准部门采购员的供货请求
- 供应商处理申请:接收系统生成的订单文件
- 采购经理:更新供应目录,跟踪供应请求,并登记交付的物品
您已经确定了以下系统要求:
- 用户必须通过提供用户名和密码来登录系统。
- 购买者将查看可供订购的供应品列表。
- 购买者将能够按类别过滤供应品清单。
- 采购员可以在一个采购请求中请求多个供应。
- 部门经理可以为部门申请一般用品。
- 部门经理必须在每周末批准或拒绝其部门的供应请求。
- 如果部门经理拒绝请求,他们必须提供一个简短的解释,概述拒绝的原因。
- 部门经理必须跟踪其部门内的支出,并确保有足够的资金用于批准的供应请求。
- 采购经理维护供应目录,并确保其准确和最新。
- 采购经理在收到供应品时进行登记,并组织供应品进行配送。
- 已被请求但未被批准的供应请求标有待定状态。
- 已批准的供应请求标有状态已批准,并生成订单。
- 订单生成后,包含订单详细信息的文件将被放入订单队列。一旦订单被放入队列,它将被标记为状态已放置。
- 一个单独的供应商处理应用将从队列中检索订单文件,解析文档,并将行项目分发到适当的供应商队列。供应商处理应用会定期从供应商队列中检索订单,并将它们发送给供应商。(这是由一个单独的团队开发的。)
- 当订单的所有项目都被登记时,订单被标记为已履行状态,买方被告知订单已准备好提货。
开发用例
在生成 SRS 并让适当的系统用户签字后,下一个任务是开发用例,它将从用户的角度定义系统将如何运行。开发用例的第一步是定义参与者。请记住第二章中的角色代表将与系统交互的外部实体(人类或其他系统)。从 SRS 中,您可以确定以下将与系统交互的参与者:
- 买方
- 部门经理
- 采购经理
- 供应商处理申请
既然您已经确定了参与者,下一步就是确定参与者将涉及的各种用例。通过检查 SRS 中的需求陈述,您可以识别各种用例。例如,“用户必须通过提供用户名和密码来登录系统”这句话表明需要一个登录用例。表 4-1 确定了 OSO 应用的用例。
表 4-1 。OSO 应用的用例
名字 | 演员 | 描述 |
---|---|---|
注册 | 采购员、部门经理、采购经理 | 用户会看到登录屏幕。然后,他们输入用户名和密码。他们要么点击登录,要么点击取消。登录后,他们会看到一个包含产品信息的屏幕。 |
查看供应目录 | 采购员、部门经理、采购经理 | 用户会看到一个包含供应品列表的目录表。该表包含供应名称、类别、描述和成本等信息。用户可以按类别过滤供应品。 |
购买申请 | 采购员、部门经理 | 购买者在表格中选择商品,然后点击按钮将它们添加到购物车中。一个单独的表显示了他们购物车中的商品、所请求的每个商品的数量和成本,以及请求的总成本。 |
部门采购请求 | 部门经理 | 部门经理在表格中选择商品,然后单击按钮将它们添加到购物车中。一个单独的表显示了他们购物车中的商品、所请求的每个商品的数量和成本,以及请求的总成本。 |
请求审查 | 部门经理 | 部门经理会看到一个屏幕,其中列出了他们部门成员的所有待定供应请求。他们审查请求,并将其标记为批准或拒绝。如果他们拒绝请求,他们输入一个简短的解释。 |
跟踪支出 | 部门经理 | 部门经理会看到一个屏幕,上面列出了部门成员每月的支出以及部门的总支出。 |
维护目录 | 采购经理 | 采购经理能够更新产品信息、添加产品或将产品标记为停产。管理员还可以更新类别信息、添加类别,以及将类别标记为停止使用。 |
项目签入 | 采购经理 | 采购经理看到一个输入订单号的屏幕。然后,采购经理会看到为订单列出的行项目。已接收的物料被标记。当收到订单的所有项目时,它会被标记为已履行。 |
下单 | 供应商处理应用 | 供应商处理应用检查队列中的外发订单文件。文件被检索、解析并发送到适当的供应商队列。 |
绘制用例图表
既然您已经确定了各种用例以及参与者,那么您就准备好构建用例的图表了。图 4-1 显示了一个用 UMLet 开发的初步用例模型,它在第二章中有介绍。
图 4-1 。初步的 OSO 用例图
在您绘制了用例之后,您现在要寻找用例之间可能存在的任何关系。可能存在的两种关系是包含关系和扩展关系。从第二章的讨论中记住,当一个用例包含另一个用例时,被包含的用例需要作为先决条件运行。例如,OSO 应用的登录用例需要包含在查看供应目录用例中。将登录作为一个单独的用例的原因是,登录用例可以被一个或多个其他用例重用。在 OSO 应用中,登录用例也将包含在跟踪支出用例中。图 4-2 描绘了这种包含关系。
图 4-2 。包括登录用例
注意在一些建模工具中,包含关系可以在用例图中通过 uses 关键字来表示。
当一个用例将扩展初始用例的行为时,扩展关系存在于两个用例之间。在 OSO 应用中,当经理提出采购请求时,她可以表明她将为部门提出采购请求。在这种情况下,部门购买请求用例成为购买请求用例的扩展。 图 4-3 图示此扩展。
图 4-3 。扩展购买请求用例
在分析了系统需求和用例之后,你可以通过分解应用并分阶段开发来使系统开发更易于管理。例如,您可以首先开发应用的购买请求部分。接下来,您可以开发请求检查部分,然后是项目签入部分。本章的其余部分集中在应用的购买请求部分。员工和部门经理将使用应用的这一部分来提出购买请求。图 4-4 显示了这个阶段的用例图。
图 4-4 。采购申请用例图
开发班级模型
开发类模型包括几项任务。首先确定类,然后添加属性、关联和行为。
识别类别
在您确定了各种用例之后,您可以开始确定系统需要包含的类,以执行用例中描述的功能。为了识别这些类,您需要深入到每个用例中,并定义一系列执行用例所需的步骤。识别用例描述中的名词短语也很有帮助。名词短语通常是将需要的类的良好指示器。
例如,以下步骤描述了查看供应目录用例:
- 用户已登录并被分配了用户状态级别。(这是前提条件。)
- 向用户呈现包含供应品列表的目录表。该表包含供应名称、类别、描述和成本等信息。
- 用户可以按类别过滤供应品。
- 用户可以选择注销或提出购买请求。(这是岗位条件。)
从这个描述中,您可以确定一个负责从数据库中检索产品信息并过滤所显示产品的类。这个类的名称将是ProductCatalog
类。
检查处理购买请求的用例描述中的名词短语揭示了 OSO 应用的候选类,如表 4-2 中所列。
表 4-2 。用于提出购买请求的候选类别
用例 | 候选类别 |
---|---|
注册 | 用户、用户名、密码、成功、失败 |
查看供应目录 | 用户、目录表、供应品列表、信息、供应品名称、类别、描述、成本 |
购买申请 | 购买者、物品、购物车、编号、请求的物品、成本、总成本 |
部门采购请求 | 部门经理、物品、购物车、编号、请求的物品、成本、总成本、部门采购请求 |
既然您已经确定了候选类,那么您需要排除那些表示冗余的类。例如,对项目和行项目的引用将代表相同的抽象。您还可以消除表示属性而不是对象的类。用户名、密码和费用是表示属性的名词短语的例子。有些类是模糊的,或者是其他类的概括。用户其实是采购员和经理的概括。类也可能实际上引用相同的对象抽象,但是指示对象的不同状态。例如,供应请求和订单在批准前和批准后代表相同的抽象。您还应该过滤掉表示实现结构的类,如 list 和 table。例如,购物车实际上是组成订单的订单项目的集合。
使用这些排除标准,您可以将类别列表缩减到以下候选类别:
Employee
DepartmentManager
Order
OrderItem
ProductCatalog
Product
现在,您可以开始为 OSO 应用的购买请求部分设计类图了。图 4-5 显示了 OSO 应用的初步类图。
图 4-5 。初步 OSO 类图
向类添加属性
开发类模型的下一个阶段是确定类必须实现的抽象层次。您可以确定哪些州信息与 OSO 应用相关。这个必需的状态信息将通过类的属性来实现。分析Employee
类的系统需求揭示了对登录名、密码和部门的需求。您还需要一个标识符,比如员工 ID,来唯一地标识不同的员工。对经理的采访显示,需要包括雇员的名和姓,以便他们可以通过名字跟踪支出。表 4-3 总结了将包含在 OSO 类中的属性。
表 4-3。 OSO 类属性
类 | 属性 | 类型 |
---|---|---|
Employee |
EmployeeID |
|
LoginName |
||
Password |
||
Department |
||
FirstName |
||
整数 | ||
串 | ||
串 | ||
串 | ||
串 | ||
串 | ||
DepartmentManager |
EmployeeID |
|
LoginName |
||
Password |
||
Department |
||
FirstName |
||
整数 | ||
串 | ||
串 | ||
串 | ||
串 | ||
串 | ||
Order |
OrderNumber |
|
OrderDate |
||
长 | ||
日期 | ||
字符串 | ||
OrderItem |
ProductNumber |
|
Quantity |
||
字符串 | ||
整数 | ||
小数 | ||
Product |
ProductNumber |
|
ProductName |
||
Description |
||
UnitPrice |
||
Category |
||
字符串 | ||
字符串 | ||
字符串 | ||
小数 | ||
字符串 | ||
字符串 | ||
ProductCatalog |
没有人 |
图 4-6 显示了到目前为止已经确定了类属性的 OSO 类图。
图 4-6 。添加了属性的购买请求组件类图
识别类别关联
开发过程的下一步是对 OSO 应用中存在的类关联进行建模。如果你研究了用例以及 SRS,你就能理解你需要在类结构设计中加入什么类型的关联。
注意你可能会发现你需要进一步细化 SRS 来暴露类关联。
例如,一个雇员将与一个订单相关联。通过检查关联的多样性,您会发现一个雇员可以有多个订单,但是一个订单只能与一个雇员相关联。图 4-7 模拟了这种关联。
图 4-7 。描述雇员类和订单类之间的关联
当您开始识别类属性时,您会注意到Employee
类和DepartmentManager
类有许多相同的属性。这是有道理的,因为经理也是员工。在本应用中,经理代表具有特殊行为的员工。这种专门化由继承关系来表示,如图图 4-8 所示。
图 4-8 。DepartmentManager 类继承自 Employee 类
以下陈述总结了 OSO 类结构中的关联:
- 一个
Order
是一个OrderItem
对象的集合。 - 一个
Employee
可以有多个Order
对象。 - 一个
Order
与一个Employee
相关联。 ProductCatalog
与多个Product
对象相关联。- 一个
Product
与ProductCatalog
相关联。 - 一个
OrderItem
与一个Product
相关联。 - 一个
Product
可能与多个OrderItem
对象相关联。 - 一个
DepartmentManager
是一个具有专门行为的Employee
。
图 4-9 显示了这些不同的关联(为了清楚起见,不包括类属性)。
图 4-9 。添加了关联的采购请求组件类图
类行为建模
既然您已经勾勒出了类的初步结构,那么您就可以对这些类如何交互和协作进行建模了。这个过程的第一步是深入到用例描述中,并创建一个用例将如何执行的更详细的场景。下面的场景描述了执行登录用例的一个可能的顺序。
- 用户会看到一个登录对话框。
- 用户输入登录名和密码。
- 用户提交信息。
- 检查并验证名称和密码。
- 向用户呈现供应请求屏幕。
尽管这个场景描述了登录用例中最常见的处理,但是您可能需要其他场景来描述预期的替代结果。以下场景描述了登录用例的另一种处理方式:
- 用户会看到一个登录对话框。
- 用户输入登录名和密码。
- 用户提交信息。
- 已检查名称和密码,但无法验证。
- 用户被告知不正确的登录信息。
- 再次向用户显示登录对话框。
- 用户要么重试,要么取消登录请求。
在这一点上,它可能有助于创建用例场景的可视化表示。记住第三章中的活动图经常被用来可视化用例处理。图 4-10 显示了为登录用例场景构建的活动图。
图 4-10 。描述登录用例场景的活动图
在分析了用例场景中所涉及的过程之后,您现在可以将注意力转向为系统的类分配必要的行为。为了帮助识别需要发生的类行为和交互,你构建一个序列图,如第三章中所讨论的。
图 4-11 显示了登录用例场景的序列图。Purchaser
(UI )类调用已经分配给Employee
类的Login
方法。该消息返回指示登录是否已被验证的信息。
图 4-11 。描述登录用例场景的序列图
接下来,让我们分析一下视图供应目录用例。以下场景描述了用例:
- 用户已登录并通过验证。
- 用户查看包含产品信息的目录表,包括供应名称、类别、描述和价格。
- 用户选择按类别过滤表格,选择一个类别,然后刷新表格。
从这个场景中,您可以看到您需要一个ProductCatalog
类的方法来返回产品类别列表。Purchaser
类将调用这个方法。ProductCatalog
类需要的另一个方法是返回按类别过滤的产品列表。图 4-12 中的序列图显示了Purchaser
(UI)类和ProductCatalog
类之间的交互。
图 4-12 。描述查看供应目录方案的序列图
以下场景是为购买请求用例开发的:
- 一名采购人员已经登录并被确认为员工。
- 购买者从产品目录中选择商品,并将它们添加到订单请求(购物车)中,指明所请求的每个商品的数量。
- 在完成订单的项目选择之后,购买者提交订单。
- 订单请求信息被更新,订单 ID 被生成并返回给购买者。
从这个场景中,您可以确定需要创建的Order
类的AddItem
方法。该方法将接受产品 ID 和数量,然后返回订购产品的小计。Order
类需要调用OrderItem
类的方法,这将创建一个订单项的实例。您还需要一个Order
类的SubmitOrder
方法,该方法将提交请求和生成订单的退货订单 ID。图 4-13 显示了该场景的相关序列图。
图 4-13 。描述购买请求场景的序列图
需要包括的一些其他场景是从购物车中删除商品、更改购物车中商品的数量以及取消订单流程。您还需要为部门采购请求用例包含类似的场景并创建类似的方法。在分析了需要发生的场景和交互之后,你可以为应用的购买请求部分开发一个类图,如图 4-14 所示。
图 4-14 。采购申请类图
开发用户界面模型设计
在应用设计过程的这一点上,您不希望提交特定的 GUI 实现(换句话说,特定于技术的实现)。然而,对应用的 GUI 所需的一些公共元素和功能进行建模是有帮助的。这将帮助您创建一个原型用户界面,您可以用它来验证已经开发的业务逻辑设计。用户将能够与原型互动,并提供反馈和逻辑设计的验证。
您需要实现的第一个原型屏幕是用于登录的屏幕。您可以构建一个活动图来帮助定义用户登录系统时需要执行的活动,如图图 4-15 所示。
图 4-15 。描述用户登录活动的活动图
分析活动图可以发现,您可以将登录屏幕实现为一个相当通用的界面。该屏幕应该允许用户输入用户名和密码。它应该包括一种方式来表明用户是作为雇员还是经理登录的。经理必须以员工身份登录才能提出购买请求,以经理身份登录才能批准请求。最后的要求是包括一个让用户中止登录过程的方法。图 4-16 显示了登录屏幕的原型。
图 4-16 。登录屏幕原型
您需要考虑的下一个屏幕是产品目录屏幕。图 4-17 描述了查看和过滤产品的活动图。
图 4-17 。描述产品浏览活动的活动图
活动图显示屏幕需要显示产品和产品信息的表格或列表。用户必须能够按类别过滤产品,这可以通过从类别列表中选择一个类别来启动。用户还需要能够发起订单请求或退出应用。图 4-18 显示了可用于查看产品的原型屏幕。单击添加到订单按钮将产品添加到订单,并启动订单详细信息屏幕,用户可以在其中调整订单数量。
图 4-18 。查看产品屏幕原型
应用的这一部分需要原型化的最后一个屏幕是购物车界面。这将有助于从订单请求中添加和删除项目。它还需要允许用户提交订单或中止订单请求。图 4-19 显示了订单请求屏幕的原型。单击添加按钮将启动之前的产品目录屏幕,允许用户添加其他行项目。移除按钮将移除选中的行项目。用户可以通过单击向上和向下箭头来更改行项目的数量。
图 4-19 。订单详细信息屏幕原型
这就完成了 OSO 应用这一阶段的初步设计。你应用了你在第二章和第三章中学到的知识来设计模型。接下来,让我们回顾一下在这个过程中要避免的一些常见错误。
避免一些常见的 OOP 设计陷阱
当你开始为自己的 OOP 设计建模时,你希望确保遵循最佳实践。以下是一些你应该避免的常见陷阱
- 不要让用户参与过程:值得强调的是,用户是你产品的消费者。他们是定义系统的业务流程和功能需求的人。
- 尝试一次性建模整个解决方案:当开发复杂系统时,将系统设计和开发分解成可管理的组件。计划分阶段生产软件。这将提供更快的建模、开发、测试和发布周期。
- 努力打造完美模型:没有一个模型从一开始就是完美的。成功的建模者明白建模过程是迭代的,模型在整个应用开发周期中不断更新和修改。
- 认为只有一种真正的建模方法:正如有许多不同的同样可行的 OOP 语言一样,也有许多同样有效的软件开发建模方法。选择一个最适合你和手头项目的。
- 重新发明轮子:寻找模式和可重用性。如果您分析应用试图解决的许多业务流程,就会出现一组一致的建模模式。创建一个存储库,您可以在其中从一个项目到另一个项目,从一个程序员到另一个程序员利用这些现有的模式。
- 让数据模型驱动业务逻辑模型:首先开发数据模型(数据库结构),然后在其上构建业务逻辑设计,这通常不是一个好主意。解决方案设计者应该首先询问需要解决什么业务问题,然后建立数据模型来解决问题。
- 混淆问题域模型和实现模型:在设计应用时,你应该开发两个不同但互补的模型。领域模型设计描述了项目的范围和实现业务解决方案所涉及的处理。这包括将涉及哪些对象,它们的属性和行为,以及它们如何相互作用和关联。领域模型应该是与实现无关的。您应该能够使用同一个域模型作为几种不同的特定于架构的实现的基础。换句话说,您应该能够采用相同的域模型,并使用 Visual Basic 富客户端、两层架构或 C#(或 Java)n 层分布式 web 应用来实现它。
摘要
现在,您已经分析了 OOP 应用的领域模型,您已经准备好将设计转化为实际的实现了。本书的下一部分将向你介绍 C# 语言。你会看到 .NET 框架,并了解如何在框架之上构建 C# 应用。将向您介绍如何使用 Visual Studio IDE,并熟悉 C# 语言的语法。下一节还将演示在 C# 中实现 OOP 结构的过程,比如类结构、对象实例化、继承和多态。利用你新获得的技能,你将重温本章介绍的案例研究,并应用这些技能将应用设计转化为一个功能性的应用。
五、.NET 框架和 Visual Studio 简介
业务应用编程已经从两层、紧耦合的模型发展成多层、松耦合的模型,通常涉及通过互联网或公司内部网的数据传输。为了让程序员更有效率并处理这种模型的复杂性,微软开发了 .NET 框架。为了有效地用 C# 编程,你需要理解构建它的底层框架。
阅读本章后,您应该熟悉以下内容:
- 那个 .NET 框架
- 公共语言运行库(CLR)的功能
- 实时(JIT)编译器的工作原理
- 那个 .NET Framework 基类库
- 命名空间和程序集
- Visual Studio 集成开发环境的功能
介绍 .NET 框架
那个 .NET Framework 是基础类的集合,旨在提供运行应用所需的公共服务。让我们看看的目标 .NET 框架,然后查看它的组件。
的目标 .NET 框架
微软设计了 .NET 框架,并考虑到某些目标。下面几节研究这些目标以及 .NET 框架实现了它们。
行业标准的支持
微软想要的是 .NET 框架基于行业标准和实践。因此,该框架严重依赖于行业标准,如可扩展标记语言(XML)、HTML 5 和 OData。微软还向欧洲计算机制造商协会(ECMA)提交了一份公共语言基础设施(CLI)工作文件,该协会负责监督计算机行业的许多公共标准。
CLI 是创建符合 .NET 框架。第三方供应商可以使用这些规范来创建。符合. NET 的语言编译器;例如,交互式软件工程(ISE)已经为 Eiffel 创建了一个. NET 编译器。第三方供应商也可以创建一个 CLR 来允许 .NET 兼容的语言在不同的平台上运行。例如,Mono 是 CLR 的开源、跨平台实现,它赋予 C# 应用在 Linux 平台上运行的能力。
展开性
为了创建一个高效的编程环境,微软实现了 .NET 框架必须是可扩展的。于是,微软向开发者公开了框架类层次结构。通过继承和接口,您可以轻松地访问和扩展这些类的功能。例如,您可以创建一个按钮控件类,它不仅从 .NET Framework,而且以应用所需的独特方式扩展了基本功能。
微软还使底层操作系统的使用变得更加容易。通过在基于类的层次结构中重新打包和实现 Windows 操作系统应用编程接口(API)函数,微软使面向对象编程人员能够更直观、更容易地使用底层操作系统提供的功能。
统一编程模型
微软的另一个重要目标是 .NET 框架是跨语言独立和集成的。为了实现这个目标,所有支持公共语言规范(CLS)的语言都编译成相同的中间语言,支持相同的基本数据类型集,并公开相同的代码可访问性方法集。结果,不仅用不同的 CLS 兼容语言开发的类可以无缝地相互通信,而且你还可以跨语言实现 OOP 结构。例如,您可以开发一个用 C# 编写的类,该类继承自用 Visual Basic (VB)编写的类。微软已经开发了几种支持 .NET 框架。除了 C#,这些语言还有 VB.NET、托管 C++、JScript 和 F#。除了这些语言之外,许多第三方供应商还开发了其他流行语言的版本,用于在 .NET 框架,比如 Pascal 和 Python。
更容易部署
微软需要一种方法来简化应用部署。以前的发展 .NET Framework 中,当部署组件时,组件信息必须记录在系统注册表中。许多这些组件,尤其是系统组件,被几个不同的客户端应用使用。当客户端应用调用该组件时,会搜索注册表以确定使用该组件所需的元数据。如果部署了较新版本的组件,它将替换旧组件的注册表信息。通常,新组件与旧版本不兼容,导致现有客户端出现故障。您可能在安装了一个服务包后遇到过这个问题,该服务包导致的问题比它修复的问题还多!
那个 .NET Framework 通过将用于处理组件的元数据存储在一个名为清单的文件中来解决这个问题,该文件打包在包含组件代码的程序集中。程序集是包含运行应用所需的代码、资源和元数据的包。默认情况下,程序集被标记为私有,并与客户端程序集放在同一目录中。这确保了组件组合不会被无意中替换或修改,并且还允许更简单的部署,因为不需要使用注册表。如果需要共享一个组件,它的程序集被部署到一个称为全局程序集缓存(GAC)的特殊目录中。程序集的清单包含版本信息,因此组件的较新版本可以与 GAC 中的较旧版本并行部署。默认情况下,客户端程序集继续请求和使用它们打算使用的组件版本。当安装组件的新版本时,旧的客户端程序集将不再失败。
改进的内存管理
为 Windows 平台开发的程序的一个常见问题是内存管理。通常,这些程序会导致内存泄漏。当程序从操作系统分配内存,但在完成内存工作后未能释放内存时,就会发生内存泄漏。该内存不再可用于其他应用或操作系统,导致计算机运行缓慢,甚至停止响应。当程序打算长时间运行时,例如在后台运行的服务,这个问题就更复杂了。为了解决这个问题 .NET Framework 使用不确定的终结。框架使用垃圾收集对象,而不是依赖应用来释放未使用的内存。垃圾收集器定期扫描未使用的内存块,并将它们返回给操作系统。
改进的安全模型
在当今高度分布式、基于互联网的应用中实现安全性是一个极其重要的问题。过去,安全性主要集中在应用的用户身上。当用户登录到应用时,会检查安全身份,当应用调用远程服务器和数据库时,会传递用户的身份。对于当今企业级的松散耦合系统来说,这种类型的安全模型被证明是低效且难以实现的。为了使安全性更容易实现和更健壮 .NET Framework 使用代码标识和代码访问的概念。
创建程序集时,它会被赋予一个唯一的标识。创建服务器程序集时,您可以授予访问权限和权利。当客户端程序集调用服务器程序集时,运行库将检查客户端的权限和权利,然后相应地授予或拒绝对服务器代码的访问。因为每个程序集都有一个标识,所以还可以通过操作系统限制对程序集的访问。例如,如果用户从 Web 下载一个组件,您可以限制该组件在用户系统上读写文件的能力。
的组件 .NET 框架
现在您已经看到了的一些主要目标 .NET 框架,我们来看看它包含的组件。
公共语言运行时
的基本组件 .NET 框架是 CLR。CLR 管理正在执行的代码,并在代码和操作系统之间提供一个抽象层。CLR 内置了以下机制:
- 将代码加载到内存中并准备执行
- 将代码从中间语言转换为本机代码
- 管理代码执行
- 管理代码和用户级安全性
- 自动释放和释放内存
- 调试和跟踪代码执行
- 提供结构化异常处理
框架基础类库
构建在 CLR 之上的是 .NET 框架基础类库。这个类库中包括引用类型和值类型,它们封装了对系统功能的访问。类型是数据结构。引用类型是一种复杂类型,例如类和接口。值类型是简单类型,例如整数或布尔。程序员使用这些基类和接口作为构建应用、组件和控件的基础。基本类库包括封装数据结构、执行基本输入/输出操作、调用安全管理、管理网络通信以及执行许多其他功能的类型。
数据类别
构建在基类之上的是支持数据管理的类。这组类通常被称为 ADO.NET。使用 ADO.NET 对象模型,程序员可以通过托管提供程序访问和管理存储在各种数据存储结构中的数据。微软已经编写并调整了 ADO.NET 类和对象模型,以便在松散耦合、互不连接的多层环境中高效工作。ADO.NET 不仅公开数据库中的数据,还公开与数据相关联的元数据。数据被公开为一种小型关系数据库。这意味着您可以在与数据源断开连接的情况下获取和使用数据,并在以后将数据与数据源同步。
微软已经为几个数据提供者提供了支持。存储在 Microsoft SQL Server 中的数据可以通过本机 SQL 数据提供程序进行访问。OLEDB 和开放式数据库连接管理(ODBC)提供程序是目前通过 OLEDB 或 ODBC 标准 API 公开的系统的两个通用提供程序。因为这些托管数据提供程序不直接与数据库引擎交互,而是与非托管提供程序进行通信,然后由非托管提供程序与数据库引擎进行通信,所以使用非本机数据提供程序比使用本机提供程序效率更低、功能更弱。由于 .NET 框架和微软对基于开放标准的承诺,许多数据存储供应商现在为他们的系统提供本地数据提供者。
建立在 ADO.NET 提供者模型之上的是 ADO.NET 实体框架。实体框架在数据库的关系数据结构和编程语言的面向对象结构之间架起了一座桥梁。它提供了一个对象/关系映射(ORM)框架,使得程序员无需为数据访问编写大部分管道代码。该框架提供诸如变更跟踪、身份解析和查询翻译等服务。程序员使用语言集成查询(LINQ)来检索数据,并将数据作为强类型对象来操作。第十章详细介绍 ADO.NET 和数据访问。
Windows 应用
在此之前。根据您是使用 C++还是 Visual Basic 进行开发,开发 Windows 图形用户界面(GUI)会有很大的不同。尽管用 VB 开发 GUI 很容易,而且可以很快完成,但 VB 开发人员与 Windows API 的底层功能隔离开来,没有完全接触到它。另一方面,尽管接触到了 Windows API 的全部功能,但用 C++开发 GUI 是非常乏味和耗时的。与 .NET Framework 中,微软引入了一组基类,在。与. NET 兼容的语言。这使得 Windows GUI 开发在各种 .NET 支持的编程语言,结合了开发的简易性和 API 的全部特性。
除了传统的 Windows 窗体和控件(自 Windows 操作系统出现以来就一直存在)之外 .NET Framework 包括一组类,统称为 Windows 演示基础(WPF)。WPF 集成了一个渲染引擎,旨在利用现代图形硬件和丰富的操作系统,如 Windows 7。它还包括应用开发功能,如控件、数据绑定、布局、图形和动画。有了 WPF 的类集,程序员可以创建提供非常引人注目的用户体验的应用。在第十一章中,你将更深入地了解如何构建基于 WPF 的应用。
网络应用
那个 .NET Framework 公开了一组基本类,可用于在 web 服务器上创建向启用 web 的客户端公开的用户界面和服务。这些类统称为 ASP.NET。使用 ASP.NET,您可以开发一个用户界面,它可以动态地响应发出请求的客户端设备的类型。在运行时 .NET Framework 负责发现发出请求的客户端的类型(浏览器类型和版本)并公开适当的接口。运行在 Windows 客户机上的 web 应用的 GUI 变得更加健壮,因为 .NET Framework 公开了许多 API 功能,这些功能以前只向传统的基于 Windows 窗体的 C++和 VB 应用公开。使用 .NET Framework 的一个优点是服务器端代码可以用任何。符合. NET 的语言。之前 .NET 中,服务器端代码必须用脚本语言编写,如 VBScript 或 JScript。
Visual Studio 2012 和 .NET framework 4.5 支持几种不同的创建 web 应用的模型。您可以基于 ASP.NET web 窗体项目、ASP.NET MVC 项目或 Silverlight 应用项目创建 Web 应用。第十二章涵盖了开发网络应用。
Windows 应用商店应用
一种新类型的 Windows 应用可用于 Windows 8 操作系统:Windows 商店应用。Windows 应用商店应用面向平板电脑和手机等利用触摸屏进行用户输入和持续互联网连接的设备。构建 Windows 应用商店应用有两种选择。您可以使用 .NET 语言(C#/VB/C++)与 XAML 结合使用,或者您可以将 JavaScript 与 HTML5 结合使用。Windows 应用商店应用是使用 WinRT API 构建的,这类似于针对。网络工作。微软甚至增加了一个. NET 框架 API 作为 WinRT API 的包装。这意味着您可以使用 .NET Framework 来使用您已经熟悉的编程语言和设计经验创建出色的 Windows 应用商店应用。事实上,正如你将在第十三章中看到的,使用 C# 和 XAML 创建 Windows Store 应用与构建 WPF 应用非常相似。
应用服务程序
包括在 .NET Framework 是基类和接口支持,用于公开可由其他应用使用的服务。在…之前 .NET 框架,用 C++和 VB 开发的应用使用了分布式组件对象模型(DCOM)技术。DCOM 是一种在分布于联网计算机上的软件组件之间进行通信的技术。因为 DCOM 是基于二进制标准的,所以通过防火墙和互联网的应用到应用的通信不容易实现。DCOM 的专有性质也限制了能够有效使用通过 COM 公开服务的应用并与之交互的客户端类型。
微软通过互联网标准公开服务来解决这些限制。包括在 .NET 框架是一组类,统称为 Windows 通信基础(WCF) 。使用 WCF,您可以将数据作为消息从一个应用发送到另一个应用。消息传输和内容可以根据消费者和环境轻松更改。例如,如果服务是通过 Web 公开的,那么可以使用基于 HTTP 的文本消息。另一方面,如果客户端在同一个公司网络上,则可以使用 TCP 上的二进制消息。
和。除了. NET 4.5 之外,微软还引入了一种新的创建 HTTP 服务的框架,称为 ASP.NET Web API。使用这个 API,您可以构建能够到达各种客户端的服务,比如 web 浏览器和移动设备。虽然您可以使用 WCF 创建这些服务,但新的 Web API 使其更容易实现,并包括内容协商、查询合成和灵活托管等功能。在第十四章中,你将使用 ASP.NET Web API 和 WCF 创建和消费应用服务。
使用 .NET 框架
使用 .NET Framework,您应该了解它是如何构造的,以及托管代码是如何编译和执行的。 .NET 应用被组织并打包成程序集。由执行的所有代码 .NET 运行库必须包含在程序集中。
了解程序集和清单
程序集包含运行应用所需的代码、资源和清单(关于程序集的元数据)。组件可以被组织成一个文件,其中所有的信息都被合并到一个动态链接库(DLL)文件或可执行(EXE)文件中,或者被组织成多个文件,其中信息被合并到单独的 DLL 文件、图形文件和清单文件中。程序集的主要功能之一是形成类型、引用和安全性的边界。大会的另一个重要功能是组成一个部署单位。
程序集最重要的部分之一是清单;事实上,每个程序集都必须包含一个清单。清单的目的是描述程序集。它包含程序集的标识、程序集向客户端公开的类和其他数据类型的说明、此程序集需要引用的任何其他程序集以及运行程序集所需的安全详细信息。
默认情况下,当创建程序集时,它被标记为私有。程序集的副本必须放在使用它的任何客户端程序集的同一目录或 bin 子目录中。如果程序集必须在多个客户端程序集之间共享,则它被放在全局程序集缓存(GAC) 中,这是一个特殊的 Windows 文件夹。若要将私有程序集转换为共享程序集,必须运行实用程序来创建加密密钥,并且必须用密钥对程序集进行签名。对程序集签名后,必须使用另一个实用工具将共享程序集添加到 GAC 中。通过制定创建和公开共享程序集的严格要求,Microsoft 试图确保不会发生恶意篡改共享程序集的情况。这也确保了同一程序集的新版本可以存在,而不会破坏使用旧程序集的当前应用。
引用程序集和命名空间
来制造 .NET 框架更易于管理,微软给了它一个层次结构。这种分层结构被组织成所谓的名称空间。通过将框架组织到名称空间中,命名冲突的机会大大减少了。将框架的相关功能组织到名称空间中也极大地增强了其对开发人员的可用性。例如,如果您想构建一个窗口的 GUI,那么您需要的功能很可能存在于System.Windows
名称空间中。
所有的 .NET Framework 类驻留在系统命名空间中。系统命名空间按功能进一步细分。使用数据库所需的功能包含在System.Data
名称空间中。一些名称空间有几层深度;例如,用于连接到 SQL Server 数据库的功能包含在System.Data.SqlClient
名称空间中。
程序集可以组织成单个命名空间或多个命名空间。几个程序集也可以组织到同一个命名空间中。
中的类进行访问 .NET Framework,您需要在代码中引用包含命名空间的程序集。然后,您可以通过提供类的完全限定名来访问程序集中的类。例如,如果您想在表单中添加一个文本框,您可以创建一个System.Windows.Controls.TextBox
类的实例,如下所示:
private System.Windows.Controls.TextBox newTextBox;
幸运的是,在 C# 中,您可以在代码文件的顶部使用 using 语句,这样就不需要在代码中不断引用完全限定名:
using System.Windows.Controls;
private TextBox newTextBox;
编译和执行托管代码
什么时候 .NET 代码被编译,它被转换成. NET 可移植可执行(PE)文件。编译器将源代码翻译成微软中间语言(MSIL)格式。MSIL 是独立于 CPU 的代码,这意味着它需要在执行前进一步转换为特定于 CPU 的本机代码。
除了 MSIL 代码,PE 文件还包括清单中包含的元数据信息。PE 文件中元数据的合并使得代码可以自我描述。不需要附加的类型库或接口定义语言(IDL)文件。
因为各种 .NET 兼容的语言编译成相同的 MSIL 和元数据格式 .NET 平台支持语言集成。这超越了微软的 COM 组件,例如,用 VB 编写的客户端代码可以实例化和使用用 C++编写的组件的方法。和 .NET 语言集成,您可以用 VB 编写一个. NET 类,它继承了用 C# 编写的类,然后重写它的一些方法。
在执行 PE 文件中的 MSIL 代码之前,. NET Framework 实时(JIT)编译器会将其转换为特定于 CPU 的本机代码。为了提高效率,JIT 编译器不会同时将所有 MSIL 代码转换为本机代码。MSIL 代码根据需要进行转换。当执行一个方法时,编译器检查代码是否已经被转换并放入缓存。如果有,则使用编译版本;否则,MSIL 码将被转换并存储在缓存中以备将来调用。
因为 JIT 编译器是针对不同的 CPU 和操作系统编写的,所以开发人员不需要重写他们的应用来针对各种平台。可以想象,你为 Windows 服务器平台编写的程序也可以在 UNIX 服务器上运行。所需要的只是一个用于 UNIX 架构的 JIT 编译器。
使用 Visual Studio 集成开发环境
您可以使用简单的文本编辑器编写 C# 代码,并使用命令行编译器编译它。然而,您会发现,使用文本编辑器编写企业级应用可能会令人沮丧且效率低下。大多数以编程为生的程序员发现集成开发环境(IDE)在易用性和提高生产率方面非常有价值。微软在 Visual Studio (VS) 中开发了一个出色的 IDE。集成到 VS 中的许多特性使得 .NET Framework 更直观、更简单、更高效。Visual Studio 的一些有用功能包括:
- 编辑器功能,如自动语法检查、自动完成和颜色突出显示
- 一个 IDE 供所有人使用。网络语言
- 广泛的调试支持,包括设置断点、单步调试代码以及查看和修改变量的能力
- 集成帮助文档
- 拖放式 GUI 开发
- XML 和 HTML 编辑
- 与 Windows Installer 集成的自动化部署工具
- 从 IDE 中查看和管理服务器的能力
- 完全可定制和扩展的界面
下面的活动将向您介绍 VS IDE 中的一些特性。当您完成这些步骤时,不要担心编码细节。只需专注于习惯在 VS IDE 中工作。您将在接下来的章节中了解更多关于代码的内容。
注意如果没有安装 Visual Studio 2012,请参考附录 C 中的安装说明。
活动 5-1。游览 VISUAL STUDIO
在本活动中,您将熟悉以下内容:
- 自定义 IDE
- 创建. NET 项目并设置项目属性
- 使用 VS IDE 中的各种编辑器窗口
- 使用 VS IDE 的自动语法检查和自动完成功能
- 用 VS IDE 编译程序集
定制 IDE
要定制 IDE,请按照下列步骤操作:
-
Start Visual Studio 2012.
注意如果这是你第一次启动 VS,你会被要求选择一个默认的开发设置。选择 Visual C# 开发设置。
-
您将看到起始页。起始页包含几个窗格,其中一个窗格链接到 MSDN (Microsoft Developer Network)网站上发布的有用文档。点击其中的一个链接将会启动一个托管在 VS 中的浏览器窗口,打开 MSDN 网站上的文档。花些时间研究一下信息和起始页上显示给你的各种链接。
-
Microsoft has taken considerable effort to make VS a customizable design environment. You can customize just about every aspect of the layout, from the various windows and menus down to the color coding used in the code editor. Select Tools Options to open the Options dialog box, shown in Figure 5-1, which allows you to customize many aspects of the IDE.
图 5-1 。VS 选项对话框
-
在对话框左侧的“类别”列表中,单击“项目和解决方案”。您可以选择更改项目的默认位置,以及在构建和运行项目时会发生什么。选择“生成开始时显示输出窗口”选项。
-
调查其他一些可用的可定制选项。完成后,单击确定按钮关闭选项对话框。
创建新项目
要创建一个新项目,请遵循以下步骤:
-
在起始页上,单击“创建项目”链接,这将启动“新建项目”对话框。(您也可以选择文件新建项目来打开此对话框。)
-
“新建项目”对话框允许您使用内置模板创建各种项目。有创建 Windows 项目、Web 项目、WCF 项目以及许多其他项目的模板,这取决于您在安装 VS 时选择的选项
-
In the Templates pane, expand the Visual C# node and select the Windows node, as shown in Figure 5-2. Observe the various C# project templates. There are templates for creating various types of Windows applications, including Windows Forms-based applications, class libraries, and console applications.
图 5-2 。VS 新建项目对话框
-
单击 Windows 窗体应用模板。将应用的名称更改为 DemoChapter5,然后单击 OK 按钮。
当项目打开时,您将看到一个添加到项目中的默认表单(名为 Form1)的表单设计器。在此窗口的右侧,您应该会看到解决方案资源管理器。
调查解决方案资源管理器和类视图
解决方案资源管理器显示作为当前解决方案一部分的项目和文件,如图 5-3 所示。默认情况下,创建项目时,会创建一个与项目同名的解决方案。该解决方案包含一些全局信息、项目链接信息和定制设置,如任务列表和调试信息。一个解决方案可以包含多个相关项目。
图 5-3 。解决方案浏览器
解决方案节点下是项目节点。项目节点组织与项目相关的各种文件和设置。项目文件将这些信息组织在一个 XML 文档中,该文档包含对作为项目一部分的类文件的引用、项目所需的任何外部引用以及已设置的编译选项。项目节点下是属性节点、引用节点、App.config 文件、Form1
类的类文件和Program
类文件。
要练习使用解决方案资源管理器和一些 VS 特性和视图,请按照下列步骤操作:
-
在“解决方案资源管理器”窗口中,右击“属性”节点并选择“打开”。这将启动项目属性窗口。窗口左侧是几个选项卡,您可以使用它们来浏览和设置各种应用设置。
-
Select the Application tab, as shown in Figure 5-4. Notice that, by default, the assembly name and default namespace are set to the name of the project.
图 5-4 。项目属性窗口
-
在“项目属性”窗口中浏览其他一些选项卡。完成后,单击窗口选项卡中的 x 关闭窗口。
-
在"解决方案资源管理器"窗口中,展开"引用"节点。节点下是应用引用的外部程序集。请注意,默认情况下包含了几个引用。默认引用取决于项目的类型。例如,因为这是一个 Windows 应用项目,所以默认情况下包含对
System.Windows.Forms
名称空间的引用。 -
解决方案资源管理器的项目节点下的
Form1
类文件有一个. cs 扩展名,表示它是用 C# 代码编写的。默认情况下,文件名已设置为与表单相同的名称。在解决方案资源管理器窗口中双击该文件。该窗体显示在设计视图中。单击 Solution Explorer 顶部工具栏中的 View Code 按钮,将会打开Form1
类的代码编辑器。 -
选择视图类视图以启动类视图窗口。“类视图”窗口的顶部按照命名空间层次结构组织项目文件。展开 DemoChapter5 根节点会显示三个子节点:引用节点、DemoChapter5 命名空间节点和 DemoChapter5 属性节点。名称空间节点由节点名称左侧的
{}
符号指定。 -
Listed under the DemoChapter5 namespace node are the classes that belong to the namespace. Expanding the Form1 node reveals a Base Types folder. Expanding Base Types shows the classes and interfaces inherited and implemented by the
Form1
class, as shown in Figure 5-5. You can further expand the nodes to show the classes and interfaces inherited and implemented by theForm
base class.图 5-5 。类视图中展开的节点
-
“类视图”窗口的底部是该类的方法、属性和事件的列表。选择“类视图”窗口顶部的“窗体”节点。请注意窗口底部列出了大量的方法、属性和事件。
-
Right-click the DemoChapter5 project node and select Add Class. Name the class
DemoClass1
and click the Add button. If the class code is not visible in the code editor, double-click the DemoClass1 node in the Class View window to display it. Wrap the class definition code in a namespace declaration as follows:namespace DemoChapter5
{
namespace MyDemoNamespace
{
class DemoClass1
{
}
}
}
-
请注意类视图中更新的层次结构。
DemoClass1
现在属于MyDemoNamespace
,后者属于DemoChapter5
名称空间。DemoClass1
的全称现在是DemoChapter5.MyDemoNamespace.DemoClass1
。 -
Add the following code to the
DemoClass1
definition. As you add the code, notice the auto-selection drop-down list provided (see Figure 5-6). Pressing the Tab key will select the current item on the list.
`class DemoClass1: System.Collections.CaseInsensitiveComparer`
`{`
`}`
![9781430249351_Fig05-06.jpg](https://gitee.com/OpenDocCN/vkdoc-csharp-zh/raw/master/docs/begin-cs-oop/img/9781430249351_Fig05-06.jpg)
图 5-6 。代码选择下拉列表
- 请注意类视图中更新的层次结构。展开 DemoClass1 节点下的 Base Types 节点,您将看到 base
CaseInsensitiveComparer
类节点。选择该节点,您将在“类视图”窗口的下半部分看到CaseInsensitiveComparer
类的方法和属性。 - Right-click the
Compare
method of theCaseInsensitiveComparer
class node and choose Browse Definition. The Object Browser window is opened as a tab in the main window and information about the Compare method is displayed. Notice it takes two object arguments, compares them, and returns an integer value based on the result (see Figure 5-7).
![9781430249351_Fig05-07.jpg](https://gitee.com/OpenDocCN/vkdoc-csharp-zh/raw/master/docs/begin-cs-oop/img/9781430249351_Fig05-07.jpg)
图 5-7 。对象浏览器
- 对象浏览器使您能够浏览对象层次结构,并查看有关层次结构中的项和方法的信息。花些时间探索对象浏览器。完成后,关闭对象浏览器并关闭"类视图"窗口。
探索工具箱和属性窗口
要浏览 VS 工具箱和属性窗口,请遵循以下步骤:
-
In the Solution Explorer window, double-click the Form1.cs node. This brings up the Form1 design tab in the main editing window. Locate the Toolbox tab to the left of the main editing window. Click the tab and the Toolbox window should expand, as shown in Figure 5-8. In the upper-right corner of the Toolbox, you should see the auto-hide icon, which looks like a thumbtack. Click the icon to turn off the auto-hide feature.
图 5-8 。VS 工具箱
-
在“工具箱”的“所有 Windows 窗体”节点下是一些控件,您可以将它们拖放到窗体上以构建 GUI。还有其他包含非图形组件的节点,这些组件有助于简化一些常见编程任务的创建和管理。例如,数据节点包含用于访问和管理数据存储的控件。向下滚动工具箱窗口,观察设计器公开的各种控件。
-
Under the All Windows Forms node, select the Label control. Move the cursor over the form; it should change to a crosshairs pointer. Draw a label on the form by clicking, dragging, and then releasing the mouse. In a similar fashion, draw a TextBox control and a Button control on the form. Figure 5-9 shows how the form should look.
图 5-9 。示例表单布局
-
通过单击工具箱窗口右上角的自动隐藏(图钉)图标,重新打开工具箱的自动隐藏功能。
-
在“解决方案资源管理器”窗口下,找到主编辑窗口右侧的“属性”窗口。(您也可以在菜单步骤中选择查看属性窗口,打开属性窗口。)属性窗口显示设计视图中当前选定对象的属性。您还可以通过此窗口编辑对象的许多属性。
-
In the Form1 design window, click Label1. The Label1 control should be selected in the drop-down list at the top of the Properties window (see Figure 5-10). Locate the
Text
property and change it to “Enter your password:” (minus the quotes).图 5-10 。VS 属性窗口
注意你可能需要重新排列表单上的控件才能看到所有的文本。
-
将 TextBox1 的
Password Char
属性设置为*。将 Button1 的Text
属性改为 OK。(单击窗体上的控件或使用“属性”窗口顶部的下拉列表来查看控件的属性。) -
通过选择文件全部保存来保存项目。
构建和执行汇编
要构建和执行程序集,请遵循以下步骤:
-
在解决方案资源管理器中,双击 Form1 以打开设计窗口。
-
在窗体设计器中,双击 Button1 控件。Form1 的代码编辑器将显示在主编辑窗口中。代码编辑器中添加了一个处理按钮单击事件的方法。
-
Add the following code to the method. This code will display the password entered in TextBox1 on the title bar of the form.
private void button1_Click(object sender, EventArgs e)
{
this.Text = "Your password is " + textBox1.Text;
}
-
Select Build Build Solution. The Output window shows the progress of compiling the assembly (see Figure 5-11). Once the assembly has been compiled, it is ready for execution. (If you can’t locate the Output window, select View menu Output.)
图 5-11 。输出窗口中显示的生成进度
-
选择调试开始调试。这将在调试模式下运行程序集。表单加载后,输入密码并单击 OK 按钮。您应该会在表单的标题栏中看到包含密码的消息。单击右上角的 x 关闭表单。
-
选择文件保存全部,然后通过选择文件退出退出 VS。
活动 5-2。使用 VS 的调试特性
在本活动中,您将熟悉以下内容:
- 设置断点并单步执行代码
- 使用 VS IDE 中的各种调试窗口
- 使用错误列表窗口定位并修复构建错误
步进代码
若要逐句通过您的代码,请按照下列步骤操作:
-
开始 VS .选择文件新建项目。
-
在 C# Windows 模板下,选择控制台应用。将项目活动重命名为 5_2。
-
You will see a Program class file open in the code editor. The class file has a
Main
method that gets executed first when the application runs. Add the following code to the program class. This code contains a method that loads a list of numbers and displays the contents of the list in the console window.class Program
{
static List<int> numList = new List<int>();
static void Main(string[] args)
{
LoadList(10);
foreach (int i in numList)
{
Console.WriteLine(i);
}
Console.ReadLine();
}
static void LoadList(int iMax)
{
for (int i = 1; i <= iMax; i++)
{
numList.Add(i);
}
}
}
-
To set a breakpoint (which pauses program execution), place the cursor on the declaration line of the
Main
method, right-click, and choose Breakpoint Insert Breakpoint. A red dot will appear in the left margin to indicate that a breakpoint has been set (see Figure 5-12).图 5-12 。在代码编辑器中设置断点
-
选择调试开始调试。程序执行将在断点处暂停。黄色箭头表示将要执行的下一行代码。
-
Select View Toolbars and click the Debug toolbar. (A check next to the toolbar name indicates it is visible.) To step through the code one line at a time, select the Step Into button on the Debug toolbar (see Figure 5-13). (You can also press the F11 key.) Continue stepping through the code until you get to the LoadList.
图 5-13 。使用调试工具栏
-
逐句通过代码,直到
for
循环循环几次。此时,您可能对这段代码的运行感到满意,并且想要跳出这个方法。在“调试”工具栏上,单击“跳出”按钮。您应该返回到 Main 方法。 -
继续遍历代码,直到
for-each
循环循环了几次。此时,您可能希望返回到运行时模式。为此,请单击“调试”工具栏上的“继续”按钮。当控制台窗口出现时,按 enter 键关闭窗口。 -
再次以调试模式启动应用。单步执行代码,直到到达方法调用
LoadList(10);
。 -
在“调试”工具栏上,选择“单步执行”按钮。这将执行该方法,并在执行返回到调用代码后重新进入中断模式。单步执行该方法后,继续单步执行几行代码,然后选择"调试"工具栏上的"停止"按钮。单击左边距中的红点以删除断点。
设置条件断点
要设置条件断点,请遵循以下步骤:
-
In the Program.cs file locate the
LoadList
method. Set a breakpoint on the following line of code:numList.Add(i);
-
Open the Breakpoints window by selecting Debug Windows Breakpoints. You should see the breakpoint you just set listed in the Breakpoints window (see Figure 5-14).
图 5-14 。断点窗口
-
Right-click the breakpoint in the Breakpoints window and select Condition. You will see the Breakpoint Condition dialog box. Enter i == 3 as the condition expression and click the OK button (see Figure 5-15).
图 5-15 。断点条件对话框
-
选择调试开始。程序执行将暂停,您将看到一个黄色箭头,指示将执行的下一行。
-
Select Debug Windows Locals. The Locals window is displayed at the bottom of the screen (see Figure 5-16). The value of i is displayed in the Locals window. Verify that it is 3. Step through the code using the Debug toolbar and watch the value of i change in the Locals window. Click the Stop Debugging button in the Debug toolbar.
图 5-16 。局部窗口
-
在屏幕底部找到断点窗口。在“断点”窗口中右击断点,然后选择“条件”。通过清除“条件”复选框来清除当前条件,然后单击“确定”按钮。
-
在“断点”窗口中右击断点,然后选择“命中次数”。将断点设置为命中次数等于 4 时中断,然后单击“确定”。
-
选择调试开始。程序执行将暂停,黄色箭头指示将执行的下一行代码。
-
Right-click the
numList
statement and select Add Watch. A Watch window will be displayed withnumList
in it. Notice thatnumList
is aSystem.Collections.Generics.List
type. Click the plus sign next tonumList
. Verify that the list contains three items (see Figure 5-17). Step through the code and watch the array fill with items. Click the Stop button in the Debug toolbar.图 5-17 。观察窗
定位并修复构建错误
要定位并修复构建错误,请遵循以下步骤:
-
In the
Program
class, locate the following line of code and comment it out by placing a two slashes in front of it, as shown here://static List<int> numList = new List<int>();
-
注意代码中
numList
下面的红色曲线。这表明在应用运行之前必须修复一个生成错误。将鼠标悬停在该行上会显示关于错误的更多信息。 -
Select Build Build Solution. The Error List window will appear at the bottom of the screen, indicating a build error (see Figure 5-18).
图 5-18 。使用“错误列表”窗口定位生成错误
-
在"错误列表"窗口中双击包含生成错误的行。相应的代码将在代码编辑器中可见。
-
通过删除斜线取消对步骤 1 中注释的行的注释。选择构建构建解决方案。这一次,输出窗口显示在屏幕的底部,表明没有构建错误。
-
保存项目并退出 VS。
摘要
本章向您介绍了的基础知识 .NET 框架。您回顾了的一些基本目标 .NET 框架。你也看到了 .NET Framework 的结构以及 CLR 编译和执行代码的方式。这些概念都是相关且一致的 .NET 兼容的编程语言。此外,您还探索了 Visual Studio 集成开发环境的一些特性。虽然您不需要 IDE 来开发 .NET 应用,当涉及到生产力、调试和代码管理时,使用它是非常宝贵的。
使用 C# 可以开发许多类型的应用。这些应用包括使用 ASP.NET 的 Windows 桌面应用、使用 WPF 的 web 应用、使用 WinRT 的 Windows 应用商店应用以及使用 ASP.NET 或 WCF Web API 的应用服务应用。在以后的章节中,你将会看到这些技术,但是首先你需要学习如何使用 C# 编程。下一章是系列文章的第一章,着眼于 OOP 概念——比如类结构、继承和多态——是如何在 C# 代码中实现的。
六、创建类
在第五章中,你看了 .NET 框架的开发以及程序如何在框架下执行。那一章向您介绍了 Visual Studio IDE,并且您对使用它有了一些熟悉。您现在可以开始编码了!本章是向您介绍如何在 C# 中创建和使用类的系列文章的第一章。它涵盖了创建和使用类的基础知识。您将创建类,添加属性和方法,并在客户端代码中实例化这些类的对象实例。
阅读本章后,您应该熟悉以下内容:
- OOP 中使用的对象如何依赖于类定义文件
- 封装在面向对象程序设计中的重要作用
- 如何定义类的属性和方法
- 类构造函数的用途
- 如何在客户端代码中使用类的实例
- 重载类构造函数和方法的过程
- 如何用 Visual Studio 创建和测试类定义文件
引入对象和类
在 OOP 中,你在程序中使用对象来封装与程序所处理的实体相关的数据。例如,人力资源应用需要与雇员一起工作。员工具有需要跟踪的相关属性。您可能对员工姓名、地址、部门等感兴趣。虽然您跟踪所有员工的相同属性,但是每个员工都有这些属性的唯一值。在人力资源应用中,Employee
对象获取并修改与雇员相关的属性。在面向对象的程序设计中,对象的属性被称为特性。
除了雇员的属性,人力资源应用还需要一组由Employee
对象公开的既定行为。例如,人力资源部门感兴趣的一个员工行为是请求休假的能力。在 OOP 中,对象通过方法公开行为。对象包含一个封装实现代码的RequestTimeOff
方法。
OOP 中使用的对象的属性和方法是通过类定义的。类是一个蓝图,它定义了作为类的实例创建的对象的属性和行为。如果您已经完成了应用的正确分析和设计,那么您应该能够参考 UML 设计文档来确定需要构造哪些类以及这些类将包含哪些属性和方法。UML 类图包含了构建系统类所需的初始信息。
为了演示使用 C# 构建一个类,您将看到一个简单的Employee
类的代码。Employee
类将拥有封装和处理雇员数据的属性和方法,作为虚构的人力资源应用的一部分。
定义类别
让我们检查创建类定义所需的源代码。第一行代码将代码块定义为一个类定义,使用关键字class
后跟类名。类定义的主体由一个左右花括号括起来。代码块的结构如下:
class Employee
{
}
创建类属性
定义了类代码块的起点和终点之后,下一步是定义类中包含的实例变量(通常称为字段)。这些变量保存类的实例将操作的数据。关键字private
确保这些实例变量只能由类内部的代码操作。以下是实例变量定义:
private int _empID;
private string _loginName;
private string _password;
private string _department;
private string _name;
当该类(客户端代码)的用户需要查询或设置这些实例变量的值时,公共属性会向它们公开。在代码的属性块中有一个Get
块和一个Set
块。Get
块将私有实例变量的值返回给该类的用户。这段代码提供了一个可读的属性。Set
块提供写使能属性;它将客户端代码发送的值传递给相应的私有实例变量。下面是一个属性块的示例:
public string Name
{
get { return _name; }
set { _name = value; }
}
有时,您可能希望限制对属性的访问,以便客户端代码可以读取属性值,但不能更改它。通过删除属性块中的set
块,您创建了一个只读属性。以下代码显示了如何将EmployeeID
属性设为只读:
public int EmployeeID
{
get { return _empID; }
}
注意私有和公共关键字影响代码的范围。有关范围的更多信息,请参见附录 a。
OOP 的新手经常会问,为什么有必要做这么多工作来获取和设置属性。难道不能创建用户可以直接读写的公共实例变量吗?答案在于 OOP 的基本原则之一:封装。封装意味着客户端代码不能直接访问数据。当处理数据时,客户端代码必须使用通过类的实例访问的明确定义的属性和方法。以下是以这种方式封装数据的一些好处:
- 防止对数据的未授权访问
- 通过错误检查确保数据完整性
- 创建只读或只写属性
- 将类的用户与实现代码中的更改隔离开来
例如,您可以通过以下代码检查以确保密码至少有六个字符长:
public string Password
{
get { return _password; }
set
{
if (value.Length >= 6)
{
_password = value;
}
else
{
throw new Exception("Password must be at least 6 characters");
}
}
}
创建类方法
类方法定义了类的行为。例如,下面为Employee
类定义了一个验证雇员登录的方法:
public Boolean Login(string loginName, string password)
{
if (loginName == "Jones" & password == "mj")
{
_empID = 1;
Department = "HR";
Name = "Mary Jones";
return true;
}
else if (loginName == "Smith" & password == "js")
{
_empID = 2;
Department = "IS";
Name = "Jerry Smith";
return true;
}
else
{
return false;
}
}
当客户端代码调用该类的Login
方法时,登录名和密码被传递给该方法(这些被称为输入参数)。检查参数。如果它们匹配一个当前的雇员,那么该类的实例将被该雇员的属性填充,一个布尔值true
将被传递回调用代码。如果登录名和密码与当前雇员不匹配,一个布尔值false
被传递回客户端代码。return
关键字用于将控制权返回给具有指定值的客户端代码。
在前面的方法中,一个值被返回给客户端代码。这由 Boolean 关键字指示,该关键字将布尔值类型分配给返回值。下面的AddEmployee
方法是Employee
类的另一个方法。当一个雇员需要被添加到数据库中时,它被调用,并将新分配的雇员 ID(作为整数类型)返回给客户机。该方法还用新添加的雇员的属性填充了Employee
类的对象实例。
public int AddEmployee(string loginName, string password,
string department, string name)
{
//Data normally saved to database.
_empID = 3;
LoginName = loginName;
Password = password;
Department = department;
Name = name;
return EmployeeID;
}
并非所有方法都向客户端返回值。在这种情况下,该方法用 void 关键字声明,以指示没有返回值。下面的方法更新密码,但不向客户端返回值。
public void UpdatePassword(string password)
{
//Data normally saved to database.
Password = password;
}
活动 6-1。创建雇员类
在本活动中,您将熟悉以下内容:
- 如何使用 Visual Studio 创建 C# 类定义文件
- 如何从客户端代码创建和使用类的实例
注意如果您还没有下载启动文件,请下载。有关说明,请参见附录 C。本实验是一个 Windows 窗体应用,它是可以用 Visual Studio 构建的 Windows 客户端类型应用之一。
定义员工类别
要创建 Employee 类,请遵循以下步骤:
-
启动 Visual Studio。选择文件打开项目。
-
导航到 Activity6_1Starter 文件夹,单击
Activity6_1.sln
文件,然后单击 Open。当项目打开时,它将包含一个登录表单。稍后您将使用这个表单来测试您创建的Employee
类。 -
Select Project Add Class. In the Add New Item dialog box, rename the class file to
Employee.cs
, and then click Add. Visual Studio adds theEmployee.cs
file to the project and adds the following class definition code to the file (along with the name space definition and someusing
declarations.):class Employee
{
}
-
Enter the following code between the opening and closing brackets to add the private instance variables to the class body in the definition file:
private int _empID;
private string _loginName;
private string _password;
private int _securityLevel;
-
Next, add the following public properties to access the private instance variables defined in step 4:
public int EmployeeID
{
get { return _empID; }
}
public string LoginName
{
get { return _loginName; }
set { _loginName = value; }
}
public string Password
{
get { return _password; }
set { _password = value; }
}
public int SecurityLevel
{
get { return _securityLevel; }
}
-
After the properties, add the following
Login
method to the class definition:public Boolean Login(string loginName, string password)
{
LoginName = loginName;
Password = password;
//Data nomally retrieved from database.
//Hard coded for demo only.
if (loginName == "Smith" & password == "js")
{
_empID = 1;
_securityLevel = 2;
return true;
}
else if (loginName == "Jones" & password == "mj")
{
_empID = 2;
_securityLevel = 4;
return true;
}
else
{
return false;
}
}
-
选择构建构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。
测试雇员类
要测试Employee
类,请遵循以下步骤:
-
Open
frmLogin
in the code editor and locate thebtnLogin
click event code.提示双击表单设计器中的登录按钮也会在代码编辑器中调出事件代码。
-
In the body of the
btnLogin
click event, declare and instantiate a variable of type Employee calledoEmployee
:Employee = new Employee();
-
Next, call the
Login
method of theoEmployee
object, passing in the values of the login name and the password from the text boxes on the form. Capture the return value in a Boolean variable calledbResult
:Boolean bResult = oEmployee.Login(txtName.Text,txtPassword.Text);
-
After calling the
Login
method, if the result is true, show a message box stating the user’s security level, which is retrieved by reading the SecurityLevel property of theoEmployee
object. If the result is false, show a login failed message.If (bResult == true)
{
MessageBox.Show("Your security level is " + oEmployee.SecurityLevel);
}
else
{
MessageBox.Show("Login Failed");
}
-
选择构建构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。
-
选择调试开始运行项目。通过输入登录名 Smith 和密码 js 来测试登录表单。您应该会收到一条消息,指示安全级别为 2。尝试输入您的姓名和密码 pass。您应该会收到一条消息,表明登录失败。
-
测试登录过程后,关闭表单;这将停止调试器。
注意本章代码对无效登录使用异常。这只是为了演示的目的。抛出错误是一个开销很大的过程,不应该用于业务处理逻辑。有关正确使用抛出错误的更多信息,请参见附录 b。
使用构造函数
在 OOP 中,当类的对象实例被实例化时,使用构造函数来执行任何需要发生的处理。例如,您可以初始化对象实例的属性或建立数据库连接。类构造器方法的命名与类相同。当类的对象实例被客户端代码实例化时,将执行构造函数方法。下面的构造函数在Employee
类中用来初始化Employee
类的对象实例的属性。将雇员 ID 传递给构造函数,以便从数据存储中检索值,如下所示:
public Employee(int empID)
{
//Retrieval of data hardcoded for demo
if (empID == 1)
{
_empID = 1;
LoginName = "Smith";
Password = "js";
Department = "IT";
Name = "Jerry Smith";
}
else if (empID == 2)
{
_empID = 2;
LoginName = "Jones";
Password = "mj";
Department = "HR";
Name = "Mary Jones";
}
else
{
throw new Exception("Invalid EmployeeID");
}
}
重载方法
重载方法的能力是 OOP 语言的一个有用的特性。通过定义多个同名但包含不同签名的方法,可以重载一个类中的方法。方法签名是方法名及其参数类型列表的组合。如果更改参数类型列表,则创建不同的方法签名。例如,参数类型列表可以包含不同数量的参数或不同的参数类型。编译器将通过检查客户端传入的参数类型列表来确定执行哪个方法。
注意改变参数的传递方式(换句话说,从byVal
到byRef
)不会改变方法签名。改变方法的返回类型也不会创建唯一的方法签名。有关方法签名和传递参数的更详细讨论,请参考附录 a。
假设您想提供Employee
类的两个方法,这两个方法将允许您向数据库中添加雇员。第一种方法是在添加员工时为员工分配用户名和密码。第二种方法添加雇员信息,但是将用户名和密码的分配推迟到以后。您可以通过重载Employee
类的AddEmployee
方法来轻松实现这一点,如以下代码所示:
public int AddEmployee(string loginName, string password,
string department, string name)
{
//Data normally saved to database.
_empID = 3;
LoginName = loginName;
Password = password;
Department = department;
Name = name;
return EmployeeID;
}
public int AddEmployee(string department, string name)
{
//Data normally saved to database.
_empID = 3;
Department = department;
Name = name;
return EmployeeID;
}
因为第一个方法(string,string,string,string)的参数类型列表与第二个方法(string,string)的参数类型列表不同,所以编译器可以确定调用哪个方法。OOP 中一个常见的技术是重载类的构造函数。例如,当创建一个Employee
类的实例时,一个构造函数可以用于新雇员,另一个可以用于当前雇员,方法是在客户机实例化类实例时传入雇员 ID。下面的代码显示了类构造函数的重载:
public Employee()
{
_empID = -1;
}
public Employee(int empID)
{
//Retrieval of data hard coded for demo
if (empID == 1)
{
_empID = 1;
LoginName = "Smith";
Password = "js";
Department = "IT";
Name = "Jerry Smith";
}
else if (empID == 2)
{
_empID = 2;
LoginName = "Jones";
Password = "mj";
Department = "HR";
Name = "Mary Jones";
}
else
{
throw new Exception("Invalid EmployeeID");
}
}
活动 6-2。创建构造函数和重载方法
在本活动中,您将熟悉以下内容:
- 如何创建和重载类构造函数方法
- 如何从客户端代码中使用类的重载构造函数
- 如何控制一个类的方法
- 如何从客户端代码中使用类的重载方法
创建和重载类构造函数
要创建和重载类构造函数,请遵循以下步骤:
-
启动 Visual Studio。选择文件打开项目。
-
导航到 Activity6_2Starter 文件夹,单击 Activity6_2.sln 文件,然后单击“打开”。当项目打开时,它将包含一个 frmEmployeeInfo 表单,您将使用它来测试
Employee
类。该项目还包括Employee.cs
文件,其中包含了Employee
类定义代码。 -
在代码编辑器中打开 Employee.cs 并检查代码。该类包含几个需要维护的与雇员相关的属性。
-
After the property declaration code, add the following private method to the class. This method simulates the generation of a new employee ID.
private int GetNextID()
{
//simulates the retrieval of next
//available id from database
return 100;
}
-
Create a default class constructor, and add code that calls the
GetNextID
method and assigns the return value to the private instance variable_empID
:public Employee()
{
_empID = GetNextID();
}
-
Overload the default constructor method by adding a second constructor method that takes an integer parameter of
empID
, like so:public Employee(int empID)
{
//Constructor for existing employee
}
-
Add the following code to the overloaded constructor, which simulates extracting the employee data from a database and assigns the data to the instance properties of the class:
//Simulates retrieval from database
if (empID == 1)
{
_empID = empID;
LoginName = "smith";
PassWord = "js";
SSN = 123456789;
Department = "IS";
}
else if (empID == 2)
{
_empID = empID;
LoginName = "jones";
PassWord = "mj";
SSN = 987654321;
Department = "HR";
}
else
{
throw new Exception("Invalid Employee ID");
}
-
选择构建构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。
测试雇员类构造函数
要测试Employee
类构造函数,请遵循以下步骤:
-
在窗体编辑器中打开 EmployeeInfoForm,双击“新雇员”按钮,在代码编辑器中调出 click 事件代码。
-
In the click event method body, declare and instantiate a variable of type Employee called
oEmployee
:Employee oEmployee = new Employee();
-
Next, update the employeeID text box with the employee ID, disable the employee ID text box, and clear the remaining textboxes:
txtEmpID.Text = oEmployee.EmpID.ToString();
txtEmpID.Enabled = false;
txtLoginName.Text = "";
txtPassword.Text = "";
txtSSN.Text = "";
txtDepartment.Text = "";
-
选择构建构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。
-
在窗体编辑器中打开 EmployeeInfoForm,双击现有雇员按钮,在代码编辑器中调出 click 事件代码。
-
In the click event method body, declare and instantiate a variable of type Employee called
oEmployee
. Retrieve the employee ID from thetxtEmpID
text box and pass it as an argument in the constructor. Theint.Parse
method converts the text to an integer data type:Employee oEmployee = new Employee(int.Parse(txtEmpID.Text));
-
Next, disable the employee ID textbox and fill in the remaining text boxes with the values of the
Employee
object’s properties:txtEmpID.Enabled = false;
txtLoginName.Text = oEmployee.LoginName;
txtPassword.Text = oEmployee.PassWord;
txtSSN.Text = oEmployee.SSN.ToString();
txtDepartment.Text = oEmployee.Department;
-
选择构建构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。
-
选择 Debug Start 运行项目并测试代码。
-
当显示 EmployeeInfo 窗体时,单击“新建雇员”按钮。您应该看到在雇员 ID 文本框中已经生成了一个新的雇员 ID。
-
单击重置按钮清除并启用员工 ID 文本框。
-
为员工 ID 输入值 1,然后单击现有员工按钮。该员工的信息显示在表单上。
-
测试完构造函数后,关闭窗体,这将停止调试器。
重载一个类方法
要重载一个类方法,请遵循以下步骤:
-
在代码编辑器中打开 Employee.cs 代码。
-
Add the following
Update
method to theEmployee
class. This method simulates the updating of the employee security information to a database:public string Update(string loginName, string password)
{
LoginName = loginName;
PassWord = password;
return "Security info updated.";
}
-
Add a second
Update
method to simulate the updating of the employee human resources data to a database:public string Update(int ssNumber, string department)
{
SSN = ssNumber;
Department = department;
return "HR info updated.";
}
-
选择构建构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。
测试重载更新方法
要测试重载的Update
方法,请遵循以下步骤:
-
在窗体编辑器中打开 EmployeeInfo 窗体,双击“更新 SI”按钮。在代码编辑器窗口中,您将看到 click 事件代码。
-
In the click event method, declare and instantiate a variable of type Employee called
oEmployee
. Retrieve the employee ID from the txtEmpID text box and pass it as an argument in the constructor:Employee oEmployee = new Employee(int.Parse(txtEmpID.Text));
-
Next, call the
Update
method, passing the values of the login name and password from the text boxes. Show the method return message to the user in a message box:MessageBox.Show(oEmployee.Update(txtLoginName.Text, txtPassword.Text));
-
Update the login name and password text boxes with the property values of the
Employee
object:txtLoginName.Text = oEmployee.LoginName;
txtPassword.Text = oEmployee.PassWord;
-
Repeat this process to add similar code to the Update HR button click event method to simulate updating the human resources information. Add the following code to the click event method:
Employee oEmployee = new Employee(int.Parse(txtEmpID.Text));
MessageBox.Show(oEmployee.Update(int.Parse(txtSSN.Text), txtDepartment.Text));
txtSSN.Text = oEmployee.SSN.ToString();
txtDepartment.Text = oEmployee.Department;
-
选择构建构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。
-
选择 Debug Start 运行项目并测试代码。
-
为员工 ID 输入值 1,然后单击现有员工按钮。
-
更改安全信息的值,然后单击“更新”按钮。
-
更改人力资源信息的值,然后单击“更新”按钮。
-
您应该看到正确的
Update
方法是根据传递给它的参数调用的。测试完Update
方法后,关闭表单。
摘要
本章为你在 C# 代码中创建和使用类打下了坚实的基础。既然您已经熟悉了构造和使用类,那么您就可以开始研究实现 OOP 的一些更高级的特性了。在第七章中,你将专注于继承和多态是如何在 C# 代码中实现的。作为一名面向对象的程序员,熟悉这些概念并学习如何在程序中实现它们是很重要的。
七、创建类层次结构
在第六章中,您学习了如何创建类、添加属性和方法,以及在客户端代码中实例化类的对象实例。本章介绍了继承和多态的概念。
继承是任何 OOP 语言最强大和最基本的特性之一。使用继承,您可以创建封装通用功能的基类。其他类可以从这些基类派生。派生类继承基类的属性和方法,并根据需要扩展功能。
第二个基本的 OOP 特性是多态。多态让基类定义必须由任何派生类实现的方法。基类定义了派生类必须遵守的消息签名,但是方法的实现代码留给了派生类。多态的强大之处在于,客户知道他们可以用同样的方式实现基类的方法。即使方法的内部处理可能不同,客户端也知道方法的输入和输出是相同的。
阅读本章后,您将了解以下内容:
- 如何创建和使用基类
- 如何创建和使用派生类
- 访问修饰符如何控制继承
- 如何覆盖基类方法
- 如何实现接口
- 如何通过继承和接口实现多态
理解继承
任何 OOP 语言最强大的特性之一就是继承。继承是创建基类的能力,该基类具有可在从基类派生的类中使用的属性和方法。
创建基类和派生类
继承的目的是创建一个基类,它封装了相同类型的派生类可以使用的属性和方法。例如,您可以创建一个基类Account
。在Account
类中定义了一个GetBalance
方法。然后,您可以创建两个单独的类:SavingsAccount
和CheckingAccount
。因为SavingsAccount
类和CheckingAccount
类使用相同的逻辑来检索余额信息,所以它们从基类Account
继承了GetBalance
方法。这使您能够创建一个更易于维护和管理的通用代码库。
然而,派生类不限于基类的属性和方法。派生类可能需要额外的方法和属性,这些方法和属性是它们所特有的。例如,从支票账户提款的业务规则可能要求维持最低余额。然而,从储蓄账户提款可能不需要最低余额。在这个场景中,派生的CheckingAccount
和SavingsAccount
类都需要它们自己对Withdraw
方法的唯一定义。
要在 C# 中创建一个派生类,需要输入类名,后跟一个冒号(:
)和基类名。以下代码演示了如何创建一个从Account
基类派生的CheckingAccount
类:
class Account
{
long _accountNumber;
public long AccountNumber
{
get { return _accountNumber; }
set { _accountNumber = value; }
}
public double GetBalance()
{
//code to retrieve account balance from database
return (double)10000;
}
}
class CheckingAccount : Account
{
double _minBalance;
public double MinBalance
{
get { return _minBalance; }
set { _minBalance = value; }
}
public void Withdraw(double amount)
{
//code to withdraw from account
}
}
下面的代码可以由创建一个对象实例CheckingAccount
的客户端实现。注意,客户端感觉不到对GetBalance
方法的调用和对Withdraw
方法的调用之间的区别。在这种情况下,客户端不知道Account
类;相反,这两种方法似乎都是由CheckingAccount
定义的。
CheckingAccount oCheckingAccount = new CheckingAccount();
double balance;
oCheckingAccount.AccountNumber = 1000;
balance = oCheckingAccount.GetBalance();
oCheckingAccount.Withdraw(500);
创建密封类
默认情况下,任何 C# 类都可以被继承。当创建可以被继承的类时,你必须小心不要以这样的方式修改它们,以至于派生类不再像预期的那样工作。如果不小心,您可能会创建难以管理和调试的复杂继承链。例如,假设您基于Account
类创建了一个派生的CheckingAccount
类。另一个程序员可以基于CheckingAccount
创建一个派生类,并以你从未想过的方式使用它。(这很容易发生在沟通和设计方法不佳的大型编程团队中。)
通过使用sealed
修饰符,您可以创建您知道不会从其派生的类。这种类型的班级通常被称为密封或最终班级。通过使一个类不可继承,可以避免与修改基类代码相关的复杂性和开销。下面的代码演示了在构造类定义时如何使用sealed
修饰符:
sealed class CheckingAccount : Account
创建抽象类
在这个例子中,客户端可以通过派生的CheckingAccount
类的实例或者直接通过基类Account
的实例来访问GetBalance
方法。有时,你可能想要一个不能被客户端代码实例化的基类。必须通过派生类来访问该类的方法和属性。在这种情况下,您使用abstract
修饰符来构造基类。以下代码显示了带有abstract
修饰符的Account
类定义:
abstract class Account
这使得Account
类成为一个抽象类。为了让客户端能够访问GetBalance
方法,它们必须创建一个派生的CheckingAccount
类的实例。
在基类中使用访问修饰符
当使用继承设置类层次结构时,必须管理如何访问类的属性和方法。到目前为止,你看到的两个访问修饰符是 public 和 private。如果基类的方法或属性被公开为 public,则派生类和派生类的任何客户端都可以访问它。如果将基类的属性或方法公开为私有,则派生类或客户端不能直接访问它。
您可能希望向派生类公开基类的属性或方法,而不是向派生类的客户端公开。在这种情况下,使用受保护的访问修饰符。下面的代码演示了 protected 访问修饰符的用法:
protected double GetBalance()
{
//code to retrieve account balance from database
return (double)10000;
}
通过将GetBalance
方法定义为受保护的,它可以被派生类CheckingAccount
访问,但是不能被访问CheckingAccount
类实例的客户端代码访问。
活动 7-1。使用基类和派生类实现继承 NCE
在本活动中,您将熟悉以下内容:
- 创建基类和继承其方法的派生类
- 使用受保护的访问修饰符来限制基类方法的使用
- 创建抽象基类
创建基类和派生类
要创建Account
类,请遵循以下步骤:
-
启动 Visual Studio。选择文件打开项目。
-
导航到 Activity7_1Starter 文件夹,单击 Activity7_1.sln 文件,然后单击“打开”。当项目打开时,它将包含一个出纳员表单。稍后您将使用这个表单来测试您创建的类。
-
在“解决方案资源管理器”窗口中,右键单击项目节点并选择“添加类”。
-
在“添加新项”对话框中,将类文件重命名为 Account.cs,然后单击“打开”。Account.cs 文件被添加到项目中,
Account
类定义代码被添加到该文件中。 -
Add the following code to the class definition file to create the private instance variable (
private
is the default modifier for instance variables):int _accountNumber;
-
Add the following
GetBalance
method to the class definition:public double GetBalance(int accountNumber)
{
_accountNumber
=accountNumber;
//Data normally retrieved from database.
if (_accountNumber == 1)
{
return 1000;
}
else if (_accountNumber == 2)
{
return 2000;
}
else
{
return -1; //Account number is incorrect
}
}
-
After the
Account
class, add the following code to create theCheckingAccount
andSavingsAccount
derived classes:class CheckingAccount : Account
{
}
class SavingsAccount : Account
{
}
-
选择构建构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。
测试类别
要测试这些类,请遵循以下步骤:
-
在代码编辑器中打开柜员表单,找到
btnGetBalance
点击事件代码。 -
Inside the event procedure, prior to the try block, declare and instantiate a variable of type
CheckingAccount
calledoCheckingAccount
, a variable of typeSavingsAccount
calledoSavingsAccount
, and a variable of typeAccount
calledoAccount
:CheckingAccount oCheckingAccount
=new CheckingAccount();
SavingsAccount oSavingsAccount
=new SavingsAccount();
Account oAccount
=new Account();
-
Depending on which radio button is selected, call the
GetBalance
method of the appropriate object and pass the account number value from theAccountNumber
text box. Show the return value in theBalance
text box. Place the following code in thetry
block prior to thecatch
statement:if (rdbChecking.Checked)
{
txtBalance.Text =
oCheckingAccount.GetBalance(int.Parse(txtAccountNumber.Text)).ToString();
}
else if (rdbSavings.Checked)
{
txtBalance.Text =
oSavingsAccount.GetBalance(int.Parse(txtAccountNumber.Text)).ToString();
}
else if (rdbGeneral.Checked)
{
txtBalance.Text =
oAccount.GetBalance(int.Parse(txtAccountNumber.Text)).ToString();
}
-
选择构建构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。
-
选择调试开始运行项目。输入账号 1,并单击支票账户类型的获取余额按钮。你应该得到 1000 英镑的余额。测试其他帐户类型。您应该得到相同的结果,因为所有的类都在使用基类中定义的相同的
GetBalance
函数。 -
测试后,关闭窗体,这将停止调试器。
将基类方法的使用限制在其派生类中
此时,基类的GetBalance
方法是公共的,这意味着它可以被派生类及其客户端访问。让我们改变这一点,使GetBalance
方法只能被派生类单独访问,而不能被它们的客户端访问。要以这种方式保护GetBalance
方法,请遵循以下步骤:
-
找到
Account
类的GetBalance
方法。 -
将
GetBalance
方法的访问修饰符从public
改为protected
。 -
切换到 frmTeller 代码编辑器,找到
btnGetBalance
click 事件代码。 -
将光标悬停在对
oCheckingAccount
对象的GetBalance
方法的调用上。您将看到一条警告,指出它是一个受保护的函数,在此上下文中不可访问。 -
注释掉
try
和catch
语句之间的代码。 -
切换到 Account.cs 代码编辑器。
-
Add the following code to create the following private instance variable to the
SavingsAccount
class definition file:double _dblBalance;
-
Add the following
Withdraw
method to theSavingsAccount
class. This function calls the protected method of theAccount
base class:public double Withdraw(int accountNumber, double amount)
{
_dblBalance
=GetBalance(accountNumber);
if (_dblBalance >= amount)
{
_dblBalance -
=amount;
return _dblBalance;
}
else
{
Return -1; //Not enough funds
}
}
-
选择构建构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。
测试受保护的基类方法
要测试Withdraw
方法,请遵循以下步骤:
-
在代码编辑器中打开 frmTeller 表单,找到
btnWithdraw
click 事件代码。 -
Inside the event procedure, prior to the
try
block, declare and instantiate a variable of typeSavingsAccount
calledoSavingsAccount
.SavingsAccount oSavingsAccount
=new SavingsAccount();
-
Call the
Withdraw
method of theoSavingsAccount
. Pass the account number value from theAccountNumber
text box and the withdrawal amount from theAmount
text box. Show the return value in theBalance
text box. Place the following code in thetry
block prior to thecatch
statement:txtBalance.Text
=oSavingsAccount.Withdraw
(int.Parse(txtAccountNumber.Text),double.Parse(txtAmount.Text)).ToString();
-
选择构建构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。
-
选择调试开始运行项目。
-
通过输入账号 1 和取款金额 200 来测试
SavingsAccount
类的Withdraw
方法。点击撤销按钮。您应该会得到 800 英镑的余额。 -
输入账号 1,取款金额 2000。点击撤销按钮。您应该得到-1,表示资金不足。
-
测试完
Withdraw
方法后,关闭表单,这将停止调试器。
将基类的所有成员限制在其派生类中使用
因为Account
基类是公共的,所以它可以被派生类的客户端实例化。您可以通过使Account
基类成为抽象类来改变这一点。抽象类只能被它的派生类访问,不能被它们的客户端实例化和访问。要创建并测试抽象类的可访问性,请遵循以下步骤:
-
在 Account.cs 代码中找到
Account
类定义。 -
Add the
abstract
keyword to the class definition code, like so:abstract class Account
-
Select Build Build Solution. You should receive a build error in the Error List window. Find the line of code causing the error.
Account oAccount
=new Account();
-
注释掉该行代码,然后再次选择 Build Build Solution。它现在应该没有任何错误。
-
保存并关闭项目。
重写基类的方法
当派生类从基类继承方法时,它继承该方法的实现。作为基类的设计者,您可能希望让派生类以自己独特的方式实现该方法。这就是所谓的重写基类方法。
默认情况下,派生类不能覆盖其基类的实现代码。要允许覆盖基类方法,必须在方法定义中包含关键字virtual
。在派生类中,使用相同的方法签名定义一个方法,并使用override
关键字指示它正在重写一个基类方法。下面的代码演示了在Account
基类中创建一个可重写的Deposit
方法:
public virtual void Deposit(double amount)
{
//Base class implementation
}
要覆盖派生的CheckingAccount
类中的Deposit
方法,请使用以下代码:
public override void Deposit(double amount)
{
//Derived class implementation
}
需要注意的一个场景是当一个派生类从基类继承,第二个派生类从第一个派生类继承。当一个方法重写基类中的一个方法时,它在默认情况下变成可重写的。为了限制重写方法在继承链上被重写,您必须在派生类的方法定义中在override
关键字之前包含sealed
关键字。如果CheckingAccount
类派生自,CheckingAccount
类中的以下代码防止覆盖Deposit
方法:
public sealed override void Deposit(double amount)
{
//Derived class implementation
}
当您指示基类方法是可重写的时,派生类可以选择重写该方法或使用基类提供的实现。在某些情况下,您可能希望使用基类方法作为派生类的模板。基类没有实现代码,但用于定义派生类中使用的方法签名。这种类型的类被称为抽象基类。你用抽象关键字定义类和方法。以下代码用于通过抽象Deposit
方法创建抽象Account
基类:
public abstract class Account
{
public abstract void Deposit(double amount);
}
注意,因为在基类中没有为Deposit
方法定义实现代码,所以省略了方法体。
从基类调用派生类方法
可能会出现这样的情况:从基类的另一个方法调用基类中可重写的方法,而派生类重写基类的方法。当从派生类的实例调用基类方法时,基类将调用派生类的重写方法。下面的代码显示了这种情况的一个例子。一个CheckingAccount
基类包含一个可重写的GetMinBalance
方法。从CheckingAccount
类继承而来的InterestBearingCheckingAccount
类覆盖了GetMinBalance
方法。
class CheckingAccount
{
private double _balance = 2000;
public double Balance
{
get { return _balance; }
}
public virtual double GetMinBalance()
{
return 200;
}
public virtual void Withdraw(double amount)
{
double minBalance = GetMinBalance();
if (minBalance < (Balance - amount))
{
_balance -= amount;
}
else
{
throw new Exception("Minimum balance error.");
}
}
}
class InterestBearingCheckingAccount : CheckingAccount
{
public override double GetMinBalance()
{
return 1000;
}
}
客户端创建了一个InterestBearingCheckingAccount
类的对象实例,并调用了Withdraw
方法。在这种情况下,执行InterestBearingCheckingAccount
类的被覆盖的GetMinimumBalance
方法,并使用最小余额 1000。
InterestBearingCheckingAccount oAccount = new InterestBearingCheckingAccount();
oAccount.Withdraw(500);
当调用Withdraw
方法时,您可以在它前面加上this
限定符:
double minBalance = this.GetMinBalance();
因为如果没有使用限定符,那么this
限定符就是默认的限定符,所以代码的执行方式和前面演示的一样。执行该方法的最大派生类实现(已实例化)。换句话说,如果客户端实例化了一个InterestBearingCheckingAccount
类的实例,如前所述,基类对GetMinimumBalance
的调用是针对派生类的实现的。另一方面,如果客户创建了一个CheckingAccount
类的实例,基类对GetMinimumBalance
的调用是针对它自己的实现的。
从派生类中调用基类方法
在某些情况下,您可能希望开发一个派生类方法,该方法仍然使用基类中的实现代码,但也用自己的实现代码来扩充它。在这种情况下,您在派生类中创建一个重写方法,并使用base
限定符调用基类中的代码。下面的代码演示了base
限定符的用法:
public override void Deposit(double amount)
{
base.Deposit(amount);
//Derived class implementation.
}
基类的重载方法
派生类继承的方法可以被重载。重载类的方法签名必须使用与重载方法相同的名称,但参数列表必须不同。这与重载同一类的方法是一样的。下面的代码演示派生方法的重载:
class CheckingAccount
{
public void Withdraw(double amount)
{
}
}
class InterestBearingCheckingAccount : CheckingAccount
{
public void Withdraw(double amount, double minBalance)
{
}
}
实例化InterestBearingCheckingAccount
实例的客户端代码可以访问两个Withdraw
方法。
InterestBearingCheckingAccount oAccount = new InterestBearingCheckingAccount();
oAccount.Withdraw(500);
oAccount.Withdraw(500, 200);
隐藏基类方法
如果派生类中的方法与基类方法具有相同的方法签名,但是没有用关键字override
、标记,那么它实际上隐藏了基类的方法。虽然这可能是有意的行为,但有时它可能会在无意中发生。虽然代码仍然可以编译,但是 IDE 会发出警告,询问这是否是预期的行为。如果您打算隐藏基类方法,您应该在派生类的方法定义中显式使用new
关键字。使用new
关键字将向 IDE 表明这是预期的行为,并消除警告。下面的代码演示隐藏基类方法:
class CheckingAccount
{
public virtual void Withdraw(double amount)
{
}
}
class InterestBearingCheckingAccount : CheckingAccount
{
public new void Withdraw(double amount)
{
}
public void Withdraw(double amount, double minBalance)
{
}
}
活动 7-2。重写基类方法
在本活动中,您将熟悉以下内容:
- 覆盖基类的方法
- 在派生类中使用基限定符
覆盖基类方法
要覆盖Account
类,请遵循以下步骤:
-
开始 VS .选择文件打开项目。
-
导航到 Activity7_2Starter 文件夹,单击 Activity7_2.sln 文件,然后单击“打开”。当项目打开时,它将包含一个出纳员表单。稍后您将使用这个表单来测试您将创建的类。该项目还包含一个 BankClasses.cs 文件。这个文件包含了基类
Account
和派生类SavingsAccount
和CheckingAccount
的代码。 -
检查基类
Account
中定义的Withdraw
方法。该方法检查帐户中是否有足够的资金,如果有,则更新余额。您将在CheckingAccount
类中覆盖这个方法,以确保维持最小的平衡。 -
Change the
Withdraw
method definition in theAccount
class to indicate it is overridable, like so:public virtual double Withdraw(double amount)
-
Add the following
GetMinimumBalance
method to theCheckingAccount
class definition:public double GetMinimumBalance()
{
return 200;
}
-
Add the following overriding
Withdraw
method to theCheckingAccount
class definition. This method adds a check to see that the minimum balance is maintained after a withdrawal.public override double Withdraw(double amount)
{
if (Balance >= amount
+GetMinimumBalance())
{
_balance -
=amount;
return Balance;
}
else
{
return -1; //Not enough funds
}
}
-
选择构建构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。
测试被覆盖的方法
要测试您已经创建的修改过的Withdraw
方法,请遵循以下步骤:
-
在代码编辑器中打开 frmTeller 表单,找到
btnWithdraw
click 事件代码。 -
Depending on which radio button is selected, call the
Withdraw
method of the appropriate object and pass the value of thetxtAmount
text box. Add the following code in thetry
block to show the return value in thetxtBalance
text box:if (rdbChecking.Checked)
{
oCheckingAccount.AccountNumber
=int.Parse(txtAccountNumber.Text);
txtBalance.Text
=oCheckingAccount.Withdraw(double.Parse(txtAmount.Text)).ToString();
}
else if (rdbSavings.Checked)
{
oSavingsAccount.AccountNumber
=int.Parse(txtAccountNumber.Text);
txtBalance.Text
=oSavingsAccount.Withdraw(double.Parse(txtAmount.Text)).ToString();
}
-
选择构建构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。
-
选择调试开始运行项目。
-
输入帐号 1,选择“检查”选项按钮,然后单击“获取余额”按钮。你应该得到 1000 英镑的余额。
-
输入取款金额 200,然后单击取款按钮。您应该会得到 800 英镑的余额。
-
输入取款金额 700,然后点击取款按钮。您应该得到-1(表示资金不足),因为最终余额将小于最小余额 200。
-
输入帐号 1,选择“储蓄”选项按钮,然后单击“获取余额”按钮。你应该得到 1000 英镑的余额。
-
输入取款金额 600,然后点击取款按钮。您应该会得到 400 英镑的余额。
-
输入取款金额 400,然后点击取款按钮。您应该得到一个 0 的结果余额,因为对于使用
Account
基类的Withdraw
方法的储蓄帐户,没有最小余额。 -
测试后,关闭窗体,这将停止调试器。
使用基本限定符调用基类方法
此时,CheckingAccount
类的Withdraw
方法覆盖了Account
类的Withdraw
方法。不执行基类的方法中的任何代码。现在您将修改代码,这样当执行CheckingAccount
类的代码时,它也会执行基类的Withdraw
方法。遵循这些步骤:
-
找到
Account
类的Withdraw
方法。 -
Change the implementation code so that it decrements the balance by the amount passed to it.
public virtual double Withdraw(double amount)
{
_balance -
=amount;
return Balance;
}
-
Change the
Withdraw
method of theCheckingAccount
class so that after it checks for sufficient funds, it calls theWithdraw
method of theAccount
base class.public override double Withdraw(double amount)
{
if (Balance > = amount
+GetMinimumBalance())
{
return base.Withdraw(amount);
}
else
{
return -1; //Not enough funds.
}
}
-
Add a
Withdraw
method to theSavingsAccount
class that is similar to theWithdraw
method of theCheckingAccount
class, but does not check for a minimum balance.public override double Withdraw(double amount)
{
if (Balance > = amount)
{
return base.Withdraw(amount);
}
else
{
return -1; //Not enough funds.
}
}
-
选择构建构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。
测试基础改性剂的使用
要测试Withdraw
方法,请遵循以下步骤:
- 选择调试开始。
- 输入帐号 1,选择“检查”选项按钮,然后单击“获取余额”按钮。你应该得到 1000 英镑的余额。
- 输入取款金额 600,然后点击取款按钮。您应该会得到 400 英镑的余额。
- 输入取款金额 300,然后单击取款按钮。您应该得到 a -1(资金不足),因为最终余额将小于 200 的最小值。
- 输入帐号 1,选择“储蓄”选项按钮,然后单击“获取余额”按钮。你应该得到 1000 英镑的余额。
- 输入取款金额 600,然后点击取款按钮。您应该会得到 400 英镑的余额。
- 输入取款金额 300,然后单击取款按钮。您应该得到 100 的结果余额,因为使用
Account
基类的Withdraw
方法的储蓄账户没有最小余额。 - 测试后,关闭窗体,这将停止调试器。
实现接口
如前所述,您可以创建一个抽象基类,它不包含任何实现代码,但定义了从基类继承的任何类必须使用的方法签名。当使用抽象类时,从它派生的类必须实现它的继承方法。您可以使用另一种技术来实现类似的结果。在这种情况下,不是定义一个抽象类,而是定义一个定义方法签名的接口。
实现接口的类被合同要求实现接口签名定义,并且不能改变它。这种技术有助于确保使用这些类的客户端代码知道哪些方法可用、应该如何调用它们以及预期的返回值。下面的代码显示了如何声明接口定义:
public interface IAccount
{
string GetAccountInfo(int accountNumber);
}
类通过在类名后使用分号后跟接口名来实现接口。当一个类实现一个接口时,它必须为该接口定义的所有方法提供实现代码。下面的代码演示了CheckingAccount
如何实现IAccount
接口:
public class CheckingAccount : IAccount
{
public string GetAccountInfo(int accountNumber)
{
return "Printing checking account info";
}
}
因为实现接口和从抽象基类继承是相似的,所以你可能会问为什么要使用接口。使用接口的一个好处是一个类可以实现多个接口。那个 .NET Framework 不支持从多个类继承。作为多重继承的一种变通方法,实现多个接口的能力也包括在内。接口对于跨不同类型的类实施通用功能也很有用。
理解多态
多态是从同一基类继承的派生类以自己独特的方式响应同一方法调用的能力。这简化了客户端代码,因为客户端代码不需要担心它引用的是哪个类类型,只要这些类类型实现相同的方法接口。
例如,假设您希望银行应用中的所有帐户类都包含一个GetAccountInfo
方法,该方法具有相同的接口定义,但基于帐户类型有不同的实现。客户端代码可以遍历一组 account-type 类,编译器将在运行时确定需要执行哪个特定的 account-type 实现。如果您后来添加了一个实现了GetAccountInfo
方法的新帐户类型,那么您不需要修改现有的客户端代码。
您可以通过使用继承或实现接口来实现多态。下面的代码演示了继承的用法。首先,定义基类和派生类。
public abstract class Account
{
public abstract string GetAccountInfo();
}
public class CheckingAccount : Account
{
public override string GetAccountInfo()
{
return "Printing checking account info";
}
}
public class SavingsAccount : Account
{
public override string GetAccountInfo()
{
return "Printing savings account info";
}
}
然后创建一个类型为Account
的列表,并添加一个CheckingAccount
和一个SavingsAccount
。
List <Account> AccountList = new List <Account> ();
CheckingAccount oCheckingAccount = new CheckingAccount();
SavingsAccount oSavingsAccount = new SavingsAccount();
AccountList.Add(oCheckingAccount);
AccountList.Add(oSavingsAccount);
然后循环遍历List
并调用每个Account
的GetAccountInfo
方法。每个Account
类型都将实现自己的GetAccountInfo
实现。
foreach (Account a in AccountList)
{
MessageBox.Show(a.GetAccountInfo());
}
您也可以通过使用接口获得类似的结果。不是从基类Account
继承,而是定义并实现一个IAccount
接口。
public interface IAccount
{
string GetAccountInfo();
}
public class CheckingAccount : IAccount
{
public string GetAccountInfo()
{
return "Printing checking account info";
}
}
public class SavingsAccount : IAccount
{
public string GetAccountInfo()
{
return "Printing savings account info";
}
}
然后创建一个类型为IAccount
的列表,并添加一个CheckingAccount
和一个SavingsAccount
。
List<IAccount>AccountList = new List<IAccount>();
CheckingAccount oCheckingAccount = new CheckingAccount();
SavingsAccount oSavingsAccount = new SavingsAccount();
AccountList.Add(oCheckingAccount);
AccountList.Add(oSavingsAccount);
然后循环遍历AccountList
并调用每个Account
的GetAccountInfo
方法。每个Account
类型都将实现自己的GetAccountInfo
实现。
foreach (IAccount a in AccountList)
{
MessageBox.Show(a.GetAccountInfo());
}
活动 7-3。实现多态
在本活动中,您将熟悉以下内容:
- 通过继承创建多态
- 通过接口创建多态
使用继承实现多态
要使用继承实现多态,请遵循以下步骤:
-
启动 Visual Studio。选择文件新建项目。
-
选择 C# 模板下的控制台应用模板。将项目命名为活动 7_3。
-
该项目包含一个 Program.cs 文件。这个文件包含一个启动 Windows 控制台应用的
Main
方法。在解决方案资源管理器窗口中右键单击项目节点,并选择 Add class。将文件命名为 Account.cs。 -
In the Account.cs file alter the code to create an abstract base
Account
class. Include anaccountNumber
property and an abstract methodGetAccountInfo
that takes no parameters and returns a string.public abstract class Account
{
private int _accountNumber;
public int AccountNumber
{
get { return _accountNumber; }
set { _accountNumber
=value; }
}
public abstract string GetAccountInfo();
}
-
Add the following code to create two derived classes:
CheckingAccount
andSavingsAccount
. These classes will override theGetAccountInfo
method of the base class.public class CheckingAccount : Account
{
public override string GetAccountInfo()
{
return "Printing checking account info for account number "
+ AccountNumber.ToString();
}
}
public class SavingsAccount : Account
{
public override string GetAccountInfo()
{
return "Printing savings account info for account number "
+ AccountNumber.ToString();
}
}
-
选择构建构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。
测试多态继承方法
要测试多态方法,请遵循以下步骤:
-
在代码编辑器中打开 Program.cs 文件,找到
Main
方法。 -
Create an instance of a list of account types.
List <Account
>AccountList
=new List <Account> ();
-
Create instances of
CheckingAccount
andSavingsAccount
.CheckingAccount oCheckingAccount
=new CheckingAccount();
oCheckingAccount.AccountNumber
=100;
SavingsAccount oSavingsAccount
=new SavingsAccount();
oSavingsAccount.AccountNumber
=200;
-
Add the
oCheckingAccount
andoSavingsAccount
to the list using theAdd
method of the list.AccountList.Add(oCheckingAccount);
AccountList.Add(oSavingsAccount);
-
Loop through the list and call the
GetAccountInfo
method of each account type in the list and show the results in a console window.foreach (Account a in AccountList)
{
Console.WriteLine(a.GetAccountInfo());
}
Console.ReadLine();
-
选择构建构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。
-
选择调试开始运行项目。您应该看到一个控制台窗口,显示列表中每个对象的
GetAccountInfo
方法的返回字符串。 -
测试完多态后,按 enter 键关闭控制台窗口,这将停止调试器。
使用接口实现多态
要使用接口实现多态,请遵循以下步骤:
-
在代码编辑器中查看 Account.cs 文件的代码。
-
注释掉
Account
、CheckingAccount
和SavingsAccount
类的代码。 -
Define an interface
IAccount
that contains theGetAccountInfo
method.public interface IAccount
{
string GetAccountInfo();
}
-
Add the following code to create two classes:
CheckingAccount
andSavingsAccount
. These classes will implement theIAccount
interface.public class CheckingAccount : IAccount
{
private int _accountNumber;
public int AccountNumber
{
get { return _accountNumber; }
set { _accountNumber
=value; }
}
public string GetAccountInfo()
{
return "Printing checking account info for account number "
+ AccountNumber.ToString();
}
}
public class SavingsAccount : IAccount
{
private int _accountNumber;
public int AccountNumber
{
get { return _accountNumber; }
set { _accountNumber
=value; }
}
public string GetAccountInfo()
{
return "Printing savings account info for account number "
+ AccountNumber.ToString();
}
}
-
选择构建构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。
测试多态接口方法
要测试多态方法,请遵循以下步骤:
-
在代码编辑器中打开 Program.cs 文件,找到
Main
方法。 -
Change the code to create an instance of a list of
IAccount
types.List <IAccount
>AccountList
=new List <IAccount> ();
-
Change the code for each loop to loop through the list and call the
GetAccountInfo
method of eachIAccount
type in the list.foreach (IAccount a in AccountList)
{
Console.WriteLine(a.GetAccountInfo());
}
Console.ReadLine();
-
选择构建构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。
-
选择调试开始运行项目。您应该看到一个控制台窗口,显示列表中每个对象的
GetAccountInfo
方法的返回字符串。 -
测试完多态后,按 enter 键关闭控制台窗口,这将停止调试器。
摘要
本章向您介绍了 OOP 的两个最强大的特性:继承和多态。不管使用什么语言,知道如何实现这些特性是成为一名成功的面向对象程序员的基础。
在第八章中,你将仔细观察应用中的对象是如何协作的。涵盖的主题包括对象如何相互传递消息,事件如何驱动程序,数据如何在类的实例之间共享,以及如何处理异常。
八、实现对象协作
在第七章中,你学习了如何在 C# 中创建和使用类层次结构。那一章还介绍了继承、多态和接口的概念。在这一章中,你将学习如何让应用的对象一起工作来执行任务。您将看到对象如何通过消息传递进行通信,以及事件如何启动应用处理。您还将了解对象如何响应和交流在执行分配给它们的任务时可能出现的异常。
阅读本章后,您应该熟悉以下内容:
- 通过消息传递进行对象通信的过程
- 可能出现的不同类型的消息传递
- 如何在 C# 应用中使用委托
- 对象如何响应事件并发布自己的事件
- 发布和响应异常的过程
- 如何在同一个类的几个实例之间创建共享数据和过程
- 如何异步发出消息调用
通过消息传递进行交流
OOP 的优势之一是它的许多方面都模仿了现实世界。之前,您看到了如何使用公司中的雇员来表示一个Employee
类的实例。我们可以将这种类比扩展到类(对象)的实例在应用中如何交互。例如,在大公司中,员工执行专门的功能。一个人负责应付账款的处理,另一个人负责应收账款的操作。当一名员工需要请求服务时——例如带薪休假(PTO)——该员工向她的经理发送一条消息。我们可以将这种交互视为请求者(员工)和服务提供者(经理)之间的服务请求。这个请求可以只涉及一个对象(自助请求),两个对象,或者它可以是一个复杂的请求者/服务提供者请求链。例如,员工向她的经理申请 PTO,经理反过来与人力资源(HR)部门核实该员工是否有足够的累积时间。在这种情况下,经理既是员工的服务提供者,也是人力资源部门的服务请求者。
定义方法签名
当消息在请求者和服务提供者之间传递时,请求者可能会也可能不会期待响应。例如,当一个员工请求 PTO 时,她希望得到一个表示批准或拒绝的响应。然而,当会计部门发放工资时,员工们并不期望公司里的每个人都会发一封回复邮件来感谢他们!
发布消息时的一个常见要求是包含执行请求所必需的信息。当员工请求 PTO 时,她的经理希望她向他提供她请求的休假日期。在 OOP 术语中,您将方法的名称(请求的服务)和输入参数(请求者提供的信息)称为方法签名。
下面的代码演示了如何在 C# 中定义方法。访问修饰符后面首先是返回类型(如果没有返回值,则使用void
),然后是方法名。参数类型和名称列在括号中,用逗号分隔。方法的主体包含在左花括号和右花括号中。
public int AddEmployee(string firstName,string lastName)
{
//Code to save data to database
}
public void LogMessage(string message)
{
//Code to write to log file.
}
传递参数
当在类中定义方法时,还必须指示参数是如何传递的。参数可以通过值或引用传递。
如果选择按值传递参数,参数数据的副本将从调用例程传递给请求的方法。被请求的方法使用副本,如果对数据进行了更改,被请求的方法必须将副本传递回调用例程,以便调用者可以选择放弃更改或复制它们。回到公司的类比,想想更新你的员工档案的过程。人力资源部门不会让你直接接触文件;相反,它会向您发送文件中值的副本。您对副本进行更改,然后将其发送回人力资源部门。然后,人力资源部门决定是否将这些更改复制到实际的员工档案中。在 C# 中,默认情况下通过值传递参数,因此不使用关键字。在下面的方法中,参数通过值传递:
public int AddEmployee(string firstName)
{
//Code to save data to database
}
传递参数的另一种方式是通过引用。在这种情况下,请求代码不会传入数据的副本,而是传递数据所在位置的引用。以前面的例子为例,当您想要进行更新时,人力资源部门不会向您发送员工文件中数据的副本,而是通知您文件的位置,并告诉您去该部门进行更改。在这种情况下,显然,通过引用传递参数会更好。在 C# 代码中,当通过引用传递参数时,使用了ref
关键字。下面的代码演示如何定义通过引用传递值的方法:
public int AddEmployee(ref string firstName)
{
//Code to save data to database
}
当应用被设计为跨处理边界通信的组件时,甚至在不同的计算机上托管时,通过值而不是通过引用传递参数是有利的。通过引用传递参数会导致开销增加,因为当服务提供对象必须使用参数信息时,它需要跨处理边界和网络进行调用。单个处理请求可能会导致请求者和服务器提供者之间的多次来回调用。在维护数据完整性时,通过引用传递值也会导致问题。请求者在服务器不知情或无法控制的情况下,为要操作的数据打开了通道。
另一方面,当调用代码和被请求的方法在同一个处理空间中(可以说,它们占用同一个“隔间”)并且具有明确建立的信任关系时,通过引用传递值可能是更好的选择。在这种情况下,允许直接访问内存存储位置并通过引用传递参数可以提供优于通过值传递参数的性能优势。
通过引用传递参数可能有好处的另一种情况是,如果对象是复杂数据类型,例如另一个对象。在这种情况下,复制数据结构并跨进程和网络边界传递它的开销超过了跨网络重复调用的开销。
注这个 .NET Framework 通过在 XML 结构中序列化和反序列化复杂数据类型,允许您有效地复制和传递这些类型,从而解决了复杂数据类型的问题。
理解事件驱动编程
到目前为止,您已经看到了对象之间的消息传递,其中直接请求启动了消息交互。如果你想一想你在现实生活中是如何与物体互动的,你会经常收到信息来回应已经发生的事件。以办公室为例,当卖三明治的小贩走进大楼时,对讲机会发出信息通知员工午餐车已经到了。这种类型的消息传递被称为广播消息传递。发出一条消息,接收者决定是忽略还是响应这条消息。
发布此事件消息的另一种方式是,接待员向一组员工发送一封电子邮件,这些员工有兴趣知道三明治供应商何时出现。在这种情况下,感兴趣的员工将向接待员订阅以接收事件消息。这种类型的消息传递通常被称为基于订阅的消息传递。
用 .NET 框架是面向对象的、事件驱动的程序。如果您跟踪应用中出现的请求/服务提供者处理链,您就可以确定启动处理的事件。在 Windows 应用的情况下,与 GUI 交互的用户通常会启动事件。例如,用户可以通过单击按钮启动将数据保存到数据库的过程。应用中的类也可以启动事件。当检测到无效登录时,安全类可以广播事件消息。您还可以订阅外部事件。您可以创建一个 Web 服务,当您在股市中跟踪的股票发生变化时,该服务将发出事件通知。您可以编写一个订阅服务并响应事件通知的应用。
理解委托
为了在 C# 中实现基于事件的编程,您必须首先了解委托。委托是指通过调用服务提供者的方法来请求服务。然后,服务提供者将这个服务请求重新路由到另一个服务该请求的方法。委托类可以检查服务请求,并在运行时动态确定将请求路由到哪里。回到公司的类比,当一个经理收到一个服务请求时,她经常将它委托给她部门的一个成员。(事实上,许多人会认为,成功经理的一个共同特点是知道何时以及如何授权。)
创建委托方法时,首先定义委托方法的签名。因为委托方法实际上并不服务于请求,所以它不包含任何实现代码。下面的代码显示了一个用于比较整数值的委托方法:
public delegate Boolean CompareInt(int I1, int I2);
一旦定义了委托方法的签名,就可以创建要委托的方法。这些方法必须与委托方法具有相同的参数和返回类型。下面的代码显示了委托方法将委托给的两个方法:
private Boolean AscendOrder(int I1, int I2)
{
if (I1 < I2)
{ return true;}
else
{ return false; }
}
private Boolean DescendOrder(int I1, int I2)
{
if (I1 > I2)
{ return true; }
else
{ return false; }
}
一旦定义了委托及其委托方法,就可以使用委托了。下面的代码显示了排序例程的一部分,它根据作为参数传入的SortType
来确定调用哪个委托方法:
public void SortIntegers(SortType sortDirection, int[] intArray)
{
CompareInt CheckOrder;
if (sortDirection == SortType.Ascending)
{ CheckOrder = new CompareInt(AscendOrder); }
else
{ CheckOrder = new CompareInt(DescendOrder); }
// Code continues ...
}
实施事件
在 C# 中,当你想发布事件消息时,首先你要为事件声明一个委托类型。委托类型定义将传递给处理事件的方法的一组参数。
public delegate void DataUpdateEventHandler(string msg);
一旦声明了委托,就声明了委托类型的事件。
public event DataUpdateEventHandler DataUpdate;
当您想要引发事件时,可以通过传入适当的参数来调用事件。
public void SaveInfo()
{
try
{
DataUpdate("Data has been updated");
}
catch
{
DataUpdate("Data could not be updated");
}
}
响应事件
为了在客户端代码中使用事件,声明了一个事件处理方法,该方法执行程序逻辑以响应事件。此事件处理程序必须与发出事件的类中声明的事件委托具有相同的方法签名。
void odata_DataUpdate(string msg)
{
MessageBox.Show(msg);
}
此事件处理程序使用+=运算符向事件源注册。这个过程被称为事件连接。以下代码连接了先前声明的DataUpdate
事件的事件处理程序:
Data odata = new Data();
odata.DataUpdate += new DataUpdateEventHandler(odata_DataUpdate);
odata.SaveInfo();
Windows 控件事件处理
Windows 窗体还通过使用+=运算符将事件处理程序绑定到事件来实现事件处理程序。下面的代码将一个按钮绑定到一个点击事件,将一个文本框绑定到一个鼠标按下事件:
this.button1.Click += new System.EventHandler(this.button1_Click);
this.textBox1.MouseDown += new System.Windows.Forms.MouseEventHandler(this.textBox1_MouseDown);
控件事件的事件处理程序方法有两个参数:第一个参数sender
,提供对引发事件的对象的引用。第二个参数传递包含特定于正在处理的事件的信息的对象。下面的代码显示了按钮单击事件的事件处理程序方法和文本框鼠标按下事件的事件处理程序。注意如何使用e
来确定左键是否被点击。
private void button1_Click(object sender, EventArgs e)
{
}
private void textBox1_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == System.Windows.Forms.MouseButtons.Left)
{
//code goes here.
}
}
活动 8-1。发布和响应事件消息
在本活动中,您将学习做以下事情:
- 从服务器类创建和引发事件
- 处理来自客户端类的事件
- 处理 GUI 事件
在类定义中添加和引发事件消息
要在类定义文件中添加和引发事件消息,请遵循以下步骤:
-
启动 Visual Studio。选择文件新建项目。
-
选择一个 Windows 窗体应用项目。将项目命名为
Activity8_1
。 -
A default form is included in the project. Add controls to the form and change the property values, as listed in Table 8-1. Your completed form should look similar to Figure 8-1.
表 8-1。登录表单和控件属性
物体 属性 值 Form1
Name
frmLogin
Text
Login
Label1
Name
lblName
Text
Name:
Label2
Name
lblPassword
Text
Password:
Textbox1
Name
txtName
Text
(empty)
Textbox2
Name
txtPassword
Text
(empty)
PasswordChar
*
Button1
Name
btnLogin
Text
Login
Button2
Name
btnClose
Text
Close
图 8-1 。完整的登录表单
-
选择项目添加类别。给类命名
Employee
。在代码编辑器中打开Employee
类代码。 -
Above the class declaration, add the following line of code to define the login event handler delegate. You will use this event to track employee logins to your application.
public delegate void LoginEventHandler(string loginName, Boolean status);
-
Inside the class declaration, add the following line of code to define the
LoginEvent
as the delegate type:public event LoginEventHandler LoginEvent;
-
Add the following
Login
method to the class, which will raise theLoginEvent
:public void Login(string loginName, string password)
{
//Data normally retrieved from database.
if (loginName == "Smith" && password == "js")
{
LoginEvent(loginName, true);
}
else
{
LoginEvent(loginName, false);
}
}
-
选择构建构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。
接收客户端类中的事件
要在客户端类中接收事件,请遵循以下步骤:
-
在设计窗口中打开 frmLogin。
-
双击登录按钮以查看登录按钮 click 事件处理程序。
-
Add the following code to wire up the
Employee
class’sLoginEvent
with an event handler in the form class:private void btnLogin_Click(object sender, EventArgs e)
{
Employee oEmployee = new Employee();
oEmployee.LoginEvent += new LoginEventHandler(oEmployee_LoginEvent);
oEmployee.Login(txtName.Text, txtPassword.Text);
}
-
Add the following event handler method to the form that gets called when the
Employee
class issues aLoginEvent
:void oEmployee_LoginEvent(string loginName, bool status)
{
MessageBox.Show("Login status :" + status);
}
-
选择构建构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。
-
选择调试开始运行项目。
-
要测试以确保引发了
Login
事件,请输入登录名 Smith 和密码 js。这应该会触发 true 登录状态。 -
测试登录事件后,关闭窗体,这将停止调试器。
用一种方法处理多个事件
要用一种方法处理多个事件,请遵循以下步骤:
-
通过在解决方案资源管理器中右键单击 frmLogin 节点并选择“视图设计器”,在窗体设计器中打开 frmLogin。
-
From the Toolbox, add a MenuStrip control to the form. Click where it says “Type Here” and enter File for the top-level menu and Exit for its submenu, as shown in Figure 8-2.
图 8-2 。添加 MenuStrip 控件
-
Add the following method to handle the click event of the menu and the Close button:
private void FormClose(object sender, EventArgs e)
{
this.Close();
}
-
Open frmLogin in the designer window. In the properties window, select the exitToolStripMenuItem. Select the event button (lightning bolt) at the top of the properties window to show the events of the control. In the click event drop-down, select the
FormClose
method (see Figure 8-3).图 8-3 。连接事件处理程序
-
重复步骤 4 将
btnClose
按钮点击事件绑定到FormClose
方法。 -
在解决方案窗口中展开 Form1.cs 节点。右键单击表格 1。Designer.cs 节点并选择“查看代码”。
-
In the code editor, expand the Windows Form Designer generated code region. Search for the code listed below. This code was generated by the form designer to wire up the events to the
FormClose
method.this.btnClose.Click += new System.EventHandler(this.FormClose);
this.exitToolStripMenuItem.Click += new System.EventHandler(this.FormClose);
-
选择构建构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。
-
选择调试开始运行项目。测试退出菜单和关闭按钮。
-
测试后,保存项目,然后退出 Visual Studio。
中处理异常 .NET 框架
当对象协作时,事情可能会出错。异常是在正常处理过程中不希望发生的事情。例如,当连接失败时,您可能试图通过网络将数据保存到数据库中,或者您可能试图将数据保存到驱动器中没有磁盘的驱动器中。您的应用应该能够优雅地处理应用处理过程中发生的任何异常。
的 .NET 框架使用结构化的异常处理机制。以下是这种结构化异常处理的一些优势:
- 所有人的共同支持和结构 .NET 语言
- 支持创建受保护的代码块
- 过滤异常以创建高效可靠的错误处理的能力
- 支持终止处理程序,以保证完成清理任务,而不管可能遇到的任何异常
的 .NET Framework 还提供了大量的异常类,用于处理可能发生的常见异常。例如,FileNotFoundException
类封装了诸如文件名、错误消息以及当试图访问一个不存在的文件时抛出的异常的来源等信息。此外 .NET Framework 允许创建应用特定的异常类,您可以编写这些异常类来处理应用特有的常见异常。
使用 Try-Catch 块
当创建可能导致异常的代码时,您应该将它放在try
块中。放置在try
块中的代码被认为是受保护的。如果在受保护代码执行过程中出现异常,代码处理将被转移到catch
块,在那里进行处理。下面的代码演示了一个类的方法,该方法尝试从不存在的文件中读取数据。当抛出异常时,它被捕获在catch
块中。
public string ReadText(string filePath)
{
StreamReader sr;
try
{
sr = File.OpenText(filePath);
string fileText = sr.ReadToEnd();
sr.Close();
return fileText;
}
catch(Exception ex)
{
return ex.Message;
}
}
所有的try
块都需要至少一个嵌套的catch
块。您可以使用catch
块来捕获try
块中可能出现的所有异常,或者您可以使用它根据异常的类型来过滤异常。这使您能够根据异常类型动态响应不同的异常。下面的代码演示了如何根据从磁盘读取文本文件时可能发生的不同异常来筛选异常:
public string ReadText(string filePath)
{
StreamReader sr;
try
{
sr = File.OpenText(filePath);
string fileText = sr.ReadToEnd();
sr.Close();
return fileText;
}
catch (DirectoryNotFoundException ex)
{
return ex.Message;
}
catch (FileNotFoundException ex)
{
return ex.Message;
}
catch(Exception ex)
{
return ex.Message;
}
}
添加 Finally 块
此外,您可以在try
块的末尾嵌套一个finally
块。与catch
块不同,finally
块的使用是可选的。finally
块用于任何需要发生的清理代码,即使遇到异常。例如,您可能需要关闭数据库连接或释放文件。当try
块的代码被执行并且异常发生时,处理将评估每个catch
块,直到找到合适的捕捉条件。在catch
块执行后,将执行finally
块。如果try
块执行并且没有遇到异常,那么catch
块不会执行,但是finally
块仍然会被处理。下面的代码显示了一个用于关闭和释放StreamReader
的finally
块:
public string ReadText(string filePath)
{
StreamReader sr = null;
try
{
sr = File.OpenText(filePath);
string fileText = sr.ReadToEnd();
return fileText;
}
catch (DirectoryNotFoundException ex)
{
return ex.Message;
}
catch (FileNotFoundException ex)
{
return ex.Message;
}
catch (Exception ex)
{
return ex.Message;
}
finally
{
if (sr != null)
{
sr.Close();
sr.Dispose();
}
}
}
抛出异常
在代码执行期间,当进行不适当的调用时,例如,方法的参数具有无效值或者传递给方法的参数导致异常,您可以抛出异常来通知调用代码违规。在下面的代码中,如果orderDate
参数大于当前日期,就会向调用代码抛出一个ArgumentOutOfRangeException
,通知它们发生了冲突。
if (orderDate > DateTime.Now)
{
throw new ArgumentOutOfRangeException ("Order date can not be in the future.");
}
//Processing code...
注意如果在 .NET Framework,您可以创建一个从System.Exception
类派生的自定义异常类。
嵌套异常处理
在某些情况下,您可能能够纠正发生的异常,并继续处理try
块中剩余的代码。例如,可能会出现被零除的错误,将结果赋值为零并继续处理是可以接受的。在这种情况下,try-catch 块可以嵌套在导致异常的代码行周围。处理完异常后,处理将返回到嵌套的try
块之后的外层try-catch
块中的代码行。下面的代码演示了如何将一个try
块嵌套在另一个块中:
try
{
try
{
Y = X1 / X2;
}
catch (DivideByZeroException ex)
{
Y = 0;
}
//Rest of processing code.
}
catch (Exception ex)
{
//Outer exception processing
}
注了解更多关于处理异常和 .NET 框架异常类,请参考附录 b。
静态属性和方法
当您声明一个类的对象实例时,该对象实例化它所实现的类的属性和方法的自己的实例。例如,如果你要写一个增加计数器的计数例程,然后实例化该类的两个对象实例,每个对象的计数器将彼此独立;当您增加一个计数器时,另一个不会受到影响。正常情况下,这种对象独立性就是你想要的行为。但是,有时您可能希望一个类的不同对象实例访问相同的共享变量。例如,您可能希望构建一个计数器,记录已经实例化了多少个对象实例。在这种情况下,您将在类定义中创建一个静态属性值。以下代码演示了如何在类定义中创建静态TaxRate
属性:
public class AccountingUtilities
{
private static double _taxRate = 0.06;
public static double TaxRate
{
get { return _taxRate; }
}
}
要访问静态属性,不需要创建该类的对象实例;相反,您可以直接引用该类。下面的代码显示了一个访问先前定义的静态属性TaxRate
的客户端:
public class Purchase
{
public double CalculateTax(double purchasePrice)
{
return purchasePrice * AccountingUtilities.TaxRate;
}
}
如果您有客户端需要访问的实用函数,但是您不希望通过创建类的对象实例来获得对方法的访问,那么静态方法是非常有用的。请注意,静态方法只能访问静态属性。下面的代码显示了一个静态方法,用于计算当前登录到应用的用户数量:
public class UserLog
{
private static int _userCount;
public static void IncrementUserCount()
{
_userCount += 1;
}
public static void DecrementUserCount()
{
_userCount -= 1;
}
}
当客户端代码访问静态方法时,它通过直接引用该类来实现。下面的代码演示了如何访问前面定义的静态方法:
public class User
{
//other code ...
public void Login(string userName, string password)
{
//code to check credentials
//if successful
UserLog.IncrementUserCount();
}
}
虽然在应用中创建类时可能不经常使用静态属性和方法,但它们在创建基类库时很有用,并且在整个 .NET Framework 系统类。下面的代码演示了System.String
类的Compare
方法的使用。这是一个静态方法,按字母顺序比较两个字符串。如果第一个字符串大于,则返回正值;如果第二个字符串大于,则返回负值;如果两个字符串相等,则返回零。
public Boolean CheckStringOrder(string string1, string string2)
{
if (string.Compare(string1, string2) >= 0)
{
return true;
}
else
{
return false;
}
}
活动 8-2。实现异常处理和静态方法
在本活动中,您将学习如何执行以下操作:
- 创建并调用类的静态方法
- 使用结构化异常处理
创建静态方法
要创建静态方法,遵循以下步骤:
-
启动 Visual Studio。选择文件新建项目。
-
选择一个 Windows 应用项目。将项目命名为 Activity8_2。
-
Visual Studio creates a default form for the project which you’ll use to create a login form named Logger. Add controls to the form and change the property values, as listed in Table 8-2. Your completed form should look similar to Figure 8-4.
图 8-4 。已完成的记录器表单
表 8-2。记录器表单和控件属性
物体 属性 值 Form1
Name
frmLogger
Text
Logger
Textbox1
Name
txtLogPath
Text
c:\Test\LogTest.txt
Textbox2
Name
txtLogInfo
Text
Test Message
Button1
Name
btnL ogInfo
Text
Log Info
-
选择项目添加类。给类命名
Logger
。 -
Because you will be using the
System.IO
class within theLogger
class, add ausing
statement to the top of the class file:using System.IO;
-
Add a static
LogWrite
method to the class. This method will write information to a log file. To open the file, create aFileStream
object. Then create aStreamWriter
object to write the information to the file. Notice the use of theusing
blocks to properly dispose theFileStream
andStreamWriter
objects and release the resources.public static string LogWrite(string logPath, string logInfo)
{
using (FileStream oFileStream = new FileStream(logPath, FileMode.Open, FileAccess.Write))
{
using (StreamWriter oStreamWriter = new StreamWriter(oFileStream))
{
oFileStream.Seek(0, SeekOrigin.End);
oStreamWriter.WriteLine(DateTime.Now);
oStreamWriter.WriteLine(logInfo);
oStreamWriter.WriteLine();
}
}
return "Info Logged";
}
-
Open frmLogger in the visual design editor. Double click the btnLogInfo button to bring up the
btnLogInfo_Click
event method in the code editor. Add the following code, which runs theLogWrite
method of theLogger
class and displays the results in the form’s text property. Note that because you designated theLogWrite
method as static (in step 6), the client does not need to create an object instance of theLogger
class. Static methods are accessed directly through a class reference.private void btnLogInfo_Click(object sender, EventArgs e)
{
this.Text = Logger.LogWrite(txtLogPath.Text, txtLogInfo.Text);
}
-
选择构建构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。
-
选择调试运行。当表单启动时,单击 Log Info 按钮。您应该得到一个类型为
System.IO.DirectoryNotFoundException
的未处理异常消息。停止调试器。
创建结构化异常处理程序
要创建结构化异常处理程序,请遵循以下步骤:
-
在代码编辑器中打开
Logger
类代码。 -
Locate the
LogWrite
method and add a try-catch block around the current code. In the catch block, return a string stating the logging failed.try
{
using (FileStream oFileStream = new FileStream(logPath, FileMode.Open, FileAccess.Write))
{
using (StreamWriter oStreamWriter = new StreamWriter(oFileStream))
{
oFileStream.Seek(0, SeekOrigin.End);
oStreamWriter.WriteLine(DateTime.Now);
oStreamWriter.WriteLine(logInfo);
oStreamWriter.WriteLine();
}
}
return "Info Logged";}
catch
{
return "Logging Failed";
}
-
选择构建构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。
-
选择调试运行。当表单启动时,单击 Log Info 按钮。这一次,您应该不会得到异常消息,因为它是由
LogWrite
方法处理的。您应该会在表单的标题中看到消息“日志记录失败”。关闭表单。
过滤异常
要过滤例外情况,请遵循以下步骤:
-
Alter the
catch
block to return different messages, depending on which exception is thrown.catch (FileNotFoundException ex)
{
return ex.Message;
}
catch (IOException ex)
{
return ex.Message;
}
catch
{
return "Logging Failed";
}
-
在 Logger 类的
LogWrite
方法上设置断点。 -
选择调试开始运行项目。通过单击日志信息按钮测试 catch 块。执行将在断点处停止。单步执行代码,注意它被
IOException
块捕获。 -
测试后,关闭表单。
-
使用记事本,在 c 盘上的测试文件夹中创建 LogTest.txt 文件,并关闭该文件。确保文件和文件夹未被标记为只读。
-
选择调试开始运行项目。点击日志信息按钮,测试
WriteLog
方法。这一次,表单的标题应该表明日志写入成功。 -
停止调试器。
-
使用记事本打开 LogTest.txt 文件,并验证是否记录了信息。
-
保存项目,然后退出 Visual Studio。
使用异步消息传递
当对象通过来回传递消息进行交互时,它们可以同步或异步传递消息。
当客户机对象对服务器对象进行同步消息调用时,客户机暂停处理,并在继续之前等待来自服务器的响应。同步消息传递是最容易实现的,也是 .NET 框架。然而,有时这是一种低效的消息传递方式。例如,同步消息传递模型不太适合长时间运行的文件读写、跨慢速网络进行服务调用,或者在客户端断开连接的情况下进行消息排队。为了更有效地处理这些类型的情况 .NET Framework 提供了在对象之间异步传递消息所需的管道。
当客户端对象异步传递消息时,客户端可以继续处理。服务器完成消息请求后,响应信息将被发送回客户端。
如果你想一想,你与现实世界中的对象进行同步和异步的交互。同步消息传递的一个很好的例子是当你在杂货店排队结账时。当店员不能确定其中一个商品的价格时,他会打电话给经理进行价格检查,并暂停结账过程,直到返回结果。异步消息调用的一个例子是当职员注意到他的零钱不够时。他提醒经理,他很快就需要更改,但他可以继续处理客户的商品,直到更改到来。
为了使异步编程更容易实现 .NET Framework 4.5 引入了基于任务的异步模式(TAP) 。当符合 TAP 时,异步方法返回一个任务对象。此任务对象表示正在进行的操作,并允许与调用者进行通信。当调用异步方法时,客户端只需使用 await 修饰符,编译器负责所有必要的管道代码。
在 .NET Framework 中,当您想要创建一个可以异步调用的方法时,您可以使用async
修饰符。异步方法提供了一种在不阻塞调用者的情况下执行潜在的长时间运行流程的方法。如果包含用户界面(UI)的主线程需要调用长时间运行的进程,这将非常有用。如果进程被同步调用,那么 UI 将冻结,直到进程完成。
一个异步方法应该包含至少一个await
语句。当遇到await
语句时,处理被挂起,控制权返回给调用者(本例中是 UI)。因此用户可以继续与 UI 进行交互。一旦异步任务完成,处理返回 await 语句,调用者可以得到处理完成的提示。为了提醒调用者流程已经完成,您创建了一个Task
返回类型。如果 async 方法需要将信息传递回调用者,则使用Task<TResult>
。下面的代码显示了一个异步读取文本文件的方法。注意用于调用StreamReader
类的ReadToEndAsync
方法的async
修饰符和await
关键字。该方法通过一个字符串结果将一个Task
对象传递回调用者。
public static async Task<string> LogReadAsync(string filePath)
{
StreamReader oStreamReader;
string fileText;
try
{
oStreamReader = File.OpenText(filePath);
fileText = await oStreamReader.ReadToEndAsync();
oStreamReader.Close();
return fileText;
}
catch (FileNotFoundException ex)
{
return ex.Message;
}
catch (IOException ex)
{
return ex.Message;
}
catch
{
return "Logging Failed";
}
}
活动 8-3。异步调用方法
在本活动中,您将学习如何执行以下操作:
- 同步调用方法
- 异步调用方法
创建一个方法并同步调用它
要创建方法并同步调用它,请遵循以下步骤:
-
启动 Visual Studio。选择文件打开项目。
-
打开您在练习 8_2 中完成的解决方案文件。
-
Add the buttons shown in Table 8-3 to the frmLogger form. Figure 8-5 shows the completed form.
图 8-5 。用于同步和异步读取的完整记录器表单
表 8-3。记录器表单的附加按钮
物体 属性 值 Button1
Name
btnSyncRead
Text
Sync Read
Button2
Name
btnAsyncRead
Text
Async Read
Button3
Name
btnMessage
Text
Message
-
在代码编辑器中打开
Logger
类。 -
Recall that because you are using the
System.IO
namespace within theLogger
class, you added a using statement to the top of the file. You are also going to useSystem.Threading
namespace, so add a using statement to include this namespace.using System.Threading;
-
Add a static
LogRead
function to the class. This function will read information from a log file. To open the file, create aFileStream
object. Then createStreamReader
object to read the information from the file. You are also using theThread
class to suspend processing for five seconds to simulate a long call across a slow network.public static string LogRead(string filePath)
{
StreamReader oStreamReader;
string fileText;
try
{
oStreamReader = File.OpenText(filePath);
fileText = oStreamReader.ReadToEnd();
oStreamReader.Close();
Thread.Sleep(5000);
return fileText;
}
catch (FileNotFoundException ex)
{
return ex.Message;
}
catch (IOException ex)
{
return ex.Message;
}
catch
{
return "Logging Failed";
}
}
-
Open frmLogger in the visual design editor. Double click the btnMessage button to bring up the
btnMessage_Click
event method in the code editor. Add code to display a message box.private void btnMessage_Click(object sender, EventArgs e)
{
MessageBox.Show("Hello");
}
-
Open frmLogger in the visual design editor. Double-click the btnSyncRead button to bring up the
btnSyncRead_Click
event method in the code editor. Add code that calls theLogRead
method of theLogger
class and displays the results in a message box.private void btnSyncRead_Click(object sender, EventArgs e)
{
MessageBox.Show(Logger.LogRead(txtLogPath.Text));
}
-
选择构建构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。
-
选择调试运行。当表单启动时,单击同步读取按钮。点按“同步阅读”按钮后,尝试点按“邮件”按钮。当点击消息按钮时,您应该不会得到响应,因为您同步调用了
ReadLog
方法。ReadLog 方法返回结果后,单击消息按钮将会响应。 -
完成测试后,关闭表单。
创建和调用异步方法
要创建异步方法,请遵循以下步骤:
-
在代码编辑器中打开
Logger
类代码。 -
Check for the following using statement at the top of the file. This namespace exposes the
Task
class and other types that are used to implement asynchronous programming.using System.Threading.Tasks;
-
Create an asynchronous method that reads the text file. The use of the Task’s
Delay
method is to simulate a long running process.public static async Task<string> LogReadAsync(string filePath)
{
string fileText;
try
{
using (StreamReader oStreamReader = File.OpenText(filePath))
{
fileText = await oStreamReader.ReadToEndAsync();
}
await Task.Delay(10000);
return fileText;
}
catch (FileNotFoundException ex)
{
return ex.Message;
}
catch (IOException ex)
{
return ex.Message;
}
catch
{
return "Logging Failed";
}
}
-
Open frmLogger in the visual design editor. Double-click the btnAsyncRead button to bring up the
btnAsyncRead_Click
event method in the code editor. Alter the method so that it is asynchronous.private async void btnAsyncRead_Click(object sender, EventArgs e)
-
Add code to call the
LogReadAsync
method of theLogger
class and display the results in a message box.btnAsyncRead.Enabled = false;
string s = await Logger.LogReadAsync(txtLogPath.Text);
MessageBox.Show(s);
btnAsyncRead.Enabled = true;
-
选择构建构建解决方案。确保"错误列表"窗口中没有生成错误。如果有,修复它们,然后重新构建。
-
选择调试运行。当表单启动时,单击异步读取按钮。单击异步读取按钮后,单击消息按钮。这一次,您应该得到一个响应,因为您异步调用了
ReadLog
方法。五秒钟后,您应该会看到一个消息框,其中包含了Logger.LogReadAsync
方法的结果。 -
完成测试后,关闭表单。
-
保存项目,然后退出 Visual Studio。
摘要
本章描述了应用中的对象如何协作。您看到了对象如何相互传递消息,事件如何驱动程序,类的实例如何共享数据,以及如何处理异常。
在第九章中,我们看集合和数组。集合和数组将相似的对象组织成一个组。使用集合是您需要在应用中应用的最常见的编程结构之一。您将研究. NET Framework 中可用的一些基本集合类型,并学习如何在代码中使用集合。
九、使用集合
在前一章中,你已经看到了在面向对象的程序中对象是如何协作和通信的。那一章介绍了消息传递、事件、委托、异常处理和异步编程的概念。在这一章中,你将会看到对象集合是如何被组织和处理的。那个 .NET Framework 包含一组用于创建和管理对象集合的广泛的类和接口。您将看到各种类型的集合结构 .NET 提供并了解它们的设计目的以及何时使用它们。您还将看到如何使用泛型来创建高度可重用、高效的集合。
在本章中,您将学习以下内容:
- 公开的各种类型的集合 .NET 框架
- 如何使用数组和数组列表
- 如何创建通用集合
- 如何实现队列和堆栈
介绍 .NET Framework 集合类型
程序员经常需要处理类型的集合。例如,如果你在一个工资系统中处理雇员时间记录,你需要按雇员对记录进行分组,循环遍历记录,并把每个记录的时间加起来。
所有的集合都需要一组基本的函数,比如添加对象、移除对象和遍历它们的对象。除了基本集合之外,一些集合还需要额外的专门功能。例如,在向帮助台电子邮件请求集合中添加和删除项目时,该集合可能需要实现先进先出功能。另一方面,如果请求按严重性划分优先级,那么集合需要能够按优先级对其项目进行排序。
那个 .NET Framework 提供了各种基本的和专门的集合类供您使用。System.Collections
名称空间包含定义各种类型集合的接口和类,例如列表、队列、哈希表和字典。表 9-1 列出并描述了一些常用的收集类。如果没有找到具有所需功能的集合类,可以扩展. NET Framework 类来创建自己的集合类。
表 9-1。常用集合类
类 | 描述 |
---|---|
Array |
为支持强类型数组的语言实现提供基类。 |
ArrayList |
使用大小根据需要动态增加的数组表示弱类型对象列表。 |
SortedList |
表示按键排序并可按键和索引访问的键/值对的集合。 |
Queue |
表示对象的先进先出(FIFO)集合。 |
Stack |
表示简单的后进先出(LIFO)的非泛型对象集合。 |
Hashtable |
表示基于键的哈希代码组织的键/值对的集合。 |
CollectionBase |
为强类型集合提供抽象基类。 |
DictionaryBase |
为键/值对的强类型集合提供抽象基类。 |
表 9-2 描述了这些集合类实现的一些接口。
表 9-2 。集合类接口
界面 | 描述 |
---|---|
ICollection |
为所有非泛型集合定义大小、枚举数和同步方法。 |
IComparer |
公开比较两个对象的方法。 |
IDictionary |
表示键/值对的非泛型集合。 |
IDictionaryEnumerator |
枚举非泛型字典的元素。 |
IEnumerable |
公开枚举数,该枚举数支持对非泛型集合的简单迭代。 |
IEnumerator |
支持对非泛型集合的简单迭代。 |
IList |
表示可以通过索引单独访问的对象的非一般集合。 |
在本章中,您将使用一些常用的集合类,从Array
和ArrayList
类开始。
使用数组和数组列表
数组是计算机编程中最常见的数据结构之一。数组保存相同数据类型的数据元素。例如,你可以创建一个整数、字符串或日期的数组。数组通常用于将值作为参数传递给方法。例如,当您使用控制台应用时,通常会提供命令行开关。下面的 DOS 命令用来在你的电脑上复制一个文件:
copy win.ini c:\windows /y
源文件、目标路径和覆盖指示符作为字符串数组传递到复制程序中。
通过数组的索引来访问数组的元素。索引是一个整数,表示元素在数组中的位置。例如,表示一周中各天的字符串数组具有以下索引值:
| 索引 | 值 |
| Zero | 在星期日 |
| one | 星期一 |
| Two | 星期二 |
| three | 星期三 |
| four | 星期四 |
| five | 星期五 |
| six | 星期六 |
这个星期几的例子是一个一维数组,这意味着索引由单个整数表示。数组也可以是多维的。多维数组元素的索引是一组等于维数的整数。图 9-1 显示了一个座位表,表示一个二维数组,其中一个学生的名字(值)被一对有序的行号、座位号(索引)引用。
图 9-1 。二维数组
当您声明数组的类型时,就实现了数组功能。作为数组实现的常见类型是数字类型,如整数或双精度类型,以及字符和字符串类型。将类型声明为数组时,在类型后使用方括号([]
),后跟数组的名称。数组的元素由一个用花括号({}
)括起来的逗号分隔的列表指定。例如,下面的代码声明了一个类型为int
的数组,并用五个值填充它:
int[] intArray = { 1, 2, 3, 4, 5 };
一旦一个类型被声明为数组,就暴露了Array
类的属性和方法。一些功能包括查询数组的上限和下限、更新数组的元素以及复制数组的元素。Array
类包含许多用于处理数组的静态方法,比如清除、反转和排序数组元素的方法。
下面的代码演示如何声明和使用整数数组。它还使用了几个由Array
类公开的静态方法。注意用于列出数组值的foreach
循环。foreach
循环提供了一种遍历数组元素的方法。
int[] intArray = { 1, 2, 3, 4, 5 };
Console.WriteLine("Upper Bound");
Console.WriteLine(intArray.GetUpperBound(0));
Console.WriteLine("Array elements");
foreach (int item in intArray)
{
Console.WriteLine(item);
}
Array.Reverse(intArray);
Console.WriteLine("Array reversed");
foreach (int item in intArray)
{
Console.WriteLine(item);
}
Array.Clear(intArray, 2, 2);
Console.WriteLine("Elements 2 and 3 cleared");
foreach (int item in intArray)
{
Console.WriteLine(item);
}
intArray[4] = 9;
Console.WriteLine("Element 4 reset");
foreach (int item in intArray)
{
Console.WriteLine(item);
}
Console.ReadLine();
图 9-2 显示了控制台窗口中这段代码的输出。
图 9-2 。一维数组输出
虽然一维数组是您将遇到的最常见的类型,但是您应该理解如何处理偶尔出现的多维数组。二维数组用于存储(在活动内存中)和处理适合表的行和列的数据。例如,您可能需要处理几天内每小时进行的一系列测量(温度或辐射水平)。要创建多维数组,可以在方括号内放置一个或多个逗号来表示维数。一个逗号表示两个维度;两个逗号表示三维,依此类推。填充多维数组时,花括号中的花括号定义了元素。下面的代码声明并填充一个二维数组:
int[,] twoDArray = { { 1, 2 }, { 3, 4 }, { 5, 6 } };
//Print the index and value of the elements
for (int i = 0; i <= twoDArray.GetUpperBound(0); i++)
{
for (int x = 0; x <= twoDArray.GetUpperBound(1); x++)
{
Console.WriteLine("Index = [{0},{1}] Value = {2}", i, x, twoDArray[i, x]);
}
}
图 9-3 显示了控制台窗口中该代码的输出。
图 9-3 。二维数组输出
当您使用集合时,通常直到运行时才知道它们需要包含的项数。这就是ArrayList
类适合的地方。数组列表的容量会根据需要自动扩展,内存重新分配和元素复制会自动执行。ArrayList
类还提供了Array
类没有提供的处理数组元素的方法和属性。下面的代码演示了其中的一些属性和方法。请注意,随着更多姓名的添加,列表的容量会动态扩展。
ArrayList nameList = new ArrayList();
nameList.Add("Bob");
nameList.Add("Dan");
nameList.Add("Wendy");
Console.WriteLine("Original Capacity");
Console.WriteLine(nameList.Capacity);
Console.WriteLine("Original Values");
foreach (object name in nameList)
{
Console.WriteLine(name);
}
nameList.Insert(nameList.IndexOf("Dan"), "Cindy");
nameList.Insert(nameList.IndexOf("Wendy"), "Jim");
Console.WriteLine("New Capacity");
Console.WriteLine(nameList.Capacity);
Console.WriteLine("New Values");
foreach (object name in nameList)
{
Console.WriteLine(name);
}
图 9-4 显示了控制台窗口中的输出。
图 9-4 。数组列表输出
虽然使用数组列表通常比使用数组更容易,但是数组列表只能有一维。此外,特定类型的数组比数组列表提供更好的性能,因为ArrayList
的元素属于类型Object
。当类型被添加到数组列表中时,它们被转换为通用的Object
类型。当从列表中检索项目时,必须再次将它们转换为特定类型。
活动 9-1。使用数组和数组列表
在本活动中,您将熟悉以下内容:
- 创建和使用数组
- 使用多维数组
- 使用数组列表
创建和使用数组
要创建并填充一个数组,遵循以下步骤:
-
启动 Visual Studio。选择文件新建项目。
-
选择控制台应用项目。将项目命名为 Activity9_1。控制台应用包含一个名为 Program 的类,它有一个 Main 方法。Main 方法是应用启动时访问的第一个方法。
-
Notice that the Main method accepts an input parameter of a string array called args. The args array contains any command line arguments passed in when the console application is launched. The members of the args array are separated by a space when passed in.
static void Main(string[] args)
{
}
-
Add the following code to the Main method to display the command line arguments passed in:
Console.WriteLine("parameter count = {0}", args.Length);
for (int i = 0; i < args.Length; i++)
{
Console.WriteLine("Arg[{0}] = [{1}]", i, args[i]);
}
Console.ReadLine();
-
In Solution Explorer, right-click the project node and choose Properties. In the project properties window, select the Debug tab. In the command line arguments field, enter “C# coding is fun” (see Figure 9-5).
图 9-5 。添加命令行参数
-
Select Debug Start to run the project. The console window should launch with the output shown in Figure 9-6. After viewing the output, stop the debugger.
图 9-6 。控制台输出为阵列
-
Add the following code before the Console.ReadLine method in the Main method. This code clears the value of the array at index 1 and sets the value at index 3 to “great”.
Array.Clear(args, 1, 1);
args[3] = "great";
for (int i = 0; i < args.Length; i++)
{
Console.WriteLine("Arg[{0}] = [{1}]", i, args[i]);
}
-
Select Debug Start to run the project. The console window should launch with the additional output shown in Figure 9-7. After viewing the output, stop the debugger.
图 9-7 。更新阵列的控制台输出
使用多维数组
要创建和填充多维数组,请遵循以下步骤:
-
注释掉
Main
方法中的代码。 -
Add the following code to the
Main
method to create and populate a two-dimensional array:string[,] seatingChart = new string[2,2];
seatingChart[0, 0] = "Mary";
seatingChart[0, 1] = "Jim";
seatingChart[1, 0] = "Bob";
seatingChart[1, 1] = "Jane";
-
Add the following code to loop through the array and print the names to the console window:
for (int row = 0; row < 2; row++)
{
for (int seat = 0; seat < 2; seat++)
{
Console.WriteLine("Row: {0} Seat: {1} Student: {2}",
(row + 1),(seat + 1),seatingChart[row, seat]);
}
}
Console.ReadLine();
-
Select Debug Start to run the project. The console window should launch with the output that shows the seating chart of the students (see Figure 9-8).
图 9-8 。二维数组的控制台输出
-
查看输出后,停止调试器。
使用数组列表
虽然您刚刚创建的二维数组可以工作,但是将每个学生的座位分配信息存储在座位分配类中,然后将这些对象组织到一个数组列表结构中可能更直观。要创建和填充座位分配的数组列表,请按照下列步骤操作:
-
向名为 SeatingAssignment.cs 的项目添加一个类文件。
-
Add the following code to create the
SeatingAssignment
class. This class contains aRow
, Seat, andStudent
property. It also contains an overloaded constructor to set these properties.public class SeatingAssignment
{
int _row;
int _seat;
string _student;
public int Row
{
get { return _row; }
set { _row = value; }
}
public int Seat
{
get { return _seat; }
set { _seat = value; }
}
public string Student
{
get { return _student; }
set { _student = value; }
}
public SeatingAssignment(int row, int seat, string student)
{
this.Row = row;
this.Seat = seat;
this.Student = student;
}
}
-
In the
Main
method of theProgram
class, comment out the previous code and add the following using statement to the top of the file:using System.Collections;
-
Add the following code to create an
ArrayList
of seating assignments:ArrayList seatingChart = new ArrayList();
seatingChart.Add(new SeatingAssignment(0, 0, "Mary"));
seatingChart.Add(new SeatingAssignment(0, 1, "Jim"));
seatingChart.Add(new SeatingAssignment(1, 0, "Bob"));
seatingChart.Add(new SeatingAssignment(1, 1, "Jane"));
-
After the
ArrayList
is populated, add the following code to write theSeatingAssignment
information to the console window.foreach (SeatingAssignment sa in seatingChart)
{
Console.WriteLine("Row: {0} Seat: {1} Student: {2}",
(sa.Row + 1), (sa.Seat + 1), sa.Student);
}
Console.ReadLine();
-
选择调试开始运行项目。控制台窗口应启动,输出与图 9-8 (学生座位表)所示的相同。
-
One of the advantages of the ArrayList class is the ability to add and remove items dynamically. Add the following code after the code in step 4 to add two more students to the seating chart:
seatingChart.Add(new SeatingAssignment(2, 0, "Bill"));
seatingChart.Add(new SeatingAssignment(2, 1, "Judy"));
-
选择调试开始运行项目。控制台窗口应该启动,输出显示新学生。
-
完成后,停止调试器,并关闭 Visual Studio。
使用泛型集合
使用集合是应用编程的常见要求。我们处理的大多数数据都需要组织成一个集合。例如,您可能需要从数据库中检索客户,并将他们加载到 UI(用户界面)的下拉列表中。客户信息由一个客户类表示,客户被组织到一个客户集合中。然后可以对集合进行排序、过滤和循环处理。
除了少数强类型的用于保存字符串的专用集合之外,由 .NET 框架是弱类型的。集合持有的项目属于类型Object
,因此它们可以是任何类型,因为所有类型都是从类型Object
派生的。
弱类型集合会导致应用的性能和维护问题。一个问题是没有内在的安全措施来限制存储在集合中的对象类型。同一个集合可以包含任何类型的项,包括日期、整数或自定义类型,如 employee 对象。如果您构建并公开了一个整数集合,而该集合无意中被传递了一个日期,那么代码很可能会在某个时候失败。
幸运的是,C# 支持泛型 .NET Framework 在System.Collections.Generic
命名空间中提供了基于泛型的集合。泛型允许您定义一个类,而无需指定它的类型。类型是在类被实例化时指定的。使用泛型集合提供了类型安全的优势和强类型集合的性能,同时还提供了与弱类型集合相关联的代码重用。
下面的代码展示了如何使用Generic.List
类创建客户的强类型集合。列表类型(在本例中为Customer
)放在尖括号(<>
)之间。Customer 对象被添加到集合中,然后检索集合中的客户,并将客户信息写出到控制台。(你会在第十一章看到将集合绑定到 UI 控件。)
List<Customer> customerList = new List<Customer>();
customerList.Add(new Customer
("WHITC", "White Clover Markets", "Karl Jablonski"));
customerList.Add(new Customer("RANCH", "Rancho grande", "Sergio Gutiérrez"));
customerList.Add(new Customer("ALFKI","Alfreds Futterkiste","Maria Anders"));
customerList.Add
(new Customer("FRANR", "France restauration", "Carine Schmitt"));
foreach (Customer c in customerList)
{
Console.WriteLine("Id: {0} Company: {1} Contact: {2}",
c.CompanyId, c.CompanyName, c.ContactName);
}
有时,您可能需要扩展由 .NET 框架。例如,您可能需要能够按照CompanyId
或CompanyName
对客户集合进行排序。要实现排序,您需要定义一个实现IComparer
接口的排序类。IComparer
接口确保排序类用适当的签名实现了一个Compare
方法。(接口在第七章中讨论过。)下面的CustomerSorter
类按照CompanyName
对一个Customer
的列表进行排序。注意,因为CompanyName
属性是一个字符串,所以可以使用字符串比较器对它们进行排序。
public class CustomerSorter : IComparer<Customer>
{
public int Compare(Customer customer1, Customer customer2)
{
return customer1.CompanyName.CompareTo(customer2.CompanyName);
}
}
现在您可以按CompanyName
对客户进行排序,然后显示它们。
customerList.Sort(new CustomerSorter());
输出如图 9-9 所示。
图 9-9 。客户排序列表的控制台输出
活动 9-2。实现和扩展通用集合
在本活动中,您将熟悉以下内容:
- 实现泛型集合
- 扩展泛型集合以实现排序
要创建和填充通用列表,请遵循以下步骤:
-
启动 Visual Studio。选择文件新建项目。
-
选择控制台应用项目。将项目命名为 Activity9_2。
-
选择项目添加类别。将该类文件命名为 Request.cs。
-
Add the following properties to the
Request
class:public class Request
{
string _requestor;
int _priority;
DateTime _date;
public string Requestor
{
get { return _requestor; }
set { _requestor = value; }
}
public int Priority
{
get { return _priority; }
set { _priority = value; }
}
public DateTime Date
{
get { return _date; }
set { _date = value; }
}
-
Overload the constructor of the
Request
class to set the properties in the constructor.public Request(string requestor, int priority, DateTime date)
{
this.Requestor = requestor;
this.Priority = priority;
this.Date = date;
}
-
Add a method to override the
ToString
method of the baseObject
class. This will return the request information as a string when the method is called.public override string ToString()
{
return String.Format("{0}, {1}, {2}",this.Requestor,
this.Priority.ToString(), this.Date);
}
-
Open the
Program
class in the code editor and add the following code to theMain
method. This code populates a generic list of typeRequest
and displays the values in the console window.static void Main(string[] args)
{
List<Request> reqList = new List<Request>();
reqList.Add(new Request("Dan",2 ,new DateTime(2011,4,2)));
reqList.Add(new Request("Alice", 5, new DateTime(2011, 2, 5)));
reqList.Add(new Request("Bill", 3, new DateTime(2011, 6, 19)));
foreach (Request req in reqList)
{
Console.WriteLine(req.ToString());
}
Console.ReadLine();
}
-
选择调试开始运行项目。控制台窗口应该启动,请求项按照添加到
reqList
的顺序列出。 -
选择项目添加类。给类命名
DateSorter
。 -
Add the following code to the
DateSorter
class. This class implements theIComparer
interface and is used to enable sorting requests by date.
`public class DateSorter:IComparer<Request>`
`{`
`public int Compare(Request R1, Request R2)`
`{`
`return R1.Date.CompareTo(R2.Date);`
`}`
`}`
- Add the following code in the
Main
method of theProgram
class prior to theConsole.ReadLine
method. This code sorts thereqList
by date and displays the values in the console window.
`Console.WriteLine("Sorted by date.");`
`reqList.Sort(new DateSorter());`
`foreach (Request req in reqList)`
`{`
`Console.WriteLine(req.ToString());`
`}`
- Select Debug Start to run the project. The console window should launch with the output shown in Figure 9-10. After viewing the output, stop the debugger and exit Visual Studio.
![9781430249351_Fig09-10.jpg](https://gitee.com/OpenDocCN/vkdoc-csharp-zh/raw/master/docs/begin-cs-oop/img/9781430249351_Fig09-10.jpg)
图 9-10 。未排序和按日期排序的通用集合
使用堆栈和队列编程
编程中经常使用的两种特殊类型的集合是堆栈和队列。堆栈是后进先出的对象集合。队列代表先进先出的对象集合。
栈是一个很好的方法来维护一个国际象棋游戏中的移动列表。当用户想要撤销他的移动时,他从他最近的移动开始,这是最后一个添加到列表中的移动,也是第一个检索到的移动。使用堆栈的另一个例子发生在程序执行一系列方法调用时。堆栈维护方法的地址,执行以调用方法的相反顺序返回方法。将项目放入堆栈时,使用push
方法。 pop
方法从堆栈中移除项目。peek
方法返回堆栈顶部的对象,但不移除它。下面的代码演示如何在堆栈中添加和移除项。在这种情况下,您使用泛型来实现一堆ChessMove
对象。RecordMove
方法将最近的移动添加到堆栈中。GetLastMove
方法返回堆栈上最近的移动。
Stack<ChessMove> moveStack = new Stack<ChessMove>();
void RecordMove(ChessMove move)
{
moveStack.Push(move);
}
ChessMove GetLastMove()
{
return moveStack.Pop();
}
为帮助台请求提供服务的应用是何时使用队列的一个很好的例子。集合维护发送到应用的帮助台请求列表。当从集合中检索请求进行处理时,中的第一个请求应该是最先检索到的请求。Queue
类使用enqueue
和dequeue
方法来添加和移除项目。它还实现了peek
方法来返回队列开头的项,而不删除该项。下面的代码演示了在PaymentRequest
队列中添加和删除项目。AddRequest
方法将请求添加到队列中,而GetNextRequest
方法将请求从队列中移除。
Queue<PaymentRequest> payRequest = new Queue<PaymentRequest>();
void AddRequest(PaymentRequest request)
{
payRequest.Enqueue(request);
}
PaymentRequest GetNextRequest()
{
return payRequest.Dequeue();
}
活动 9-3。实现堆栈和队列
在本活动中,您将熟悉以下内容:
- 实现堆栈集合
- 实现队列集合
要创建和填充通用列表,请遵循以下步骤:
-
启动 Visual Studio。选择文件新建项目。
-
选择控制台应用项目。将项目命名为活动 9_3。
-
Add the following code to the
Main
method of theProgram
class.This code creates a stack of strings and loads it with strings representing moves in a game. It then uses thePeek
method to write out the move stored at the top of the stack to the console window.Stack<string> moveStack = new Stack<string>();
Console.WriteLine("Loading Stack");
for (int i = 1; i <= 5; i++)
{
moveStack.Push("Move " + i.ToString());
Console.WriteLine(moveStack.Peek().ToString());
}
-
Add the following code to the
Main
method of theProgram
class after the code in step 3. This code removes the moves from the stack using thePop
method and writes them to the console window.Console.WriteLine("Press the enter key to unload the stack.");
Console.ReadLine();
for (int i = 1; i <= 5; i++)
{
Console.WriteLine(moveStack.Pop().ToString());
}
Console.ReadLine();
-
Select Debug Start to run the project. The console window should launch with the output shown in Figure 9-11. Notice the last-in, first-out pattern of the stack. After viewing the output, stop the debugger.
图 9-11 。堆栈的后进先出模式
-
Comment out the code entered in steps 3 and 4. Add the following code to the
Main
method of theProgram
class. This code creates a queue of strings and loads it with strings representing requests to a consumer help line. It then uses thePeek
method to write out the request stored at the beginning of the queue of to the console window.Queue<string> requestQueue = new Queue<string>();
Console.WriteLine("Loading Queue");
for (int i = 1; i <= 5; i++)
{
requestQueue.Enqueue("Request " + i.ToString());
Console.WriteLine(requestQueue.Peek().ToString());
}
-
Add the following code to the
Main
method of theProgram
class after the code in step 6. This code removes the requests from the queue using theDequeue
method and writes them to the console window.Console.WriteLine("Press the enter key to unload the queue.");
Console.ReadLine();
for (int i = 1; i <= 5; i++)
{
Console.WriteLine(requestQueue.Dequeue().ToString());
}
Console.ReadLine();
-
Select Debug Start to run the project. The console window should launch with the output shown in Figure 9-12. Notice that as the queue is loaded the first request stays at the top of the queue. Also notice the first-in, first-out pattern of the queue. After viewing the output, stop the debugger and exit Visual Studio.
图 9-12 。队列的先进先出模式
摘要
在本章中,您研究了由 .NET 框架。您学习了如何使用数组、数组列表、队列、堆栈和泛型集合。
本章是向您介绍各种 OOP 结构(如类、继承和多态)的系列文章的最后一章。您应该对 C# 中的类结构、对象协作和集合是如何实现的有一个很好的理解。已经向您介绍了 Visual Studio IDE,并且您已经练习了使用它。现在,您已经准备好将这些部分放在一起,开发一个可工作的应用。
第十章是你将发展的系列中的第一章 .NET 应用。在此过程中,您将使用 ADO.NET 和实体框架调查数据访问;使用 Windows 演示基础(WPF)和新的 Windows 8 应用商店创建基于 Windows 的 GUI 使用 web 窗体和 ASP 创建基于 Web 的 GUI。网;,并使用 ASP.NET 通信基金会(WCF)和 web API 创建 Web 服务。
十、实现数据访问层
在过去的几章中,您已经了解了各种面向对象的编程结构,如类、继承和多态,因为它们是在 C# 代码中实现的。已经向您介绍并练习了如何使用 Visual Studio 集成开发环境。您还应该对类结构和对象协作是如何实现的有一个牢固的理解。
现在,您已经准备好将这些部分放在一起,开发一个可工作的应用。因为大多数业务应用都涉及处理和更新后端关系数据库中的数据,所以您将看到 .NET Framework 提供了处理关系数据的功能。
读完本章后,你将理解以下内容:
- 如何使用
Connection
对象建立到数据库的连接 - 如何使用一个
Command
对象来执行 SQL 查询 - 如何使用一个
Command
对象来执行存储过程 - 如何用
DataReader
对象检索记录 - 如何填充数据表和数据集
- 如何在数据集中的表之间建立关系
- 如何编辑和更新数据集中的数据
- 如何创建实体数据模型
- 如何使用语言集成查询(LINQ)到实体框架(EF)来查询数据
- 如何使用实体框架更新数据
介绍 ADO.NET
为企业开发的大多数应用都需要与数据存储设备进行交互。数据存储可以以多种不同的形式出现:例如,在平面文件系统中,许多传统的大型机系统就是这种情况;或者在关系数据库管理系统中,如 SQL Server、Oracle 或 MySQL。您还可以在分层的文本文件结构中维护数据,如 XML。为了在这些不同的数据存储区中以一致的方式访问和使用数据 .NET Framework 提供了一组组织到System.Data
名称空间中的类。这个类的集合被称为 ADO.NET。
回顾微软数据访问技术的历史,可以发现从连接模式到非连接模式的演变。在开发 20 世纪 80 年代和 90 年代早期流行的传统两层客户机-服务器应用时,打开与数据库的连接,使用实现服务器端游标的数据,并在处理完数据后关闭连接通常会更有效。这种方法的问题在 20 世纪 90 年代后期变得很明显,因为公司试图将其数据驱动的应用从传统的两层客户端-服务器应用发展到多层基于 web 的模型:打开并保持连接直到处理完成是不可伸缩的。可伸缩性是指应用在不明显降低性能的情况下处理数量不断增加的并发客户端的能力。微软将 ADO.NET 设计成高度可伸缩的。为了实现可伸缩性,微软围绕一个断开的模型设计了 ADO.NET。与数据库建立连接,在本地检索和缓存数据和元数据,然后关闭连接。
在此期间开发的传统数据访问技术的另一个问题是缺乏互操作性。具有高度互操作性的系统可以很容易地来回交换数据,而不管各种系统的实现技术如何。传统的数据访问技术依赖于专有的数据交换方法。使用这些技术,对于使用 Microsoft 技术(如 ADO(pre-1)构建的系统来说是很困难的 .NET)和 DCOM 与使用 Java 技术(如 JDBC 和 CORBA)构建的系统交换数据。整个行业都意识到,为不同系统之间的数据交换开发开放标准符合各方的最佳利益。微软已经接受了这些标准,并将对这些标准的支持纳入了 .NET 框架。
使用数据提供者
要建立到一个数据源的连接,比如一个 SQL Server 数据库,并处理它的数据,必须使用适当的 .NET 提供程序类。SQL Server 提供程序类位于System.Data.SqlClient
名称空间中。还存在其他数据提供者,比如位于System.Data.OleDb
名称空间中的 OleDb data provider for Oracle classes。这些提供程序中的每一个都实现了一个相似的类结构,您可以用它来与预期的数据源进行交互。表 10-1 总结了System.Data.SqlClient
提供者名称空间的主要类。
表 10-1。系统中的类。Data.SqlClient 命名空间
班级 | 责任 |
---|---|
SqlConnection |
与数据库建立连接和唯一会话。 |
SqlCommand |
表示要在数据库中执行的 Transact-SQL 语句或存储过程。 |
SqlDataReader |
提供从数据库中读取只进行流的方法。 |
SqlDataAdapter |
填充数据集并将更改更新回数据库。 |
SqlParameter |
表示用于在存储过程之间传递信息的参数。 |
SqlTransaction |
表示要在数据库中进行的 Transact-SQL 事务。 |
SqlError |
收集与数据库服务器返回的警告或错误相关的信息。 |
SqlException |
定义当数据库服务器返回警告或错误时引发的异常。 |
类似的一组类存在于System.Data.OleDb
提供者名称空间中。例如,您有一个OleDbConnection
类,而不是SqlConnection
类。
建立连接
从数据库中检索数据的第一步是建立一个连接,,这是使用一个基于所使用的提供者类型的Connection
对象来完成的。为了建立到 SQL Server 的连接,您实例化了一个类型为SqlConnection
的Connection
对象。您还需要为Connection
对象提供一个ConnectionString
。ConnectionString
由一系列分号分隔的名称-值对组成,提供连接数据库服务器所需的信息。ConnectionString
通常传递的一些信息是目标服务器的名称、数据库的名称和安全信息。以下代码演示了一个用于连接到 SQL Server 数据库的ConnectionString
:
"Data Source=TestServer;Initial Catalog=Pubs;User ID=Dan;Password=training"
您需要通过 ConnectionString 提供的属性取决于您使用的数据提供程序。下面的代码演示了一个 ConnectionString,该 ConnectionString 使用用于 Access 的 OleDb 访问接口连接到 Access 数据库:
"Provider=Microsoft.Jet.OleDb.4.0;Data Source=D:\Data\Northwind.mdb"
下一步是调用Connection
对象的Open
方法。这将导致Connection
对象加载适当的驱动程序并打开到数据源的连接。一旦连接打开,您就可以处理数据了。完成与数据库的交互后,调用Connection
对象的Dispose
方法很重要,因为当Connection
对象超出范围或被垃圾收集时,连接不会被隐式释放。以下代码演示了在 SQL Server 中打开与 Pubs 数据库的连接、处理数据以及释放连接的过程:
SqlConnection pubConnection = new SqlConnection();
string connString;
try
{
connString = "Data Source=drcsrv01;Initial Catalog=pubs;Integrated Security=True";
pubConnection.ConnectionString = connString;
pubConnection.Open();
//work with data
}
catch (SqlException ex)
{
throw ex;
}
finally
{
pubConnection.Dispose();
}
C# 提供了一个using
语句来帮助确保连接被关闭,资源被正确处理。当执行离开using
语句的范围时,连接会自动关闭,连接对象会被高效地处理掉。前面的代码可以重写以利用using
语句,如下所示:
string connString = "Data Source=drcsrv01;Initial Catalog=pubs;Integrated Security=True";
using(SqlConnection pubConnection = new SqlConnection())
{
try
{
pubConnection.ConnectionString = connString;
pubConnection.Open();
//work with data
}
catch (SqlException ex)
{
throw ex;
}
}
执行命令
一旦应用建立并打开了与数据库的连接,就可以对其执行 SQL 语句。一个Command
对象存储并执行针对数据库的命令语句。您可以使用Command
对象来执行数据存储理解的任何有效的 SQL 语句。对于 SQL Server,这些语句可以是数据操作语言语句(Select、Insert、Update 和 Delete)、数据定义语言语句(Create、Alter 和 Drop)或数据控制语言语句(Grant、Deny 和 Revoke)。Command
对象的CommandText
属性保存将要提交的 SQL 语句。根据返回的内容,Command
对象包含三种向数据库提交 CommandText
的方法。如果记录被返回,就像执行Select
语句的情况一样,那么您可以使用ExecuteReader
。如果返回单个值——例如,Select Count
聚合函数的结果——您应该使用ExecuteScalar
方法。当查询没有返回记录时——例如,从Insert
语句中——您应该使用ExecuteNonQuery
方法。下面的代码演示了如何使用一个Command
对象对 Pubs 数据库执行一条 SQL 语句,返回雇员的数量。注意嵌套的using
子句的使用,一个用于连接,一个用于命令。
public int GetEmployeeCount()
{
string connString = "Data Source=drcsrv01;Initial Catalog=pubs;Integrated Security=True";
using (SqlConnection pubConnection = new SqlConnection())
{
using (SqlCommand pubCommand = new SqlCommand())
{
try
{
pubConnection.ConnectionString = connString;
pubConnection.Open();
pubCommand.Connection = pubConnection;
pubCommand.CommandText = "Select Count(emp_id) from employee";
return (int)pubCommand.ExecuteScalar();
}
catch (SqlException ex)
{
throw ex;
}
}
}
}
使用存储过程
在许多应用设计中,客户端必须执行存储过程,而不是直接执行 SQL 语句。存储过程是封装数据库逻辑、提高可伸缩性和增强多层应用安全性的一种极好的方式。要执行一个存储过程,可以使用一个Command
对象,将其CommandType
属性设置为StoredProcedure
,将其CommandText
属性设置为存储过程的名称。下面的代码执行一个存储过程,返回 Pubs 数据库中的雇员人数:
public int GetEmployeeCount()
{
string connString = "Data Source=drcsrv01;Initial Catalog=pubs;Integrated Security=True";
using (SqlConnection pubConnection = new SqlConnection())
{
using (SqlCommand pubCommand = new SqlCommand())
{
try
{
pubConnection.ConnectionString = connString;
pubConnection.Open();
pubCommand.Connection = pubConnection;
pubCommand.CommandType = CommandType.StoredProcedure;
pubCommand.CommandText = "GetEmployeeCount";
return (int)pubCommand.ExecuteScalar();
}
catch (SqlException ex)
{
throw ex;
}
}
}
}
执行存储过程时,通常必须提供输入参数。您可能还需要通过输出参数来检索存储过程的结果。要使用参数,您需要实例化一个类型为SqlParameter
的参数对象,然后将其添加到Command
对象的Parameters
集合中。构造参数时,需要提供参数名称和 SQL Server 数据类型。对于某些数据类型,还需要提供大小。如果参数是输出、输入输出或返回参数,则必须指示参数方向。以下示例调用接受字母输入参数的存储过程。该过程返回姓氏以给定字母开头的雇员数。计数以输出参数的形式返回。
public int GetEmployeeCount(string lastInitial)
{
string connString = "Data Source=drcsrv01;Initial Catalog=pubs;Integrated Security=True";
using (SqlConnection pubConnection = new SqlConnection(connString))
{
using (SqlCommand pubCommand = new SqlCommand())
{
try
{
pubConnection.Open();
pubCommand.Connection = pubConnection;
pubCommand.CommandText = "GetEmployeeCountByLastInitial";
SqlParameter inputParameter = pubCommand.Parameters.Add
("@LastInitial", SqlDbType.NChar, 1);
inputParameter.Value = lastInitial.ToCharArray()[0];
SqlParameter outputParameter = pubCommand.Parameters.Add
("@EmployeeCount", SqlDbType.Int);
outputParameter.Direction = ParameterDirection.Output;
pubCommand.CommandType = CommandType.StoredProcedure;
pubCommand.ExecuteNonQuery();
return (int)outputParameter.Value;
}
catch (SqlException ex)
{
throw ex;
}
}
}
}
使用 DataReader 对象检索数据
一个DataReader
对象通过一个只进、只读的流访问数据。通常,您会希望遍历一组记录并按顺序处理结果,而不需要在缓存中维护数据。这方面的一个很好的例子是用数据库返回的值加载一个列表或数组。在声明了一个类型为SqlDataReader
的对象后,通过调用一个Command
对象的ExecuteReader
方法来实例化它。DataReader
对象的Read
方法访问返回的记录。在处理完记录后,调用DataReader
对象的Close
方法。以下代码演示了如何使用DataReader
对象从 SQL Server 数据库中检索姓名列表并将其返回给客户端:
public ArrayList ListNames()
{
string connString = "Data Source=LocalHost;Initial Catalog=pubs;Integrated Security=True";
using (SqlConnection pubConnection = new SqlConnection(connString))
{
using (SqlCommand pubCommand = new SqlCommand())
{
try
{
pubConnection.ConnectionString = connString;
pubConnection.Open();
pubCommand.Connection = pubConnection;
pubCommand.CommandText =
"Select lname from employee";
using (SqlDataReader employeeDataReader = pubCommand.ExecuteReader())
{
ArrayList nameArray = new ArrayList();
while (employeeDataReader.Read())
{
nameArray.Add(employeeDataReader["lname"]);
}
return nameArray;
}
}
catch (SqlException ex)
{
throw ex;
}
}
}
}
使用 DataAdapter 检索数据
在许多情况下,您需要从数据库中检索一组数据,处理这些数据,并将对数据的任何更新返回给数据库。在这种情况下,您使用一个DataAdapter
作为数据源和数据的内存缓存之间的桥梁。这种数据的内存缓存包含在独立的 DataTable 或 DataSet 中,DataSet 是 DataTable 的集合。
注意DataTable
和DataSet
对象将在“使用数据表和数据集”一节中详细讨论。
要从数据库中检索一组数据,首先要实例化一个DataAdapter
对象。然后将DataAdapter
的SelectCommand
属性设置为现有的Command
对象。最后,执行Fill
方法,传递要填充的DataSet
对象的名称。如果调用 fill 方法时 connection 对象没有打开,则它会打开以检索数据,然后关闭。如果调用 fill 方法时它是打开的,则在检索数据后它将保持打开状态。在这里,您将看到如何使用一个DataAdapter
来填充一个DataTable
并将DataTable
传递回客户端:
public DataTable GetEmployees()
{
string connString = "Data Source=LocalHost;Initial Catalog=pubs;Integrated Security=True";
using (SqlConnection pubConnection = new SqlConnection(connString))
{
using (SqlCommand pubCommand = new SqlCommand())
{
pubCommand.Connection = pubConnection;
pubCommand.CommandText = "Select emp_id, lname, Hire_Date from employee";
using (SqlDataAdapter employeeAdapter = new SqlDataAdapter())
{
employeeAdapter.SelectCommand = pubCommand;
DataTable employeeDataTable = new DataTable();
employeeAdapter.Fill(employeeDataTable);
return employeeDataTable;
}
}
}
}
您可能会发现,与传入 SQL 语句相比,您需要通过执行存储过程来检索一组数据。下面的代码演示如何执行接受输入参数并返回一组记录的存储过程。记录被加载到一个DataSet
对象中并返回给客户端。
public DataSet GetEmployees(string lastInitial)
{
string connString = "Data Source=LocalHost;Initial Catalog=pubs;Integrated Security=True";
using (SqlConnection pubConnection = new SqlConnection(connString))
{
using (SqlCommand pubCommand = new SqlCommand())
{
pubCommand.Connection = pubConnection;
pubCommand.CommandText = "Select emp_id, lname, Hire_Date from employee";
pubCommand.CommandText = "GetEmployeesByLastInitial";
SqlParameter inputParameter = pubCommand.Parameters.Add
("@LastInitial", SqlDbType.NChar, 1);
inputParameter.Value = lastInitial.ToCharArray()[0];
pubCommand.CommandType = CommandType.StoredProcedure;
using (SqlDataAdapter employeeAdapter = new SqlDataAdapter())
{
employeeAdapter.SelectCommand = pubCommand;
DataSet employeeDataSet = new DataSet();
employeeAdapter.Fill(employeeDataSet);
return employeeDataSet;
}
}
}
}
活动 10-1。从 SQL SERVER 数据库中检索数据
在本活动中,您将熟悉以下内容:
- 建立与 SQL Server 数据库的连接
- 通过一个
Command
对象执行查询 - 使用
DataReader
对象检索数据 - 使用
Command
对象执行存储过程
注意要完成本章中的活动,您必须能够访问安装了 Microsoft Pubs 和 Northwind 示例数据库的 SQL Server 2005 或更高版本的数据库服务器。您必须使用被授予访问这些数据库的适当权限的 Windows 帐户登录。根据您的设置,您可能需要更改ConnectionString
。有关更多信息,请参考简介和附录 c 中的“软件要求”一节。
创建连接并执行 SQL 查询
要创建连接并执行 SQL 查询,请按照下列步骤操作:
-
启动 Visual Studio。选择文件新建项目。
-
选择控制台应用项目。将项目命名为
Activity10_1
。 -
项目打开后,向名为
Author
的项目添加一个新类。 -
Open the
Author
class code in the code editor. Add the following using statements at the top of the file:using System.Data;
using System.Data.SqlClient;
-
Add code to declare a private class-level variable containing the connection string:
class Author
{
string _connString = "Data Source=localhost;Initial Catalog=pubs;Integrated Security=True";
-
Add a method to the class that will use a
Command
object to execute a query to count the number of authors in theAuthors
table. Because you are only returning a single value, you will use theExecuteScalar
method of theCommand
object.public int CountAuthors()
{
using (SqlConnection pubConnection = new SqlConnection(_connString))
{
using (SqlCommand pubCommand = new SqlCommand())
{
pubCommand.Connection = pubConnection;
pubCommand.CommandText = "Select Count(au_id) from authors";
pubConnection.Open();
return (int)pubCommand.ExecuteScalar();
}
}
}
-
Add the following code to the
Main
method of theProgram
class, which will execute theGetAuthorCount
method defined in theAuthor
class:static void Main(string[] args)
{
try
{
Author author = new Author();
Console.WriteLine(author.CountAuthors());
Console.ReadLine();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Console.ReadLine();
}
}
-
选择调试开始运行项目。控制台窗口应该会启动,并显示作者的数量。查看输出后,停止调试器。
使用 DataReader 对象检索记录
要使用DataReader
对象来检索记录,请遵循以下步骤:
-
在代码编辑器中打开
Author
类代码。 -
Add a public method to the class definition called
GetAuthorList
that returns a genericList
of strings.public List<string> GetAuthorList()
{
}
-
Add the following code, which executes a SQL Select statement to retrieve the authors’ last names. A
DataReader
object then loops through the records and creates a list of names that gets returned to the client.List<string> nameList = new List<string>();
using (SqlConnection pubConnection = new SqlConnection(_connString))
{
using (SqlCommand authorsCommand = new SqlCommand())
{
authorsCommand.Connection = pubConnection;
authorsCommand.CommandText = "Select au_lname from authors";
pubConnection.Open();
using (SqlDataReader authorDataReader = authorsCommand.ExecuteReader())
{
while (authorDataReader.Read() == true)
{
nameList.Add(authorDataReader.GetString(0));
}
return nameList;
}
}
}
-
Change the code in the
Main
method of theProgram
class to show the list of names in the console window.static void Main(string[] args)
{
try
{
Author author = new Author();
foreach (string name in author.GetAuthorList())
{
Console.WriteLine(name);
}
Console.ReadLine();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Console.ReadLine();
}
}
-
选择调试开始运行项目。控制台窗口应该会启动,并显示作者的姓名。查看输出后,停止调试器。
使用命令对象执行存储过程
要使用Command
对象执行存储过程,请遵循以下步骤:
-
在代码编辑器中打开
Author
类代码。 -
Add a public method that overloads the
GetAuthorList
method by accepting an integer parameter namedRoyalty
. This function will call the stored procedure by royalty in the Pubs database. The procedure takes an integer input of the royalty percentage and returns a list of author IDs with the percentage.public List<string> GetAuthorList(int royalty)
{
List<string> nameList = new List<string>();
using (SqlConnection pubConnection = new SqlConnection(_connString))
{
using (SqlCommand authorsCommand = new SqlCommand())
{
authorsCommand.Connection = pubConnection;
authorsCommand.CommandType = CommandType.StoredProcedure;
authorsCommand.CommandText = "byroyalty";
SqlParameter inputParameter = new SqlParameter();
inputParameter.ParameterName = "@percentage";
inputParameter.Direction = ParameterDirection.Input;
inputParameter.SqlDbType = SqlDbType.Int;
inputParameter.Value = royalty;
authorsCommand.Parameters.Add(inputParameter);
pubConnection.Open();
using (SqlDataReader authorDataReader = authorsCommand.ExecuteReader())
{
while (authorDataReader.Read() == true)
{
nameList.Add(authorDataReader.GetString(0));
}
}
return nameList;
}
}
}
-
In the
Main
method of theProgram
class, supply an input parameter of 25 to theGetAuthorList
method.foreach (string name in author.GetAuthorList(25))
-
选择调试开始运行项目。控制台窗口应该启动,并显示作者的 id。查看输出后,停止调试器。
-
完成测试后,退出 Visual Studio。
使用数据表和数据集
数据表和数据集是数据的内存缓存,提供一致的关系编程模型来处理数据,而不管数据源是什么。数据表代表一个关系数据表,由列、行和约束组成。您可以将数据集视为一个小型关系数据库,它包括数据表和它们之间的关系完整性约束。如果从单个表中检索数据,可以直接填充和使用 DataTable,而无需先创建数据集。创建数据表或数据集有几种方法。最明显的方法是从现有的关系数据库管理系统(RDBMS )(如 SQL Server 数据库)中填充 DataTable 或 DataSet。如前所述,DataAdapter
对象提供了 RDBMS 和数据表或数据集之间的桥梁。通过使用一个DataAdapter
对象,数据表或数据集完全独立于数据源。尽管您需要使用一组特定的提供者类来加载任一类型的对象,但是您使用相同的一组 .NET Framework 类来处理数据表或数据集,而不管它是如何创建和填充的。System.Data
名称空间包含处理数据表或数据集对象的框架类。表 10-2 列出了一些包含在System.Data
名称空间中的主要类。
表 10-2。系统的主要成员。数据命名空间
班级 | 描述 |
---|---|
DataSet |
表示一组DataTable 和DataRelation 对象。组织关系数据的内存缓存。 |
DataTable |
代表一个由DataColumn 、DataRow 和Constraint 对象组成的集合。组织与数据实体相关的记录和字段。 |
DataColumn |
表示DataTable 中某一列的模式。 |
DataRow |
代表DataTable 中的一行数据。 |
Constraint |
表示可以在DataColumn 对象上实施的约束。 |
ForeignKeyConstraint |
在两个DataTable 对象之间实施父/子关系的参照完整性。 |
UniqueConstraint |
强制一个DataColumn 或一组DataColumns 的唯一性。这是在父/子关系中实施参照完整性所必需的。 |
DataRelation |
表示两个DataTable 对象之间的父/子关系。 |
从 SQL Server 数据库填充数据表
要从数据库中检索数据,需要使用一个Connection
对象建立与数据库的连接。建立连接后,创建一个Command
对象从数据库中检索数据。如前所述,如果您从单个表或结果集中检索数据,您可以直接填充并使用DataTable
,而无需创建DataSet
对象。DataTable
的 Load 方法用一个DataReader
对象的内容填充表格。下面的代码用 Pubs 数据库的 publishers 表中的数据填充了一个DataTable
:
public DataTable GetPublishers()
{
string connString = "Data Source=drcsrv01;" +
"Initial Catalog=pubs;Integrated Security=True";
DataTable pubTable;
using (SqlConnection pubConnection = new SqlConnection(connString))
{
using (SqlCommand pubCommand = new SqlCommand())
{
pubCommand.Connection = pubConnection;
pubCommand.CommandText =
"Select pub_id, pub_name, city from publishers";
pubConnection.Open();
using (SqlDataReader pubDataReader = pubCommand.ExecuteReader())
{
pubTable = new DataTable();
pubTable.Load(pubDataReader);
return pubTable;
}
}
}
}
从 SQL Server 数据库填充数据集
当您需要将数据加载到多个表中并维护表之间的引用完整性时,您需要使用DataSet
对象作为DataTables
的容器。为了从数据库中检索数据并填充DataSet
,使用Connection
对象建立与数据库的连接。建立连接后,创建一个Command
对象从数据库中检索数据,然后创建一个DataAdapter
来填充DataSet
,将之前创建的Command
对象设置为DataAdapter
的SelectCommand
属性。为每个DataTable
创建一个单独的DataAdapter
。最后一步是通过执行DataAdapter
的Fill
方法用数据填充DataSet
。下面的代码演示了用 Pubs 数据库的 publishers 表和 titles 表中的数据填充一个DataSet
:
public DataSet GetPubsDS()
{
string connString = "Data Source=Localhost;" +
"Initial Catalog=pubs;Integrated Security=True";
DataSet bookInfoDataSet= new DataSet();
using (SqlConnection pubConnection = new SqlConnection(connString))
{
//Fill pub table
using (SqlCommand pubCommand = new SqlCommand())
{
pubCommand.Connection = pubConnection;
pubCommand.CommandText =
"Select pub_id, pub_name, city from publishers";
using (SqlDataAdapter pubDataAdapter = new SqlDataAdapter())
{
pubDataAdapter.SelectCommand = pubCommand;
pubDataAdapter.Fill(bookInfoDataSet, "Publishers");
}
}
//Fill title table
using (SqlCommand titleCommand = new SqlCommand())
{
titleCommand.Connection = pubConnection;
titleCommand.CommandText =
"Select pub_id, title, ytd_sales from titles";
using (SqlDataAdapter titleDataAdapter = new SqlDataAdapter())
{
titleDataAdapter.SelectCommand = titleCommand;
titleDataAdapter.Fill(bookInfoDataSet, "Titles");
}
}
return bookInfoDataSet;
}
}
在数据集中的表之间建立关系
在 RDBMS 系统中,表之间的引用完整性是通过主键和外键关系来实现的。使用一个DataRelation
对象,您可以在DataSet
中的表之间实施数据引用完整性。该对象包含一个DataColumn
对象数组,这些对象定义了父表和子表之间用于建立关系的公共字段。本质上,父表中标识的字段是主键,子表中标识的字段是外键。建立关系时,为每个表中的公共列创建两个DataColumn
对象。接下来,创建一个DataRelation
对象,为DataRelation
、传递一个名称,并将DataColumn
对象传递给DataRelation
对象的构造函数。最后一步是将DataRelation
添加到DataSet
对象的Relations
集合中。下面的代码在出版商和上一节中创建的bookInfoDataSet
的标题表之间建立了一个关系:
//Create relationahip between tables
DataRelation Pub_TitleRelation;
DataColumn Pub_PubIdColumn;
DataColumn Title_PubIdColumn;
Pub_PubIdColumn = bookInfoDataSet.Tables["Publishers"].Columns["pub_id"];
Title_PubIdColumn = bookInfoDataSet.Tables["Titles"].Columns["pub_id"];
Pub_TitleRelation = new DataRelation("PubsToTitles", Pub_PubIdColumn, Title_PubIdColumn);
bookInfoDataSet.Relations.Add(Pub_TitleRelation);
编辑数据集中的数据
客户端通常需要能够更新数据集。他们可能需要添加记录、删除记录或更新现有记录。因为数据集对象在设计上是断开连接的,所以对数据集所做的更改不会自动传播回数据库。它们保存在本地,直到客户机准备好将更改复制回数据库。要复制更改,您需要调用DataAdapter
的Update
方法,该方法确定对记录做了什么更改,并实现适当的 SQL 命令(Update、Insert 或 Delete ),该命令已被定义为将更改复制回数据库。
当您创建一个Update
命令时,命令文本引用命令的Parameters
集合中与数据表中的字段相对应的参数。
SqlCommand updateCommand = new SqlCommand();
string updateSQL = "Update publishers set pub_name = @pub_name," +
" city = @city where pub_id = @pub_id";
updateCommand = new SqlCommand(updateSQL, pubConnection);
updateCommand.CommandType = CommandType.Text;
对于Update
语句中的每个参数,Parameter
对象被添加到Command
对象的Parameter
集合中。向Parameters
集合的Add
方法传递关于参数名、SQL 数据类型、大小和数据集的源列的信息。
updateCommand.Parameters.Add("@pub_id", SqlDbType.Char, 4, "pub_id");
updateCommand.Parameters.Add("@city", SqlDbType.VarChar, 20, "city");
updateCommand.Parameters.Add("@pub_name", SqlDbType.VarChar, 40, "pub_name");
Once the parameters are created, the updateCommand object is set to the UpdateCommand property of the DataAdapter object.
pubDataAdapter.UpdateCommand = updateCommand;
现在已经设置了 SqlDataAdapter,您调用 SQLDataAdapter 的Update
方法,传入DataSet
和要更新的表的名称。SQLDataAdapter 检查表中行的RowState
属性,并对要更新的每一行迭代执行 update 语句。
pubDataAdapter.Update(bookInfoDataSet, "Publishers");
以类似的方式,您可以实现DataAdapter
的InsertCommand
和DeleteCommand
属性,以允许客户端在数据库中插入新记录或删除记录。
注意对于数据源中单个表的简单更新 .NET 框架提供了一个CommandBuilder
类来自动创建DataAdapter
的InsertCommand
、UpdateCommand
和DeleteCommand
属性。
活动 10-2。使用数据集对象
在本活动中,您将熟悉以下内容:
- 从 SQL Server 数据库填充一个
DataSet
- 编辑
DataSet
中的数据 - 将更改从
DataSet
更新到数据库 - 在
DataSet
中建立表之间的关系
从 SQL Server 数据库填充数据集
要从 SQL Server 数据库填充一个DataSet
,请执行以下步骤:
-
启动 Visual Studio。选择文件新建项目。
-
选择 Windows 窗体应用。将项目重命名为 Activity10_2,然后单击“确定”按钮。
-
项目打开后,向名为
Author
的项目添加一个新类。 -
Open the
Author
class code in the code editor. Add the followingusing
statements at the top of the file:using System.Data;
using System.Data.SqlClient;
-
Add the following code to declare a private class level variable for the connection string.
class Author
{
string _connString = "Data Source=localhost;" + "Initial Catalog=pubs;Integrated Security=True";
-
Create a method of the
Author
class calledGetData
that will use aDataAdapter
object to fill theDataSet
and return it to the client.public DataSet GetData()
{
DataSet authorDataSet;
using (SqlConnection pubConnection = new SqlConnection())
{
pubConnection.ConnectionString = _connString;
using(SqlCommand selectCommand = new SqlCommand())
{
selectCommand.CommandText = "Select au_id, au_lname,au_fname from authors";
selectCommand.Connection = pubConnection;
using (SqlDataAdapter pubDataAdapter = new SqlDataAdapter())
{
pubDataAdapter.SelectCommand = selectCommand;
authorDataSet = new DataSet();
pubDataAdapter.Fill(authorDataSet, "Author");
}
}
return authorDataSet;
}
-
构建项目并修复任何错误。
-
Add the controls listed in Table 10-3 to Form1 and set the properties as shown.
表 10-3。表格 1 控件
控制 财产 价值 DataGridView
Name
dgvAuthors
AllowUserToAddRows
False
AllowUserToDeleteRows
False
ReadOnly
False
Button
Name
btnGetData
Text
Get Data
Button
Name
btnUpdate
Text
Update
-
Open the
Form1
class code file in the code editor. Declare a class-levelDataSet
object after the class declaration.public partial class Form1 : Form
{
private DataSet _pubDataSet;
-
在窗体设计器中打开 Form1。双击“获取数据”按钮,在代码编辑器中打开按钮单击事件方法。
-
Add the following code to the
btnGetData
click event procedure, which will execute theGetData
method defined in theAuthor
class. This dataset is then loaded into the grid using theDataSource
property.
`private void btnGetData_Click(object sender, EventArgs e)`
`{`
`Author author = new Author();`
`_pubDataSet = author.GetData();`
`dgvAuthors.DataSource = _pubDataSet.Tables["Author"];`
`}`
- 构建项目并修复任何错误。一旦项目构建完成,在调试模式下运行项目并测试
GetData
方法。您应该会看到网格中充满了作者信息。测试后,停止调试器。
编辑和更新数据集中的数据
要编辑和更新DataSet
中的数据,请遵循以下步骤:
-
在代码编辑器中打开
Author
类代码。 -
Create a method of the
Author
class calledUpdateData
that will use theUpdate
method of theDataAdapter
object to pass updates made to theDataSet
to the Pubs database.public void UpdateData(DataSet changedData)
{
using (SqlConnection pubConnection = new SqlConnection())
{
pubConnection.ConnectionString = _connString;
using (SqlCommand updateCommand = new SqlCommand())
{
updateCommand.CommandText = "Update authors set au_lname = @au_lname," +
"au_fname = @au_fname where au_id = @au_id";
updateCommand.Parameters.Add ("@au_id", SqlDbType.VarChar, 11, "au_id");
updateCommand.Parameters.Add ("@au_lname", SqlDbType.VarChar, 40, "au_lname");
updateCommand.Parameters.Add ("@au_lname", SqlDbType.VarChar, 40, "au_lname");
updateCommand.Connection = pubConnection;
using (SqlDataAdapter pubDataAdapter = new SqlDataAdapter())
{
pubDataAdapter.UpdateCommand = updateCommand;
pubDataAdapter.Update(changedData, "Author");
}
}
}
}
-
构建项目并修复任何错误。
-
在表单设计器中打开
Form1
。双击“更新数据”按钮,在代码编辑器中打开按钮 click 事件方法。 -
Add the following code to the
btnUpdate
click event procedure, which will execute theUpdateData
method defined in theAuthor
class. By using theGetChanges
method of theDataSet
object, only data that has changed is passed for updating.private void btnUpdate_Click(object sender, EventArgs e)
{
Author author = new Author();
author.UpdateData(_pubDataSet.GetChanges());
}
-
构建项目并修复任何错误。一旦项目构建完成,在调试模式下运行项目并测试
Update
方法。首先,单击“获取数据”按钮。更改几个作者的姓氏,然后单击更新按钮。再次单击“获取数据”按钮,从数据库中检索已更改的值。测试后,停止调试器。
建立数据集中表格之间的关系
要在一个DataSet
中建立表之间的关系,请遵循以下步骤:
-
将名为
StoreSales
的新类添加到项目中。 -
Open the
StoreSales
class code in the code editor. Add the followingusing
statements at the top of the file:using System.Data;
using System.Data.SqlClient;
-
Add the following code to declare private class level variables for the connection string and DataSet.
class StoreSales
{
string _connString = "Data Source=localhost;" + "Initial Catalog=pubs;Integrated Security=True";
-
Create a method of the
StoreSales
class calledGetData
that will select store information and sales information and establish a relationship between them. This information is used to fill aDataSet
and return it to the client.public DataSet GetData()
{
DataSet storeSalesDataSet;
storeSalesDataSet = new DataSet();
using (SqlConnection pubConnection = new SqlConnection(_connString))
{
//Fill store table
using (SqlCommand storeCommand = new SqlCommand())
{
storeCommand.Connection = pubConnection;
storeCommand.CommandText =
"SELECT [stor_id],[stor_name],[city],[state] FROM [stores]";
using (SqlDataAdapter storeDataAdapter = new SqlDataAdapter())
{
storeDataAdapter.SelectCommand = storeCommand;
storeDataAdapter.Fill(storeSalesDataSet, "Stores");
}
}
//Fill sales table command
using (SqlCommand salesCommand = new SqlCommand())
{
salesCommand.Connection = pubConnection;
salesCommand.CommandText =
"SELECT [stor_id],[ord_num],[ord_date],[qty] FROM [sales]";
using (SqlDataAdapter salesDataAdapter = new SqlDataAdapter())
{
salesDataAdapter.SelectCommand = salesCommand;
salesDataAdapter.Fill(storeSalesDataSet, "Sales");
}
}
//Create relationahip between tables
DataColumn Store_StoreIdColumn =
storeSalesDataSet.Tables["Stores"].Columns["stor_id"];
DataColumn Sales_StoreIdColumn =
storeSalesDataSet.Tables["Sales"].Columns["stor_id"];
DataRelation StoreSalesRelation = new DataRelation ("StoresToSales", Store_StoreIdColumn, Sales_StoreIdColumn);
``storeSalesDataSet.Relations.Add(StoreSalesRelation);`
return storeSalesDataSet;
}
}``
* 构建项目并修复任何错误。* Add a second form to the project. Add the controls listed in Table 10-4 to Form2 and set the properties as shown.表 10-4。Form2 控件
控制 财产 价值 DataGridView
Name
dgvStores
DataGridView
Name
dgvSales
Button
Name
btnGetData
Text
Get Data
- Open the
Form2
class code file in the code editor. Declare a class-levelDataSet
object after the class declaration.
public partial class Form2 : Form
{
DataSet StoreSalesDataSet;
- 在窗体设计器中打开 Form2。双击“获取数据”按钮,在代码编辑器中打开按钮单击事件方法。* Add the following code to the
btnGetData
click event procedure, which will execute theGetData
method defined in theStoreSales
class. This Stores table is then loaded into the Stores grid using theDataSource
property. Setting theDataMember
property of the Sales grid loads it with the sales data of the store selected in the Stores grid.
private void btnGetData_Click(object sender, EventArgs e)
{
StoreSales storeSales = new StoreSales();
StoreSalesDataSet = storeSales.GetData();
dgvStores.DataSource = StoreSalesDataSet.Tables["Stores"];
dgvSales.DataSource =StoreSalesDataSet.Tables["Stores"];
dgvSales.DataMember = "StoresToSales";
}
- Open the
Program
class in the code editor. Change the code to launch Form2 when the form loads.
Application.Run(new Form2());
- 表单加载后,单击“获取数据”按钮加载网格。在商店网格中选择一个新行应该会更新销售网格,以显示商店的销售额。完成测试后,停止调试器并退出 Visual Studio。`
- Open the
`使用实体框架
实体框架(EF) 是内置于 ADO.NET 的对象关系映射(ORM)技术。EF 消除了 .NET 语言和数据库系统的关系数据结构。例如,要加载和使用客户对象,开发人员必须向数据库引擎发送一个 SQL 字符串。这要求开发人员熟悉数据的关系模式。此外,SQL 被硬编码到应用中,应用不会受到底层模式变化的影响。另一个缺点是,由于应用将 SQL 语句作为字符串发送到数据库引擎进行处理,Visual Studio 无法实现语法检查并发出警告和构建错误来提高程序员的工作效率。
实体框架提供了映射模式,允许程序员在更高的抽象层次上工作。他们可以使用面向对象的结构编写代码来查询和加载实体(由类定义的对象)。映射模式将针对实体的查询转换为针对数据执行 CRUD(创建、读取、更新和删除)操作所需的特定于数据库的语言。
为了在您的应用中使用实体框架,您必须首先将 ADO.NET 实体数据模型添加到您的应用中。这一步启动实体数据模型向导,,它允许您从头开始开发您的模型,或者从现有的数据库中生成它。选择从现有数据库生成它,可以创建到数据库的连接,并选择要包含在模型中的表、视图和存储过程。向导的输出是一个.edmx
文件。这个文件是一个基于 XML 的文件,有三个部分。第一种由商店模式定义语言(SSDL) 组成;这描述了存储数据的表格和关系。以下代码显示了从 Pubs 数据库生成的数据模型的 SSDL 的一部分:
<EntityContainer Name="pubsModelStoreContainer">
<EntitySet Name="sales" EntityType="pubsModel.Store.sales"
store:Type="Tables" Schema="dbo" />
<EntitySet Name="stores" EntityType="pubsModel.Store.stores"
store:Type="Tables" Schema="dbo" />
<AssociationSet Name="FK__sales__stor_id__1273C1CD"
Association="pubsModel.Store.FK__sales__stor_id__1273C1CD">
<End Role="stores" EntitySet="stores" />
<End Role="sales" EntitySet="sales" />
</AssociationSet>
</EntityContainer>
<EntityType Name="sales">
<Key>
<PropertyRef Name="stor_id" />
<PropertyRef Name="ord_num" />
<PropertyRef Name="title_id" />
</Key>
<Property Name="stor_id" Type="char" Nullable="false" MaxLength="4" />
<Property Name="ord_num" Type="varchar" Nullable="false" MaxLength="20" />
<Property Name="ord_date" Type="datetime" Nullable="false" />
<Property Name="qty" Type="smallint" Nullable="false" />
<Property Name="payterms" Type="varchar" Nullable="false" MaxLength="12" />
<Property Name="title_id" Type="varchar" Nullable="false" MaxLength="6" />
</EntityType>
第二部分由概念图式定义语言(CSDL);它指定了实体以及它们之间的关系。这些实体用于处理应用中的数据。以下代码来自 Pubs 数据库生成的数据模型的 CSDL 部分:
<EntityContainer Name="pubsEntities" annotation:LazyLoadingEnabled="true">
<EntitySet Name="sales" EntityType="pubsModel.sale" />
<EntitySet Name="stores" EntityType="pubsModel.store" />
<AssociationSet Name="FK__sales__stor_id__1273C1CD"
Association="pubsModel.FK__sales__stor_id__1273C1CD">
<End Role="stores" EntitySet="stores" />
<End Role="sales" EntitySet="sales" />
</AssociationSet>
</EntityContainer>
<EntityType Name="sale">
<Key>
<PropertyRef Name="stor_id" />
<PropertyRef Name="ord_num" />
<PropertyRef Name="title_id" />
</Key>
<Property Name="stor_id" Type="String" Nullable="false"
MaxLength="4" Unicode="false" FixedLength="true" />
<Property Name="ord_num" Type="String" Nullable="false"
MaxLength="20" Unicode="false" FixedLength="false" />
<Property Name="ord_date" Type="DateTime" Nullable="false" />
<Property Name="qty" Type="Int16" Nullable="false" />
<Property Name="payterms" Type="String" Nullable="false"
MaxLength="12" Unicode="false" FixedLength="false" />
<Property Name="title_id" Type="String" Nullable="false"
MaxLength="6" Unicode="false" FixedLength="false" />
<NavigationProperty Name="store"
Relationship="pubsModel.FK__sales__stor_id__1273C1CD"
FromRole="sales" ToRole="stores" />
</EntityType>
.edmx
文件的最后一部分由用映射规范语言(MSL) 编写的代码组成。MSL 将概念模型映射到存储模型。以下代码显示了从 Pubs 数据库生成的数据模型的 MSL 部分的一部分:
<EntityContainerMapping StorageEntityContainer="pubsModelStoreContainer"
CdmEntityContainer="pubsEntities">
<EntitySetMapping Name="sales"><EntityTypeMapping TypeName="pubsModel.sale">
<MappingFragment StoreEntitySet="sales">
<ScalarProperty Name="stor_id" ColumnName="stor_id" />
<ScalarProperty Name="ord_num" ColumnName="ord_num" />
<ScalarProperty Name="ord_date" ColumnName="ord_date" />
<ScalarProperty Name="qty" ColumnName="qty" />
<ScalarProperty Name="payterms" ColumnName="payterms" />
<ScalarProperty Name="title_id" ColumnName="title_id" />
</MappingFragment></EntityTypeMapping></EntitySetMapping>
Visual Studio 提供了一个可视化设计器来处理实体模型。使用这个设计器,你可以更新实体,添加关联,实现继承。在可视化模型中所做的更改被转换回。相应更新的 edmx 文件。图 10-1 显示了可视化设计器中从 pubs 数据库生成的实体。
图 10-1 。实体模型设计器中的实体
从 LINQ 到英孚查询实体
当使用实体数据模型向导创建 ADO.NET 实体数据模型时,会创建一个代表模型中定义的实体容器的ObjectContext
类。ObjectContext
类支持针对实体模型的基于 CRUD 的查询。针对ObjectContext
类编写的查询是使用 LINQ 到 EF 编写的。如前所述,LINQ 代表语言集成查询。LINQ 允许开发人员用 C# 语法编写查询,当执行时,查询被转换成数据提供者的查询语法。一旦执行了查询并返回了数据,实体框架就将结果转换回实体对象模型。
下面的代码使用Select
方法返回 Stores 表中的所有行,并将结果作为一列Store
实体返回。商店名称随后被写入控制台窗口。
var context = new pubsEntities();
var query = from s in context.stores
select s;
var stores = query.ToList();
foreach (store s in stores)
{
Console.WriteLine(s.stor_name);
}
Console.ReadLine();
LINQ 到 EF 提供了一组丰富的查询操作,包括过滤、排序和分组操作。下面的代码演示了按状态过滤商店:
var context = new pubsEntities();
var query = from s in context.stores
where s.state == "WA"
select s;
var stores = query.ToList();
以下代码选择订购了 25 个以上对象的销售实体,然后按降序对它们进行排序:
var context = new pubsEntities();
var query = from s in context.sales
where s.qty > 25
orderby s.ord_date descending
select s;
var sales = query.ToList();
由于实体框架包括实体之间的导航属性,您可以轻松地基于相关实体构建复杂的查询。以下查询选择有五个以上销售订单的商店:
var context = new pubsEntities();
var query = from s in context.stores
where s.sales.Count > 5
select s;
var stores = query.ToList();
注关于 LINQ 查询语言的更多信息,请参考位于http://msdn.microsoft.com
的 MSDN 图书馆。
使用实体框架更新实体
实体框架跟踪对在Context
对象中表示的实体类型所做的改变。您可以添加、更新或删除实体对象。当您准备好将更改保存回数据库时,您可以调用context
对象的SaveChanges
方法。EF 创建并执行针对数据库的插入、更新或删除语句。您还可以显式映射存储过程来实现数据库命令。以下代码使用商店 ID 选择一个商店,更新商店名称,并将其发送回数据库:
var context = new pubsEntities();
var store = (from s in context.stores
where s.stor_id == storeId
select s).First();
store.stor_name = "DRC Books";
context.SaveChanges();
活动 10-3。使用实体框架检索数据
在本活动中,您将熟悉以下内容:
- 创建实体数据模型
- 使用 LINQ 到 EF 执行查询
创建实体数据模型
要创建实体数据模型,请遵循以下步骤:
-
启动 Visual Studio。选择文件新建项目。
-
选择控制台应用。将项目重命名为 Activity10_3,然后单击“确定”按钮。
-
在解决方案资源管理器中右键单击项目节点,并选择 Add New Item。
-
在“添加新项”窗口的“数据”节点下,选择一个 ADO.NET 实体数据模型。将模型命名为 Pubs.edmx,然后单击 Add。
-
在选择模型内容屏幕中,选择从数据库生成,然后单击下一步。
-
In the Choose Your Data Connection screen, create a connection to the Pubs database and choose Next. (See Figure 10-2)
图 10-2 。使用实体数据模型向导创建数据库连接
-
In the Choose Your Database Objects screen, expand the Tables node and select the Sales, Stores, and Titles tables, as shown in Figure 10-3. Click Finish.
图 10-3 。为实体数据模型选择数据库对象
-
You are presented with the Entity Model Designer containing the sales, store, and title entities, as shown in Figure 10-4.
图 10-4 。实体模型设计器
-
在实体模型设计器中,右击
title
实体并选择重命名。改名为book
。在图书实体中,将title1
属性重命名为title
。
查询实体数据模型
要使用 LINQ 查询该实体数据模型,请按照下列步骤操作:
-
在代码编辑器窗口中打开 Program.cs 文件。
-
Add the following method to select the book entities and write their titles to the Console window:
private static void GetTitles()
{
var context = new pubsEntities();
var query = from b in context.books select b;
var books = query.ToList();
foreach (book b in books)
{
Console.WriteLine(b.title);
}
Console.ReadLine();
}
-
Call the
GetTitles
method from theMain
method.static void Main(string[] args)
{
GetTitles();
}
-
在调试模式下运行程序。您应该会看到控制台窗口中列出的标题。完成测试后,停止调试器。
-
Add the following method that gets books in the 10 to 20 dollar range and orders them by price:
private static void GetTitlesByPrice()
{
var context = new pubsEntities();
var query = from b in context.books
where b.price >= (decimal)10.00
&& b.price <= (decimal)20.00
orderby b.price
select b;
var books = query.ToList();
foreach (book b in books)
{
Console.WriteLine(b.price + " -- " + b.title);
}
Console.ReadLine();
}
-
Call the GetTitlesByPrice method from the Main method.
static void Main(string[] args)
{
//GetTitles();
GetTitlesByPrice();
}
-
在调试模式下运行程序。您应该会在控制台窗口中看到标题和价格。完成测试后,停止调试器。
-
Add the following method to list the book titles and the sum of their sales amount. Notice that this query gets the sales amount by adding up the book’s related sales entities.
private static void GetBooksSold()
{
var context = new pubsEntities();
var query = from b in context.books
select new
{
BookID = b.title_id,
TotalSold = b.sales.Sum(s =>(int?) s.qty)
};
foreach (var item in query)
{
Console.WriteLine(item.BookID + " -- " + item.TotalSold);
}
Console.ReadLine();
}
-
Call the
GetBooksSold
method from theMain
method.static void Main(string[] args)
{
//GetTitles();
//GetTitlesByPrice();
GetBooksSold();
}
-
在调试模式下运行程序。您应该会在控制台窗口中看到图书 id 和销售量。完成测试后,停止调试器并退出 Visual Studio。
摘要
本章是向您展示如何构建 OOP 应用各层的系列文章的第一章。为了实现应用的数据访问层,您学习了 about 和用于处理关系数据源的类。您看到了组成System.Data.SqlClient
名称空间的各种类;这些类检索和更新存储在 SQL Server 数据库中的数据。您还研究了处理断开连接的数据的System.Data
名称空间类。此外,您还接触了实体框架和 LINQ,看到了它们如何允许您使用 OOP 结构查询数据。您根据实体编写查询,框架将查询翻译成数据源的查询语法,检索数据,并加载实体。
在第十一章中,你将看到如何实现 Windows 应用的用户界面层。在这个过程中,您将进一步了解 .NET Framework 用于创建丰富的基于 Windows 的用户界面。`
十一、开发 WPF 应用
在第十章中,你学习了如何构建应用的数据访问层。为了实现它的逻辑,您使用了System.Data
名称空间的类。这些类检索和处理关系数据,这是许多业务应用的常见需求。现在,您已经准备好了解用户将如何与您的应用进行交互。用户通过用户界面层与应用进行交互。这一层又与业务逻辑层交互,业务逻辑层又与数据访问层交互。在本章中,您将学习如何使用?NET Windows 演示基础(WPF)。WPF 通常用于开发在 Windows 7 及更高版本上运行的桌面业务生产力应用。商业生产力应用面向需要查询和更新存储在后端数据库中的数据的商业用户。它由一套全面的应用开发功能组成,包括可扩展应用标记语言(XAML) 、控件、数据绑定和布局。本章讨论的概念也将为第十三章奠定基础,在第十三章中,我们将着眼于为 Windows 8 创建新的 Windows Store 应用用户界面。这些应用使用类似的模型来创建具有 XAML 和数据绑定控件的界面。
阅读本章后,您将能够轻松执行以下任务:
- 使用 XAML 标记设计用户界面
- 使用布局控件
- 使用显示控件
- 响应控制事件
- 使用数据绑定控件
- 创建和使用控件模板
Windows 基础
窗口是具有可视界面的对象,绘制在屏幕上,为用户提供与程序交互的方式。像面向对象语言中的大多数对象一样 .NET 窗口公开属性、方法和事件。窗口的属性定义了它的外观。例如,它的Background
属性决定了它的颜色。窗口的方法定义了它的行为。例如,调用它的Hide
方法对用户隐藏它。窗口的事件定义了与用户(或其他对象)的交互。例如,当用户在窗口上单击鼠标右键时,您可以使用MouseDown
事件来启动一个动作。
控件是具有可视化界面的组件,为用户提供了与程序交互的方式。窗口是一种特殊类型的控件,称为容器控件,它承载其他控件。您可以在窗口上放置许多不同类型的控件。在 windows 上使用的一些常见控件有文本框、标签、选项按钮、列表框和复选框。除了提供的控件之外 .NET Framework,您也可以创建自己的自定义控件或从第三方供应商处购买控件。
介绍 XAML
WPF 用户界面是使用一种称为 XAML 的声明性标记语言构建的。 XAML 声明组成界面的控件。左尖括号(<
)后跟控件类型名称和右尖括号定义控件。例如,下面的标记在网格中定义了一个按钮控件。
```cs`