发布新日志

  • 网络协议::HTTP详解

    2010-03-10 22:34:14

     

    1. HTTP版本

    HTTP/1.0

    HTTP/1.1

    HTTP-NG

     

    2. 会话方式

    HTTP/1.0

    建立连接->请求->响应->断开连接

    每次连接只处理一次请求和相应,对资源的每一次访问都要建立一个单独的连接。

    浏览器到服务器的每次通讯都是完全分开的。

    没有Host域,所以不可以创建基于主机头的虚拟主机。

     

    HTTP/1.1

    在一个TCP连接中可以传送多个HTTP请求和响应

    不需要等待上次HTTP响应完毕,可以多个HTTP请求同时进行。服务器会根据浏览器发送的请求顺序来按顺序进行响应,这被称作管线

    Host域,可以建立虚拟主机。

    3. 请求消息

    结构:

    请求行

    若干消息头(可选)

    (CRLF)

    实体内容(可选)

     

    GET无实体内容

     

    4. 响应消息

    结构:

    状态行

    若干消息头(可选)

    (CRLF)

    实体内容(可选)

     

    5. HTTP消息

    对于HTTP/1.1,如果消息中包括实体内容,且没有采用Transfer-Encoding: chunked传输编码方式,则必须要有Content-Length消息头。否则浏览器和服务器都不知道何时消息结束。

     

    6. 简单请求消息和简单响应消息

    没有消息头。

    其中,简单请求消息只可用于GET方式,且请求行中不指定HTTP版本号,

    对于简单请求消息,服务器将会返回简单响应消息,只返回实体内容。

    如:

    ROOT /index.html

     

    7. HTTP消息头概述

    浏览器通过消息头,比如可以告诉服务器浏览器的国家语言版本信息,可以告诉服务器访问者是从哪个页面访问到当前页面的。

     

    服务器通过消息头,比如可以告诉浏览器隔多长时间刷新一下,用哪种字符集显示内容,创建Cookie

     

    结构:

    头字段名称(不区分大小写):_值,值,值(CRLF)

    头字段名称(不区分大小写):_值,值,值(CRLF)

    头字段名称(不区分大小写):_值,值,值(CRLF)

     (可以任意顺序排列)

     

    分为:通用信息头、请求头、响应头、实体头四类。

     

    8. 请求行

    结构:

    请求方式_资源路径_HTTP版本号(CRLF)

    如:

    GET /index.htm HTTP/1.1

     

    请求方式:

    Method

     

    GET

    请求获取Request-URI所标识的资源

    POST

    Request-URI所标识的的资源后附加新的数据

    HEAD

    请求获取Request-URI所标识的资源的响应消息报头

    PUT

    请求服务器存储一个资源,并用Request-URI作为资源标识

    DELETE

    请求服务器删除Request-URI所标识的资源

    TRACE

    请求服务器回送收到的请求信息,主要用于测试和诊断

    CONNECT

    保留将来使用

    OPTIONS

    请求查询服务器的性能,或者查询与资源相关的选项和需求

     

    9. 状态行

    结构:

    HTTP版本号_状态码_状态描述(CRLF)

    如:

    HTTP/1.1 200 OK

     

    10. 使用GETPOST传递参数

    GET使用URL传递参数

    如:

    GET /List.aspx?Catagoryid=5&Cityid=23 HTTP/1.1

     

    POST使用实体内容传递参数

    如:

    POST /List.aspx HTTP/1.1

    Content-Type:application/x-www-form-urlencoded

    Content-Length:22

     

    Catagoryid=5&Cityid=23

     

    POST消息头中要设置Content-Type的值为application/x-www-form-urlencoded,以及使用Content-Length 以标识实体内容的长度。

     

    Content-Length长度比实体内容长度短时,则会忽略多出部分的实体内容。当Content-Length大于实体长度时,则会继续等待。

     

    11. 响应状态码

     

    状态代码由三位数字组成,第一位定义了响应的类别:

    1xx:指示信息——表示请求已接收,继续处理。

    2xx:成功——表示请求已被成功接收、理解、接受。

    3xx:重定向——要完成请求必须进行更进一步的操作。

    4xx:客户端错误——请求有语法错误或请求无法实现。

    5xx:服务器端错误——服务器未能实现合法的请求。

     

    常见状态码:

    Status-Code

    Reason-Phrase

     

    200

    OK

    客户端请求成功

    206

     

    客户端发送了带有Range头的GET请求,服务器正确的返回了该范围的数据

    302/307

     

    指出被请求的文档已经临时移动到别处,此文档的新的URLLocation响应头中给出

    304

     

    客户机缓存的版本是最新的,客户机应该继续使用它

    400

    Bad Request

    客户端请求有语法错误,不能被服务器理解

    401

    Unauthorized

    表示客户机访问的是一个受口令和密码保护的页面,并且在WWW-Authenticate响应头提示客户机应重新发出一个带有Authorization头的请求信息。

    403

    Forbidden

    服务器收到请求,但是拒绝提供服务

    404

    Not Found

    请求的资源不存在

    500

    Internal Server Error

    服务器端的CGIASPJSP发生错误

    503

    Server Unavaliable

    服务器当前不能处理客户端的请求,一段时间后可能恢复正常

     

    12. 通用信息头

    通用信息头既能用于请求消息中,也可以用于响应消息中,他包括一些与被传输的实体没有关系的常用消息头字段。

     

    Cache-Control: no-cache

    如果用于客户机发送的请求消息时,通知代理服务器该如何处理缓存。如设置为no-cache,则代理服务器必须要去服务器验证资源,以确保发给客户端的文档时最新的。

    如果用于响应消息中,则通知客户机及代理服务器如何缓存当前的响应消息。如服务器有一些资源只有实时才有意义,如网页计数器、股票信息,这时应当在响应消息中使用no-cache,通知代理服务器和客户端不要缓存资源。

     

    Connection: close

    用于指定处理完本次请求和响应后,客户端与服务器是否还要继续保持连接。当请求消息中设置了Connection: close时,则通知服务器端在响应本次请求后,断开连接。当请求消息中设置了Connection: Keep-Alive时,则通知服务器端在响应本次请求后,不要断开连接。

    HTTP/1.1默认为Connection: Keep-Alive,所以当没有指定该域时,则默认为Connection: Keep-Alive

     

    Date: Tue, 11 Jul 2000 18:23:51 GMT

    用于表示HTTP消息产生的时间。必须为GMT格式。服务器返回的正常响应消息中总是包含Date头的。

     

    Progma: no-cache

    值只能为no-cache。在HTTP/1.0中,指示客户端不要缓存当前的响应消息。

     

    Trailer: Date

    用于指示在实体内容的后面可以出现哪些头字段。一般情况下会将消息头放在实体内容的前面,但也可以将消息头放在实体内容的后面。对于这些要放在后面的消息头,则使用Trailer来说明。上例表示:Date消息头将放在实体内容的后面传输。

     

    Transfer-Encoding: chunked

    如果HTTP消息的实体内容部分采用了某种传输编码方式,那么Transfer-Encoding消息头是用于指定传输编码方式的。目前的标准设置值只有chunkedChunked表示,要将整个HTTP响应消息的实体内容分成若干段以后再进行传输,并且在每个分段的开始部分都要使用一个16进制的数字来表示这个即将传输的这个分段的大小,最后一个分段的大小必须为0,这个0分段表示这个HTTP消息传送完毕。

    服务器端程序(ASPJSP)并不是将资源按一个字符一个字符的发送到客户端,而是先将资源内容写在缓冲区中,当缓冲区写满时,将内容发送给客户端。如果资源完毕,则也将缓冲区中的内容发送给客户端。当第一次缓冲时就将已将全部内容都写入到实体内容时,则服务器知道本消息的Content-Length,所以将会在消息头中指明Content-Length

     

    Upgrade: HTTP/2.0, SHTTP/1.3

    表示客户端支持并且希望切换到的协议。

     

    Via: HTTP/1.1 Proxy1, HTTP/1.1 Proxy2

    用于表明这个HTTP请求所途径的代理服务器的名称和所使用的协议。这个头的值由代理服务器进行追加。于是该头也记录了代理服务器的顺序。

     

    Warning: any text

    用于存储状态码所不能表明的一些信息。

     

    13. 请求头

    请求头用于客户端在请求消息中向服务器传递附加消息,主要包括:客户端可以接受的数据类型、压缩算法、语言、以及发出请求的超链接所属网页的URL地址等信息。

     

    Accept: text/html, image/*

    用于指出客户端程序能够处理的MIME类型。如:服务器可以输出png格式或gif格式等图片,但是某些浏览器不支持png格式的图片。所以服务器在响应时要检查Accept消息头,看浏览器是否支持png格式。

     

    Accept-Charset: ISO-8859-1, Unicode-1-1

    用于指出客户端可以显示的字符集。

     

    Accept-Encoding: gzip, compress

    用于指定客户机可以解码的编码方式,主要指压缩方式。

     

    Accept-Language: en-gb, zh-cn

    用于指定客户机期望服务器返回哪个国家的语言的文档。如设置Accept-Language: ja, zh-cn,则访问www.google.com则会打开日文的谷歌。

     

    Authorization: Basic enh4OjEyMzQ1Ng==

    当客户端访问受用户名和密码保护的服务器资源时,服务器就会向客户端发送401的响应状态码和一个WWW-Authenticate响应头,要求客户端使用Authorization请求头来进行应答。根据服务器发送的WWW-Authenticate响应头指定的验证方式的不同,客户端需要使用的Authorization请求头的值的格式也不同。有两种方式,一个是Basic,一个是Regist。使用Basic方式传递时,会将用户名和密码用“:”分隔形成一个串,然后进行Base-64编码。使用Base-64很容易就会被解码,所以相当于是用明文传送用户名和密码。

     

    Host: www.111.com:80

    用于指定客户端访问的资源所在的主机名和端口号

     

    If-Match: “xyzzy”, “r2d2xxx”

    浏览器可以缓存服务器响应的数据。当浏览器再次访问服务器的该资源时,只有当服务器的该资源已经更新,则服务器才将新内容传递给客户机,否则客户机要使用上次缓存的内容。

    可以定义各种条件来判断服务器端资源是否已经更新。如,服务器在响应中,可以发送一些代表实体内容的头字段 “xyzzy”, “r2d2xxx”,这些头字段被称为实体标签,当客户机再次向服务器请求这些内容时,就可以使用If-Match请求头传送以前缓存的实体标签内容。服务器在收到If-Match请求头后,则会比较这些实体标签内容是否与当前的页面的特征一致。如果相同,则说明资源没有更改,服务器不用将再次发送这些资源。

     

    If-Modified-Since: Tue. 11 Jul 2000 18:23:51 GMT

    当客户机访问一个已缓存的资源时,可以设置If-Modified-Since头,指定当服务器上的资源修改时间比这个头的值的时间要新,则服务器才返回新的资源。该值必须为GMT格式,一般情况这个头的值是使用上次访问该资源时响应消息中的LastModified头的值。

     

    If-None-Match: “xyzzy”, “r2d2xxx”

    If-Match相反。

     

    If-Range: Tue. 11 Jul 2000 18:23:51 GMT

    结合Range头使用。可以设为实体标签,也可以是时间值。当在If-Range值之前服务器资源没有改变,则根据Range头的值进行续传。否则服务器返回整个文档内容。

     

    If-Unmodified-Since: Tue. 11 Jul 2000 18:23:51 GMT

    If-Modified-Since相反。

     

    Max-Forwards: 1

    指定了当前HTTP请求可以途径的代理服务器个数。每经过一个代理服务器,这个值就会被减1。如果减到0,则代理服务器中止继续发送。

     

    Proxy-Authorization: Basic enh4OjEyMzQ1Ng==

    Authorization类似,是与代理服务器的验证时使用。

     

    Range: bytes=100-599

    客户端通知服务器只需返回资源的部分内容,以及部分内容的范围。

    这对于较大文档的断点续传是有很大帮助的。如果客户机在一次请求中,只收到了服务器返回的部分内容,即服务器作出的响应只有一部分到达了客户端,则客户端可以发出一个带Range头的请求,这时服务器将会返回Range头值的那部分内容。

    Range头有三种格式:

    bytes=100-599,返回第100到第599个字节之间的内容(初始为0,包括100,599)

    bytes=100-,返回第100个字节以后的所有的内容。

    bytes=-100,返回整个文档中的最后100个字节的内容。

     

    Referer: http://www.google.cn

    告诉服务器,这次请求是通过点击哪个网页上的超链接和转向过来的。由于可以使用telnet来仿造HTTP请求,所以Referer是不可靠的。由于HTTP的作者的拼写错误,所以不可以写成正确的拼写方式Referrer

     

    TE: trailers, deflate

    用于说明客户机可接受的除了chunked以外的传输编码类型。或者当使用chunked时,是否可以使用trailers头字段在每一次发送的实体片段内容之后来设置一些响应头。

     

    User-Agent

    用于指定浏览器的类型和名字。如:服务器看到使用PDA版的IE,则可以返回wml的页面。

     

    14. 实体头

    实体头用作实体内容的元信息,描述了实体内容的属性,包括实体内容类型、长度、压缩方法、最后一次修改时间、数据有效期等。

     

    Allow: GET, POST

    可以使用哪些方式访问资源

     

    Content-Encoding: gzip

    实体内容以哪种方式编码。

     

    Content-Language: zh-cn

    实体内容的国家语言类型。

     

    Content-Length: 80

    实体内容的大小。

     

    Content-Location: http://www.111.org/index.html

    服务器可以说明返回响应实体内容的真正的实际位置。

     

    Content-MD5: CVBNMDYHJFK==

    实体内容的MD5摘要算法Base-64值,以提供实体内容的完整性校验。服务器可以通过对实体内容进行MD5摘要算法与此头的值是否相同来确定接收的请求是否没有错误与改变。

     

    Content-Range: bytes 2543-4532/7898

    实体内容的部分的所在位置。表明本次响应是实体内容的第2543字节到第4532字节,7898是实体内容的总大小。

     

    Content-Type: text/html, charset=GB2312

    由于网络上传送的均为二进制流,所以浏览器不知道传送的资源时什么类型的,所以服务器要告诉浏览器本响应的资源时什么类型的。计算机中有多种的数据格式,人们为每一个格式都定义了一个名称,称作MIME。本头指出实体内容的MIME。由于WEB服务器不知道这些资源文件是哪种MIME,所以可以对WEB服务器进行设置,使文件扩展名与MIME之间进行映射。

     

    Expires: Tue. 11 Jul 2000 18:23:51 GMT

    当前文档在何时之后被认为过期。浏览器在这个时间之后再访问这个页面时,将不再使用缓存中的内容。而是在需要时发出新的访问请求。

     

    Last-Modified: Tue. 11 Jul 2000 18:23:51 GMT

    指定文档的最后更新时间。

     

    15. 扩展头

    HTTP消息中,也可以使用一些在HTTP/1.1正式规范里没有定义的头字段,这些头字段统称为自定义HTTP头或扩展头,他们通常被当作是一种实体头处理。

    现在流行的浏览器基本都支持CookieSet-CookieRefreshContent-Disposition等几个常用的扩展头字段。

     

    Refresh: 1

    Refresh: 1;url=http://www.111.com

    1秒之后刷新或跳转页面。

     

    Content-Type: application/octet-stream

    Content-Disposition: attachment; filename=aaa.zip

    Content-Disposition只有一个值,为attachment,在后面可以增加filename=aaa.zip,表示被保存的初始参考文件名。

  • 【转】一个程序员的奋斗历程

    2010-03-02 21:04:47

    这些日子我一直在写一个实时操作系统内核,已有小成了,等写完我会全部公开,希望能够为国内IT的发展尽自己一份微薄的力量。最近看到很多学生朋友和我当年一样没有方向,所以把我的经历写出来与大家共勉,希望能给刚如行的朋友们一点点帮助。一转眼我在IT行业学习工作已经七年多了,这期间我做过网页,写过MIS、数据库,应用程序,做过通信软件、硬件驱动、协议栈,到现在做操作系统内核和IC相关开发,这中间走了很多弯路,也吃了不少苦。

        我上的是一个三流的高校,就连同一个城市的人多数都不知道。因为学校不好也就没有指望能靠学校名气找一个好工作。所有的希望都寄托在自己的努力上了,大一开学前的假期我就开始了学习,记得我买的第一本书是《计算机基础DOS3.0》,大家别吓着了,其实当时已经普及了DOS6.22了,只是我在书店里看到了DOS4.0,5.0,6.0的书,以为像英语那样是第四、五、六册,记得当时到处找DOS1.0,现在想想也幸好我没有找到:)开学前我学完了PASCAL,那时既没有计算机也没有人可以请教,我连程序是什么的概念都没有,只好死记硬背代码,然后拿纸写,我一直到大三才有了一台486,在这之前用纸写了多少程序我也记不清楚了,只知道最长的一个我拿A4大小的草稿纸写了30多页,我的C语言、C++ 、VC都是在这样的条件下入门的。所以说条件是可以克服的,希望我的经历多少给条件艰苦的同学们一点信心。第一次上机是在我姐夫的机房,我的心情激动的无与伦比,但是一上机我立刻傻了眼,他们用的是英文版的Win3.1,我的那点DOS知识都见了鬼,上机提心吊胆的一阵瞎摸,一不小心把Word弄成了全屏,怎么都还不了原,当时真是心急如焚,我以为机器被我弄坏了。第一个C语言程序,就是那个经典的HelloWorld,我调了几个星期,上机机会非常少,也没有书告诉我开发环境(TC2.0)需要设置,而且开始我都不知道有编译器,我甚至自作聪明把写好的程序扩展名从.c改成.exe,结果可想而知。大一学完了C、X86的汇编、数据结构、C++。由于精力都花在自学上了,大一下四门课挂了彩,三类学校就是这点好,挂上一二十门也照样毕业。不过扯远点说,我那么刻苦都及不了格,可见我们国家的计算机教育有多死板。

    大二准备学VC和BC,当时难以取舍,后来选了VC,不为别的,只为书店里两本书,VC 那本便宜6块钱。我的努力在班上无人能及,学的日夜不分,大三有了计算机后更是如此,很多次父亲半夜教训我说我不要命了,我一直觉得自己基础差,记忆又不行,条件也不好,所以觉得只有多花点时间才能赶上别人。居然后来有许多朋友说我有学计算机的天赋,让我哭笑不得。我用的是486,16M内存,1G硬盘,当时同学们的配置都是P166MMX,我安装一个Windows NT4.0需要一个通宵,编译一个BC5.0向导生成的程序需要近两个小时,我的显示器是个二手的,辐射非常大,开机屏幕冒火花,看起来很酷的:),有一次程序写的太久,觉得怎么白色的编辑器背景变成了紫色,以为显示器坏了,后来才发现眼睛不行了,不过说来也奇怪,到今天我的视力还能保持1.5,真是个奇迹。但是就是那台破机器陪伴了我两年,让我学会了VC、Delphi、SQLServer等。后来那台机器给我阿姨打字用,据她说一天她正打的开心,一股青烟夹着火苗从显示器钻出来,之后它才寿终正寝。

    大三假期找了个机会在一个计算机研究所实习,与其说实习不如说是做义工,工作了两个月一分钱没有拿。但是这两个月对我的发展帮助很大,让我早一步了解了社会,刚去的时候我当然是一窍不通,在那里我熟悉了网络,学会了Delphi和Oracle。由于工作很认真,得到了比较好的评价,在一位长者的引荐下,我开始和他们一起做项目,这使我在大三大四就有了自己的收入,大四又找了两家MIS公司兼职,虽然钱不多,但是在学生期间有1000多的收入我已经非常满足了,我终于用自己赚的钱把计算机换了。大四下开始找工作,这时我的工作经验已经比较多(当然现在想想非常幼稚),开始听父母的想去那个研究所,实习过那个部门也希望我能去,但是不知道为什么最后不了了之,这种单位就是比较官僚,我一气之下就到了我兼职的一个公司做MIS的TeamLeader。在大三到毕业一年的时间,做过了各种MIS,从煤气、烟厂、公安、铁路、饮食到高校,什么有钱做什么,工作也很辛苦,经常加班和熬通宵,从跟客户谈需求到设计、编码、测试、交付都要上。那时觉得很有成就感,觉得自己还不错,现在想想真是很肤浅。

    刚走上工作岗位的学生很容易被误导,各种开发工具让人眼花缭乱,同时也觉得很受公司器重,但这样工作永远是一个低层次的开发者。不要跟我说什么系统分析有多么多么重要,多么多么难。你以为自己跟用户谈需求做设计就是系统分析和设计了吗,国内又有几个公司能够做的很到位很规范?我是ISO9000内审员,也在Rational公司受过多次培训,拿了4个证书,还有一个公司让我去做CMM。这些我听过很多,但是很多事情到国内就变了性质,一个公司不是通过了ISO9000或者CMM就能规范了,我现在在一家有几十年历史的外企工作,里面的管理不是一般国内企业能及的。作为一个毕业不久以前没有步入过社会的学生,几乎不可能在很短的时间掌握系统分析和设计,面向对象、UML只是一个工具,关键是人本身的思想,不是说你熟悉了C++、Rose就能够做出好的设计,相反如果你具备了很高的素质,你可以用C写出比别人用C++更加模块化的程序。

    话说远一些,国内软件开发行业有一个怪圈,很多人觉得VC > Delphi > VB,真是很搞笑。这几个软件我都做过开发,说白了他们都是工具,应该根据应用的需要选择采用哪个,而不是觉得哪个上层次。如果你因为用某个开发工具很有面子而选择的话,只能说明你很浅薄。如果说层次,那么这些工具都不上层次,因为它们用来用去都是一些系统的API,微软的朋友不会因为你记住他们多少个API或者多少个类就会觉得你很了不起,你永远只是他们的客户,他们看重的是你口袋里的银子。我也做过系统内核,我也封装过很多API,同样我也不会看重那些使用这些API做二次开发的客户,除非他能够作出自己独到的设计。

    至于有人认为C++ > C那更是让人笑掉大牙,不妨你去打听一下,现在有几个操作系统内核是用C++写的,又有几个实时系统用的是C++,当然我也不是说C++不好,但是目前的内核和实时系统中C++还无法与C匹敌,至于说C++适合做应用系统的开发那是另外一回事。所以我的观点是不在于你用什么工具和语言,而在于你干什么工作。你的设计体现了你的技术层次。

    这样干了一年我觉得非常苦闷,做的大多数都是熟练工种的活,个人技术上没有太多的提高也看不到方向。所以决定离开这个城市去上海,寻求更好的发展,并且打算放弃我以前的MIS转到通信行业。

    写到这里不能不提到我女朋友,我们是在来上海前半年认识的,她大四在我公司实习,公司派她给我写文档,我们的感情发展的很快。她告诉我很多事情,她家原本是改革开放的第一批暴发户,她母亲爱打牌,输掉了几百万,还欠了很多债,她有男朋友,但是她对他没有感情,只因为他给了她母亲两万多块钱,后来还强迫她写了四万块的借条,她男朋友背叛过她并且不止一次打她,现在逼她结婚不然就要她还钱。这人居然还是一个高校的老师!她母亲把父亲给她的学费花了,因为拖欠学费她没有办法拿到毕业证。她母亲现在有病需要钱,我拿出了自己的一点积蓄并且跟朋友们接了一些,替她交了学费并给她母亲看病(后来才知道看病的钱又不知所终,就连她母亲是不是有病我都不知道,但她也是没有办法)。这个时候我家知道了一些事情,坚决反对我和她在一起,她原来的男朋友也极力破坏。无奈之下我们决定早一定离开这个伤心的城市,并且瞒着我们家。由于时间仓促,我只准备了4000块钱,她仅有的几百块钱也被她母亲要去了,我买了三张票,一张是中午的,两张是晚上的,中午我的家人把我送上船,他们一离开我就下了船,我和她乘坐晚上的船离开了这个我和她生活了很多年的城市,带走的只是一身债务。没有来过上海的我们两个性倔强,都不愿意去麻烦同学和朋友。来到上海是傍晚6点半,我们都不知道该去哪里,我们找了一个20块钱的旅馆,这个房间连窗户都没有,7月份的天气酷热难耐,房间里非常闷热。第二天我们开始租房子,因为身上的钱不多,我们基本都是步行,花了一个星期时间,不知道在浦东转了多少圈后找到了一个400块的房子,但是我们都不了解上海是付三压一,还要付半个月的中介费,买了一些锅碗瓢盆后,我们身上只有800块钱了,工作都还没有着落,这800块钱要支持到我们拿到第一个月工资,为了省钱我们自己做饭,每天买菜只花两块钱,她非常喜欢吃(也可能她在大学经常挨饿的愿意),看到她现在这样省吃俭用我真的很不忍心。她以前的男朋友也没有放过她,经常打电话来骚扰,并且来上海看她,还说了不少恐吓她的话,她过于善良,说他以前毕竟帮助过她,叫我不要与他一般见识。以后的每天在家就是苦等面试通知,原本我想迅速找一家MIS公司解决眼前的困难,但是她坚持让我不要放弃自己的理想,终于功夫不负有心人,我找到了一家通信公司,4000块的工资虽然赶不上MIS公司给我开出的价位,但也够在上海生存。她也找到了工作,第一天上班她哭了,这是她来上海第一次流泪,我心里很难受也很感动。
    由于是全新的行业,我把自己降到了零点,我学的VC、Delphi、数据库派不上用场,摆在我面前的是嵌入式、协议、信令一些我从未接触过的知识。我知道我没有退路,于是拼命的学习,我把自己当做一个应届毕业生一样,一分努力一分收获,半年过去我终于熟悉了工作,并且得到了公司的表彰,薪水也加了一级。后面的日子里我们省吃俭用,把欠朋友的1万多块钱还了,日子终于上了正轨。这时女朋友告诉我她想考研究生,我也很支持,于是她辞职在家备考。

    另外,在这里我要感谢我的ProjectManager,他原来是一个大通信公司的产品经理,对人非常和善,我从他那里学到了很多知识,而且他也给了我许许多多无私的帮助。在工作上他给我充分的空间和信任。记得公司安排我维护一个接入服务器软件,由于代码量不算太小(5万行),资料和文档都不齐全,我维护起来非常吃力,所以想重新把它做一遍,公司领导不太支持,可能觉得工作量太大,但是他极力支持我,私下里他让我放手去做,我的维护工作他挤时间做。在他的支持下,我花了半年时间完成了接入服务器的软件,并且实现了一个相对完整的TCP/IP协议栈。在这里我学会了嵌入式系统设计、驱动开发、TCP/IP和很多通信的知识,我花了一年时间终于使自己从MIS开发转到了通信行业,并且站稳了脚跟。我的开发大量是对硬件的直接操作,不再受微软的操作系统,VC、Delhpi这些开发工具的约束,我终于看到了另外一片天空。

    我做事情喜欢追根问底,随着开发的深入,软件开发与硬件联系越来越紧密,硬件知识的匮乏又对我的发展产生了障碍,而且芯片技术基本上掌握在国外公司的手里,这对做系统级设计是一个非常大的制约,一个新产品出来,第一道利润(也往往是最丰厚的利润)常常都被IC公司如Intel、Motorola赚去了,国内的厂商只能喝点汤。所以我决心解决自己的硬件技术障碍,并打算离开通信行业,进入IC设计相关领域。

    当然我明白如果我对硬件了解的非常少,没有哪家IC公司会仁慈到招我这样一个一窍不通的人来培训。所以我必须努力打好基础,学一些相关知识为以后做准备。就像我开始从MIS转到通信一样,我看过大量通信方面的书,并且给一个ISP做过RADIUS计费分拣台,在这样的背景下这家通信公司才给了我这个机会。我在的通信公司是做系统设计的,有不少PCB Layout硬件人员,平常我就注意向他们学习,由于我做的是软件,在公司看硬件资料不好意思,所以开始只好在家看,刚来上海工作我连续一年都在加班,后来不加了,因为我要挤出时间学习,通常我12点左右睡,第二天5点半起,我上班比较早,地铁上如果人不多我也用来看书。学习当然不会是一帆风顺的,有些实在不懂的问题就积累起来问硬件人员,他们的帮助使我学习进度快了很多,因为在没有人点拨的情况下自学,我的一半时间是花在解决疑难问题上,但这种问题经常是别人的一句话就可以让我豁然开朗,我非常庆幸我有这样的学习环境。在后面的一年里,我学会了看硬件原理图,学会了简单的硬件设计(模拟电路方面还有不小的差距),事情就是这样的,当你安安份份做软件,别人永远认为你是软件开发人员,在你开始学习硬件时别人未必会认同,有位中兴通讯的朋友还对我说过,一个人不可能把所有东西都学完。我也明白这一点,但我希望自己做的更好。但当你熟悉硬件后大家又会觉得你好像原本就是软硬件都懂的,同事们也都习以为常了。这个时候我可以把硬件资料堂堂正正的拿到公司看,没有人再大惊小怪了。让我比较自豪的是我通过自己的努力做了一个IAD(软交换的终端设备)系统方案,包含软硬件的选型、设计等内容,这个方案得到了公司和同事们的认同,让我感到非常欣慰。

    技术是相辅相成的,当我的硬件有了一定的进步后,我的软件设计也有了很大的提高,我可以从更深层次理解问题,我做的接入服务器CPU是Motorola PowerPC860,熟悉的朋友都知道860 QMC与软件的批量数据传输通常采用BD表的方式,硬件人员做驱动的时候习惯采用固定BD表,每接收或发送数据都将数据从BD表拷贝到用户Buffer,或从用户Buffer拷贝到BD表,由于理解的比较深入,我自己重新实现了这个过程,采用动态BD表的方式,驱动从一个网口接收数据,提交给我的软件进行三层交换,直至从另外的接口发送出去,没有进行一次拷贝。这样的设计大大提高了性能,使系统的指标接近理论值。软硬件的结合使我的设计水平上了一个台阶。我现在写的这个操作系统,编译后我把程序反编译成汇编,找出其中不优化的代码,然后在C程序中进行调整。举个例子,很多CPU没有专门的乘法指令,这个大家应该都知道,在这种CPU上进行一个乘法操作常常会花费大量的指令周期,有的朋友会说这个我知道,我会尽量避免采用×号,但是事情往往不是那么简单,你知道C语言中数组的下标操作是怎么实现的吗?仔细看看反汇编的代码你就会明白,同样是通过下标的定位操作,C编译器会有时候会产生位移指令,但有时候会用乘法实现,两者效率往往是天壤之别,所以明白这些问题你才能将系统性能提升到极致。一些问题就不多说了,有兴趣的话以后可以共同探讨。

    话说远一点,我由衷的希望在软件上做的比较深入的朋友们有机会学学硬件以及其它相关知识,尤其是做底层开发和嵌入式设计的。这对软件技术的提高有非常大的帮助,否则很多事情你只知道该这样但不会明白为什么该这样。我这个观点在我现在的IC公司Project Manager那里也得到了验证。他告诉我们公司现在的802.11芯片产品的软件经理原本是做该芯片硬件设计的,某某某原本是做软件的,现在在做IC,类似的例子还有很多,只是在国内这样的风气不是非常流行。

    我有一些心得体会与大家分享,只有当我干好本职工作后,我才会学习与工作关系不大的技术,这样公司的上司才不至于反感,在入门阶段的问题我通常不去问那些资深人士,而是问一些资历比较浅的朋友,比如刚毕业不久的学生,因为他们往往会跟你详细的讲解,而资深人士通常觉得你的问题太简单,所以回答的也很简单,我又不好意思多问。等技术上了一定的层次后我才会问他们,他们也能给你比较深入的回答。另外,有些朋友说我机会比较好,他们也希望能从事新的工作可惜没有机会,我听了只有苦笑,我的机会了解的人都应该知道,我没有出生在什么IT世家:)也没有谁一路提拔我,所有的路都是自己走出来的,我母亲去世比较早,我的后母(我叫她阿姨)看着我努力过来的,一次她看我大年30还在写程序,她说像我这样努力木头都能学出来。

    我的最终目的是IC而不是PCB,所以我下一步的准备开始学习IC设计的知识。公司的同事没有懂IC设计的,后面的路又要靠自己了,我买了不少相关的书,在网上也查了很多的资料,我花了大量的时间去学习VHDL,并且用软件进行了一些简单的设计和仿真(没有设计ASIC,只是针对FPGA),随着学习的深入,我渐渐明白了IC设计的基本流程,同时也明白了这条路的艰辛。这个时候我已经做好了跳槽的准备,我向一家业界又一定知名度的IC设计公司投了简历,并通过了漫长的面试(4个多小时)。其他的一切我都比较满意,唯独薪资差强人意,我也明白原因,因为我是这个行业的新人,我没有经验,我再一次将自己清零了。公司老板问我6000多一个月能不能接受,我知道他也是照章办事。想想我通信行业的朋友们,基本上都是年薪10万以上,月薪过万的也比比皆是,朋友们也帮我介绍了不少待遇不错的公司,我该怎么选择,当时我很犹豫,我热爱我的事业,我向往我的追求,但我也是一个普通的人,我也需要养家糊口,我也想早一点买房买车。生活给我出了一道难题。

    爱因斯坦在63岁时说过“一个人没有在30岁以前达成科学上的最大成就,那他永远都不会有。”这句话给了我很大的压力和震动,我马上就26岁了,离30只有四年时间,我必须抓紧这几年宝贵的时间,努力达到我技术上的最高峰。为了这个理想,为了能离自己的梦更近一些,我选择了这家IC公司,我明白自己的薪资和公司刚进来的硕士研究生相差无几,但为了今后的发展只能忍受,一切又得重新开始。换行业是一个非常痛苦的过程,尤其从一个春风得意的位置换到一个陌生的岗位,感觉像从温暖的被子里钻出来跳进冰水中,让人难以接受。在原来那家通信公司,我是唯一两年时间涨了五次工资的员工,公司和同事都给了我极大的认可,工作上也常常被委以重任。但现在这一切都成了过去,在新的公司我只是一个新人,没有人知道也没有人在意我过去的成绩。我决定重新开始,我把自己看作新毕业的学生,我要用自己的努力得到公司的认可。进入新的行业是非常痛苦的,我告诉自己必须忍受这一切,虽然外面有很多诱惑,但是既然作出了选择我就不允许自己轻易放弃。
    我现在已经在这家新公司上了一个多月的班,开始非常艰难,现在慢慢适应了。第一 个月结束时,Team Leader找我谈话,说我是新进员工中最优秀的一个,我心里很欣慰,这也算对我努力的一个肯定吧。在这里还要感谢我的女朋友,她给了我很大的支持和鼓舞,每次在我动摇的时候她都在鼓励我,让我坚持自己的理想,刚来上海是她让我不要勉强去做MIS,这次也是她让我顶住了月薪过万的诱惑,没有她我可能不会有今天的成绩。现在的公司有自己的操作系统,自己的CPU、DSP和其它芯片,在这里我能学到世界上最先进的技术,我们的设计开发不再完全依赖别人的硬件和系统,这让我很开心。我打算等工作步入正轨后,全力学习新的知识,实现我的理想。

    在后面的两年里我给自己定下了几个目标:

    一.努力做好本职工作,在工作上得到公司和同事们的认同;

    二.努力学习IC硬件设计知识,多向同事请教,并利用一切机会多实践;

    三.实现我的实时操作系统的主要部分,完成TCP/IP协议栈模块,并免费发布源代码;

    四.和我女朋友结婚并买一套小房子,这是最重要的,因为我明白事业是可以重来的,但是珍贵的感情很难失而复得。

    在这里提一下我现在开发的操作系统,它是一个实时嵌入式系统,目前支持以下特性:

    a.支持时间片轮转调度和基于优先级调度,最多64个优先级;

    b.抢占式实时内核;

    c.为了便于移植,主体用标准C实现;

    d.汇编代码非常少,不到100行;

    e.支持任务管理,各任务有独立的堆栈;

    f. 进程同步和通信目前完成了Semaphore,Message Queue正在调试;

    g.实现了定时系统调用;

    h.可以在windows上仿真调试

    我还打算下一步实现优先级反转保护,Event Flag,Data Pipe,内存管理(以前实现过)、驱动接口等。在这之后我还会努力完善它,比如加入文件系统,协议栈、调试接口等。希望朋友们提出自己的意见和建议,在此不胜感激!


    后记:

    就像有的朋友说的,我的经历或许会给一些朋友产生误导,在这里我必须说明一下。我来上海以前学习过于拼命,常常晚上只睡3个多小时,我身高1米71,那时只有108斤(我现在130多),家人也说我这样拼命活不过60岁,但是当时的我太固执,我对他们说只要能实现理想活50岁我就够了。那时的拼命使我的身体受到了影响,有一次早上突然腰肌剧痛难忍,痛的我倒在床上站不起来。虽然我现在已经比较注意,但有时候还会隐隐作痛。后来在女朋友说服了我,来上海以后我不再如此。我经常引用父亲的一句话“身体是革命的本钱”。

    而且我也发现拼命不是办法,我可以熬一两个通宵,最多的一次我连续工作了三天三夜,但是我半个月都没有恢复过来,这样是不是得不偿失?学习工作应该是一个长期的过程,像马拉松而不是百米冲刺。我现在非常注意调整学习和工作的强度,我要保证每天尽量有相对充沛的精力,一些年轻的朋友觉得自己也应该拼命努力,这让我多少有些担心,如果我的故事能让你在学习工作上多一点兴趣,我会感到很开心,但如果误导了某些朋友,让你做一些不值得的付出,我会感到很内疚。

    技术没有贵贱之分,我以前换行业是因为自己的兴趣所致,而不是对哪个行业有什么偏见。我希望我的经历不要给朋友一个错误的导向,觉得我始终向更高的技术发展。其实各行各业做到顶尖都是很困难的。话又说回来虽然技术没有贵贱,但是门槛是有高低的,无论如何,做IC的门槛要比做网页的高,这一点无可否认。国家各种人才都是需要的,但是作为个人奋发向上的想法还是应该有的,努力在自己喜欢的行业上做的更好,而不应该停留在比较肤浅的层次上。
        我是一个自己觉得比较有自知之明的人,或许我最大的优点就是知道自己有很多缺点:)。我的故事中很多的曲折和错误都是由我的缺点造成的,希望大家用审慎的眼光看待我的经历,不要被我的“花言巧语”所迷惑。我学习有些随心所欲,这给我带来了无尽的麻烦,也大大阻碍的我的发展。记得我小时候成绩比较出色,但是后来学习严重偏科,导致我中学成绩一再滑坡,也没有考上什么好的学校,小时候的一个朋友,当时的成绩和我相仿,但是没有我这个缺点,她上了清华,后来在去了美国深造,在一个著名导师手下研究理论科学,这未尝不是一条更好的出路。另外我的学习方法也是在不断改善中的,过去的学习过于讲究数量和时间,那样学习既苦而已效率不高,现在我非常注意学习的效率和技巧,这样才是学习的捷径(当然不是指投机取巧),比如说学一相对陌生的技术,如果有条件,不妨问一问有经验的人,不需要问很多,往往他不经意的几句话会给你非常大的帮助,甚至超过你看一个星期的书。带着这样的思想再去学习你会节省很多时间,这样何乐不为呢?这些年中我学了不少的东西,由于开始非常盲目,所以学的东西杂乱无章,现在回想起来让我啼笑皆非,我把大量的时间浪费在一些没有必要深入了解的知识上,毕竟一个人的精力是有限度的。很多朋友很我一样都背过五笔字形,的确它是个不错的输入法,但是对一个研发人员它绝对不值得你去背,你的时间应该花在有价值的地方。我这样的事情还做过很多,我背过CCED、WPS的命令和快捷键,在dBase基本退出历史舞台后我还花了很多时间去学习它的使用。所以我的学习在前期缺乏规划,没有明确的短期目的、中期目标,只有一个虚无飘渺的长期的理想。这就像做设计一样,好的设计是从需求抽象到代码有很多过程,而不能得到了需求就立刻开始编码。

    一个程序员的奋斗历程(续)
    前段时间处理了很多事情,一直没有写下去,花光了所有的积蓄买了一套房子,同时把户口的事情也基本办完了,这几天稍微缓口气。昨天跟我的一个老上司见面聊了半天,心里感慨万千。他从外在条件看让不少外人羡慕,二十多岁做过到了863项目的负责人,博士毕业的爱人单位也非常好。现在三十出头的他在一个通信公司做产品经理,工资虽然不算高但也有一两万,而且还持有股份。但是我们了解的人才理解他的艰辛。“白领”这个词在一些人看来是仿佛是一个动人的光环,但是在我看来是一个无奈的名字,每天行走在大街上,来来往往的车流中有多少是“白领”的?又有几个“白领”住的起高档的住宅?在上海一套别墅300万不足为奇,按揭贷款下来总额接近600万,年薪二十万在上海算是一个中高级“白领”,高额的税金去掉了你百分之几十的收入后,这样算下来不吃不喝也要四十多年,加上生活的其他开支,注定了你与这样的住宅无缘。看着外面一套套别墅,一辆辆好车,我不知道它们是谁的,但我知道其中没有什么白领。我觉得自己很渺小,在这个喧闹的都市中我如同一只蚂蚁,但我有不甘于平凡,我不愿做一个单纯的“白领”。

      其实很多朋友并不了解我,我不是一个追逐时尚技术的人,我只是不愿意做一个所谓的“白领”,更加不愿意做一个单纯的“程序员”。我不甘愿平凡的生活一辈子。我在不断的努力,我的方向非常明确,我要做多数人不做和做不到的事情,很多朋友对我这样频繁的换方向不理解,觉得一个人只要熟悉一种技术就可以了,对于这样的看法我只能说你浅薄,现在的大的系统和产品往往都是软件、硬件和应用相结合的,我要做的不是哪个方面的专家,而是希望能够成为系统设计师。我不相信一个只精通发动机的专家能够设计一辆好车,同样我也不相信对硬件一窍不通的人能做出一个操作系统,或者一个对财会没有一点概念的人能设计出一个优秀的财务软件。在工作中我发现社会上非常缺乏边缘人才,尤其是在国内。在国外一个人软硬件兼修非常普遍。如果设计产品的人只了解他的专业那么是很难有出色的设计。所以我必须趁着自己年轻学的更加广泛一些,这样才能提高自己的综合素质,这也是为什么高校那么多非专业课程。学习工作了这些年,实际上都没有脱离IT这个行业,我现在的公司开发一个系统时,先是将最终功能列举清楚并分析可行性,然后划分哪些是用芯片实现,哪些是用硬件电路实现,哪些是用软件实现,这样的设计才能做出最好的系统。如果一个设计者单纯只懂一个方面是不可能做到这一点的。

      自负常常伴随着无知,记得我大学毕业时,论文答辩会上我和专家组组长争起来了,因为我对自己的设计非常得意,而他虽然是鸡蛋里挑骨头,但是由于知识非常有限,我无法回答他的问题,所以有些“恼羞成怒”。我原来一直喜欢用“所谓”最好的开发工具,记得做过一个愚蠢的设计,一个排课表的软件我用VC+Oracle开发。这些经历我牢记在心,时刻提醒自己学会谦虚。我的亲身经历加上我对一些身边朋友的观察发现这样一个现象。当一个人只会他认为最好的技术,而对其他的一无所知,这样的人经常是目空一切。
    从第一个“Hello World”到今天的操作系统,前前后后写了很多代码,从这中间我也积累了很多心得。由于我是在没有人指导的情况下自学编码的,所以走了很多弯路,也犯了不少错误。最初我写程序全凭自己的感觉,写一个新程序对结构设计不很重视,以为学好语言,数据结构就可以写出好的程序,其实远不是这样的。没有设计的情况下,也可以写,但是程序无法写的很大、很复杂。我个人的经验是这样的系统超过8000行我就无法控制了,以前我用VC写过一个Windows下的应用程序,大概8000行左右我对它失去了控制,整个代码一团糟,这8000行倒是可以相对稳定的运行,但是我没有能力再增加什么新的代码,动辄前后冲突,要么就是新代码与旧设计格格不入,需要调整旧的程序。最开始我写程序喜欢追求代码的精巧,别人很多行写出来的代码自己只写很少就可以实现,感觉那样比较酷。其实这样也是非常错误的,我现在写程序非常注重结构设计,为了结构清晰我愿意牺牲一点效率。

      下面一段话是我写程序的座右铭,希望与大家共勉:

      Make it right before you make it faster.
      Keep it right when you make it faster.
      Make it clear before you make it faster.
      Do not sacrifice clarity for small gains in efficiency.
      Brian Kernighan

      另外补充一点:我和我的女朋友现在非常好,双方的家人都认可了,我们决定在近期结婚。

  • c#编程第十四章 堆栈

    2010-03-02 20:58:03

    第十四章 堆栈

    一般来说堆栈指的是,临时寄存货物的地方,比如你正在读书,同房间的人请求你马上将垃圾袋拿出去,你放下手中的书并小心的做好标记,当你手提垃圾袋走到门边时,突然电话铃声响了,你放下垃圾袋,并留意一下垃圾袋的位置就去接电话,接完电话,回到放垃圾袋的地方然后把它拿出去,当你返回并从新坐下后,将书翻到原先读到的地方,又继续读起来。

    计算机程序有时也需要停止当前所做的事情去处理其他任务,事实上函数调用就是告诉程序停止当前执行的函数,转向一个新的函数,正像你被打断前需要记住正在做的事情一样,程序在转向下一个函数之前,也需记录当前函数停在了什么地方以及其中各个变量的值,用来保存这些临时信息的数据结构在编程语言中叫堆栈。

    堆栈是一个按后进先出(LIFO)方式工作的数据列表,当中断任务完成后,最后放进堆栈的数据一定是最先被弹出,就像盘中的一堆菜,最后放在上面的菜是最先要吃到的菜。堆栈通常是使用数组或链接表来建立的。

    在命名空间System.Collections中,有专门用于堆栈的非泛型类Stack,另外在System.Collections.Generic命名空间中还有泛型类Stack。下面我们学习非泛型类Stack。

    ==========================
    一 Stack 构造函数
    ==========================

    语法1:
    public Stack ()
     
    说明:初始化 Stack 类的新实例,该实例为空并且具有默认初始容量。

    语法2:

    public Stack (int capacity)

    参数
    capacity:Stack 可包含的初始元素数。

    ==========================
    二 常用方法
    ==========================

    ★  Stack.Push  

    语法

    public virtual void Push (Object obj)
     
    参数
    obj:要推入到 Stack 中的 Object。该值可以为空引用。

    说明:将对象插入 Stack 的顶部。

    示例1

    using System;
    using System.Collections;
    public class SamplesStack
    {
        public static void Main()
        {
            Stack myStack = new Stack();
            myStack.Push("1");
            myStack.Push("2");
            myStack.Push("3");
            foreach (Object i in myStack)
            {
                Console.WriteLine(" {0}", i);
            }
        
    }

    ★ Stack.Pop

    语法

    public virtual Object Pop ()
     
    返回值:返回从 Stack 的顶部移除的 Object。

    示例2

    using System;
    using System.Collections;
    public class SamplesStack
    {
        public static void Main()
        {
            Stack myStack = new Stack();
            myStack.Push("1");
            myStack.Push("2");
            myStack.Push("3");
            foreach (Object i in myStack)
            {
                Console.WriteLine(" {0}", i);
            }
            //移除顶部的一个元素并返回它
            Console.WriteLine("(Pop)\t\t{0}", myStack.Pop());
            //验证移除
            foreach (Object i in myStack)
            {
                Console.WriteLine(" {0}", i);
            }
        

    }

    ★ Stack.Peek

    语法

    public virtual Object Peek ()

    返回值:位于 Stack 顶部的 Object但不将其移除。

    示例3

    using System;
    using System.Collections;
    public class SamplesStack
    {
        public static void Main()
        {
            Stack myStack = new Stack();
            myStack.Push("1");
            myStack.Push("2");
            myStack.Push("3");
            foreach (Object i in myStack)
            {
                Console.WriteLine(" {0}", i);
            }
            //返回顶部的一个元素但不将其移除
            Console.WriteLine("(Peek)\t\t{0}", myStack.Peek());
            //验证
            foreach (Object i in myStack)
            {
                Console.WriteLine(" {0}", i);
            }
        

    }

    ★ Stack.Contains

    语法

    public virtual bool Contains (Object obj)

    参数
    obj
    要在 Stack 中查找的 Object。该值可以为空引用。

    返回值
    如果在 Stack 中找到 obj,则为 true;否则为 false。
     
    说明

    此方法确定某元素是否在 Stack 中。

    示例4

    using System;
    using System.Collections;
    public class SamplesStack
    {
        public static void Main()
        {
            Stack myStack = new Stack();
            myStack.Push("a");
            myStack.Push("b");
            myStack.Push("c");
            foreach (Object i in myStack)
            {
                Console.WriteLine(" {0}", i);
            }
            Console.WriteLine("Contains返回值:{0}", myStack.Contains("a"));
        }

    }

    ★ Object.Equals

    语法1
     
    public virtual bool Equals (Object obj)

    语法2

    public static bool Equals (Object objA,Object objB)

    说明:确定两个 Object 实例是否相等。

    示例5

    using System;
    public class Sample
    {
        void Method()
        {
            Object Obj1 = new Object();
            Object Obj2 = new Object();
            Console.WriteLine(Obj1.Equals(Obj2));
            Console.WriteLine(Equals(Obj1,Obj2));
            Obj2 = Obj1;
            Console.WriteLine(Obj1.Equals(Obj2));
            Console.WriteLine(Equals(Obj1, Obj2));
        }
        static void Main()
        {
            Sample mySample = new Sample();
            mySample.Method();
        }
    }

    ★ Stack.CopyTo

    语法

    public virtual void CopyTo (Array array,int index)

    说明:
    从指定数组索引开始将 Stack 复制到现有一维 Array 中,复制过程按后进先出的顺序进行,类似于连续调用 Pop 所返回的元素的顺序。

    示例6

    using System;
    using System.Collections;
    public class SamplesStack
    {

        public static void Main()
        {
            Stack myStack = new Stack();
            myStack.Push("a");
            myStack.Push("b");
            myStack.Push("c");
            myStack.Push("d");
            myStack.Push("e");
            myStack.Push("f");

            //-------------------------------
            Object[] myarray = new Object[10];
            myarray[0] = "1";
            myarray[1] = "2";
            myarray[2] = "3";
            myarray[3] = "4";
            myarray[4] = "5";
          
           //-------------------------------
            myStack.CopyTo(myarray,4);
            foreach (Object obj in myarray)
            {
                System.Console.WriteLine(obj);
            }
        }
    }

    ★ Stack.ToArray

    语法
    public virtual Object[] ToArray ()

    返回值:返回新数组,包含 Stack 的元素的副本。

    说明:将 Stack 复制到新数组中,复制过程按后进先出的顺序进行。

    示例7

    using System;
    using System.Collections;
    public class SamplesStack
    {

        public static void Main()
        {
            Stack myStack = new Stack();
            myStack.Push("a");
            myStack.Push("b");
            myStack.Push("c");
            myStack.Push("d");
            myStack.Push("e");
            myStack.Push("f");
           //-------------------------------
            Object[] Myarray = myStack.ToArray();
            foreach (Object obj in Myarray)
            {
                System.Console.WriteLine(obj);
            }
        }
    }

    ==========================
    三 常用属性
    ==========================

    ★ Stack.Count 属性

    语法:public virtual int Count { get; }

    属性值:Stack 中包含的元素数。

    说明:获取 Stack 中包含的元素数,Count 是 Stack 中实际存储的元素数,通常容量始终大于或等于 Count。

    示例8

    using System;
    using System.Collections;
    public class SamplesStack
    {

        public static void Main()
        {
            Stack myStack = new Stack();
            myStack.Push("a");
            myStack.Push("b");
            myStack.Push("c");
            myStack.Push("d");
            myStack.Push("e");
            myStack.Push("f");
           //-------------------------------
            System.Console.WriteLine(myStack.Count);
           
        }
    }

    ★ Stack.SyncRoot 属性

    语法:public virtual Object SyncRoot { get; }

    属性值:可用于同步对 Stack 的访问的 Object。

    说明:

    若要创建 Stack 的同步版本,请使用 Synchronized 方法。但是,派生类可以使用 SyncRoot 属性来提供它们自己的 Stack 同步版本。同步代码必须在 Stack 的 SyncRoot 上执行操作,而不是直接在 Stack 上执行操作。这确保了从其他对象派生的集合的正确操作。特别是,它维护了与其他线程的正确同步,这些线程可能同时正在修改 Stack 对象。

    从头到尾对一个集合进行枚举本质上并不是一个线程安全的过程。即使一个集合已进行同步,其他线程仍可以修改该集合,这将导致枚举数引发异常。若要在枚举过程中保证线程安全,可以在整个枚举过程中锁定集合,或者捕捉由于其他线程进行的更改而引发的异常。

    示例9

    示例显示如何在整个枚举过程中使用 SyncRoot 锁定集合:

    using System;
    using System.Collections;
    public class SamplesStack
    {
        public static void Main()
        {
            Stack myStack = new Stack();
            myStack.Push("a");
            myStack.Push("b");
            myStack.Push("c");
            myStack.Push("d");
            myStack.Push("e");
            myStack.Push("f");
           //-------------------------------   
            lock (myStack.SyncRoot)
            {
                foreach (Object item in myStack)
                {
                    System.Console.WriteLine(item);
                }
            }
        }
    }

    ★ Stack.IsSynchronized 属性

    语法:
    public virtual bool IsSynchronized { get; }

    属性值:
    如果对 Stack 的访问是同步的(线程安全),则为 true;否则为 false。默认为 false。

    说明:

    若要保证 Stack 的线程安全,则必须通过由 Synchronized 方法返回的包装来执行所有操作。

    示例10

    using System;
    using System.Collections;
    public class SamplesStack
    {

        public static void Main()
        {
            Stack myStack = new Stack();
            myStack.Push("The");
            myStack.Push("quick");
            myStack.Push("brown");
            myStack.Push("fox");
           //--------------------------------
            Stack mySyncdStack = Stack.Synchronized(myStack);
            Console.WriteLine("{0}",myStack.IsSynchronized);
            Console.WriteLine("{0}", mySyncdStack.IsSynchronized);
        }
    }

    补充:

    数据在计算机内的存贮形式和数据的表示方法

    一、内存的组织形式

    ⑴ 位  二进制数所表示的数据的最小单位,就是二进制的1位数,简称位(bit)。
    计算机中的存贮器是由千千万万个小的电子线路单元组成的,每个单元称为一个“位”,它有两个稳定的工作状态(例如二极管或三极管的截止和导通,磁性元件的消磁与充磁等),分别以0和1表示,因此计算机存贮的信息是以二进制形式存贮的。内存贮器通常是由集成电路组成的,它包括几万、几十万、几百万甚至上亿个“位”。

    ⑵ 字节  为了便于管理,通常将8个“位”组成一个“字节”(byte)。也就是说一个字节可以放8个二进制数,如01100111,内存中存储数据时是以字节为单位的,字节是计算机中的最小存储单元。例如:一个字符占一个字节,一个整数占2个字节,一个实数占4个字节等。

    ⑶ 字长   若干个字节组成一个字(Word),其位数称为字长。一个“字”中可以存放一条计算机指令或一个数据,如果一个计算机系统以32个二进制的信息表示一条指令,就称这台计算机的“字长”为32位。通常所说的“32位机”就是以32位作为一个“字”的,一次传输的信息为32个位。

    字长是计算机能直接处理的二进制数的数据位数,直接影响到计算机的功能、用途及应用领域。常见的字长有8位、16位、32位、64位等。

    ⑷ 字节、字的位编号

    1个字节的位编号如下:               
    B7  B6  B5  B4 B3  B2  B1  B0
    高位字节               低位字节

    2个字节(16位)组成的字的编号如下:
    B15 B14 B13 B12 B11 B10 B9 B8 B7 B6 B5 B4 B3 B2 B1 B0
      高    位    字   节          低    位     字   

    我们把字最左边的一位称为最高有效位,最右边的一位称为最低有效位。在16位字中,我们称左边8位为高位字节,右边8位为低位字节。
     
    ⑸ 地址   每个字节有一个“地址”,只有通过地址才能找到某个存贮单元,并从中取数或向其存贮数据。计算机的整个内存被划分成若干个存储单元,每个存储单元可存放8位二进制数。每个存储单元可以存放数据或程序代码。为了能有效地存取该单元内存储的内容,每个单元必须有唯一的编号来标识,这个编号称为地址。

    二、计算机中数据的表示

    计算机最主要的功能是信息处理,要使计算机能处理信息,首先必须将各类信息转换成由二进制数0和1表示的代码,这一过程称为编码。计算机能处理的数据除了数值数据之外,更多的是字符、图像、图形、声音等非数值信息所对应的非数值数据。在计算机内部,各种信息都必须经过数字化编码后才能被传送、存储和处理。因此要了解计算机的工作原理,就必须了解编码知识,掌握信息编码的概念与处理技术是很重要的。

    1、数字化编码的概念
    所谓编码,就是采用少量的基本符号,按照一定的组合原则,表示大量复杂多样的信息。基本符号的种类和这些符号的组合规则是一切信息编码的两大要素。例如用26个英文字母表示英文词汇,用10个阿拉伯数码表示数字等,就是典型的编码例子。
    在计算机中,广泛采用的是只用“0”和“1”两个基本符号组成的二进制码。
    2、二进制数
    (1) 二进制数的表示方法
    数制,即进位计数制,是指用统一的符号规则来表示数值的方法。数制有多种形式。我们最熟悉的是十进制数,除习惯上使用的十进制数制外,计算机领域中更多是使用二进制、八进制和十六进制等数制。
    数制中的三个术语:

    ⑴ 数位。数位是指数码在一个数中所处的位置,例如
    数字 1235.67

    处在0位上的数字是 5
    处在1位上的数字是 3
    ....

    处在-1位上的数字是 6
    处在-2位上的数字是 7

    通常小数点左侧的位数用n表示,右侧的位数用m表示。

    ⑵ 基数。基数是指在数位上所能使用的数码的个数,例如十进位计数制中,每个数位上可以使用的数码为0,1,2…9十个数码,即其基数为10。

    ⑶ 权位。一个数字放在不同的数位上,表示的大小是不一样的,例如数字6放在0位(个位)上,其大小为6,即6*10^0,放在1位(十位)上,表示60,即6*10^1;也就是说一个数字放在不同的数位上,其大小是该数字乘一个固定的数值,这个固定的数值叫位权;

    位权 = 基数^n|m

    十进制数有十个基本数码0、1、2、3、4、5、6、7、8、9,进位原则是逢10进1,基数为10,依照这个规律,二进制数的数码为0和1,进位原则是逢2进1,基数为2。十进制与二进制的表示方法如下。

    十进制与二进制的对应关系表

    十进制数            9
    二进制数 10  11 100 101 110 111 1000  1001

    (2) 计算机中为什么要使用二进制数

    ⑴ 实现容易。二进制数只有两个数码:0和1,而电子器件的物理状态有两种稳定状态的很多,从而实现容易。例如,晶体管的导通和截止、脉冲的有和无等等,都可以有来表示二进制的1和0。

    ⑵ 运算规则简单。例如,一位二进制数的加法运算和一位二进制数的乘法运算规则为:
            0+0=0                            0×0=0
            0+1=1+0=1                        0×1=1×0=0
            1+1=10(逢二向高位进一)         1×1=1
    而减法和除法是加法和乘法的逆运算,根据上述规则,很容易实现二进制的四则运算。

    ⑶ 能方便使用逻辑代数。二进制数的0和1与逻辑代数“假”和“真”相对应,可使算术运算和逻辑运算共用一个运算器,易于进行逻辑运算。逻辑运算与算术运算的主要区别是:逻辑运算是按位进行的,没有进位和借位。

    ⑷ 记忆和传输可靠。电子元件对应的两种状态是一种质的区别,而不是量的区别,识别起来较容易。用来表示0和1的两种稳定状态的电子元件工作可靠、抗干扰强、存储和可靠性好,不易出错。

    3、数制之间的转换

    (1)十进制和二进制之间的转换

    由于人们习惯于十进制,因此常常要进行十进制和二进制数的转换工作。只要记住二进制的最基本的规定是逢二进一。一个十进制整数化为二进制数只需将它一次又一次的被2除,得到的余数(从最后一次的余数读起)就是用二进制表示的数。例如:

    得到 (11)10 =(1011)2
    在上面的式子中,括号的注脚10或2分别表示括号中的数是十进制数或二进制数。

    如果一个二进制整数要化为十进制数,课外要将它的最后一位乘以2^0,最后第二位乘以2^1,…依此类推,将各项相加就得到用十进制数表示的数。如:
    (1011)2 = 1*2^3+0*2^2+1*2^1+1*2^0
            = 2^3+2^1+2^0
            = (11)10

    (2)八进制

    ⑴ 八进制与二进制之间的转换

    由于二进制写起来很长,很难记,为方便起见,二进制数由低向高每三位组成一组,如:10110101111可分为10,110,101,111四组,每一组代表一个0到7之间的数,因为3位的二进制数是不会等于或大于8的,(111)2=(7)10,也就是说,以三位二进制作为一组(位)的数是适八进一的。(8)10 = 2^3=(1000)2就需要4位二进制数表示,即要向前一组数进一位。这种逢八进一的数称为八进制数。现分别把上面的数据每3位一组用八进制表示:

    二进制        10  110  101  111
    八进制              7

    八进制数和二进制数很容易互相转换,一个二进制数要转换为八进制数,只需将每3位二进制的数用一个八进制数表示即可。反之,如果知道一个八进制数,要转换为二进制数,只需将每位八进制数分别用3位二进制数表示即可。如八进制数10500用二制数表示:

    八进制              0
    二进制    001   000  101  000  000

    即得:(001000101000000)2

    ⑵ 八进制与十进制数间的转换

    八进制数转换为十进制数,它的基数为8,位权为8^n|m

    (163. 24)8 = 1×8^2+6×8^1+3×8^0+2×8^-1+4×8^-2
                 =(115.3125)10

    反之,一个十进制数要转换为八进制数,只需将它不断除以8,其余数的排列(由最手一个余数开始)就是以八进制表示的数。如下所示:

    (3)十六进制

    ⑴ 十六进制与二进制之间的转换

    由于一个字节包含8个二进位,因此常把一个字节中的8位分成二组,每组4个位,如10110101可以分为1011和0101,两组之间用逗号分割,即 1011,0101 。第一组用一个数来代表,一个4位的二进制数不会超过十进制数15,因为(1111)2是十进制15,(16)10是(10000)2,超过4位了。为便于表示,规定0到15之间每一个数都用一个符号来表示,在16进制中以A,B,C,D,E,F 分别代表十进制数10,11,12,13,14,15.见下表:

    二进制数      八进制数     十六进制数     十进制数

    0000           00                       0
    0001           01                       1
    0010           02                       2
    0011           03                       3
    0100           04                       4
    0101           05                       5
    0110           06                       6
    0111           07                       7
    1000           10                       8
    1001           11                       9
    1010           12                       10
    1011           13                       11
    1100           14                       12
    1101           15                       13
    1110           16                       14
    1111           17                       15
    10000          20           10             16

    如果有一个二进制数1010110000110111,可表示为:

    二进制     1010   1100   0011  0111
    十六进制                7

    即得(1010110000110111)2 = (AC37)16

    将一个十六进制数转换为二进制数,只需将其每一位分别转换为二进制数即可。例如:

    十六进制                 F
    二进制     0111   0010   1100   1111

    即得 (72CF)16 = (0111001011001111)2

    每个十六进制数都就表示为4位二进制数,如2应表示为0010,而不应只写成10,否则就会丢失某些位。很容易看出:72CF点两个字节,即每两位十六进制数在内存中占一个字节。

    ⑵ 十六进制与十进制数间的转换

    十六进制数的基数为16,权位为16^n|m

    例如:(2BC.48)16 = 2*16^2+B*16^1+C*16^0+4*16^-1+8*16^-2
                     = (700.28125)10

    反之,一个十进制数要转换成十六进制数,课外需将它不断除以16,取其余数由下到上顺序排列即可。如下所示:


    即得(41700)10 = (A2E4)16

    一个数或一个代码究竟以十进制形式表示或以八进制形式表示,或以十六进制形式表示,全视使用或阅读方便。

    一般说,在计算机外部(例如在程序中的常量)用十进制数表示。表示内存中存贮情况用二进制、八进制或十六进制比较直观。其实,八进制和十六进制是二进制派生出来的,用八进制或十六进制的好处在于数的位数少,便于记忆和书写。例如十进制数245用二进制表示为八位(11111110),用八进制表示为3位(376),用十六进制表示只需2位(FE)。

    在应用时,应弄清楚所用的数是以什么进制表示的(二进制?八进制?十六进制?或十进制?)对非十进制中的10和100应分别读作“壹零”和“壹零零”,而不要误读作“拾”和“壹佰”。

  • c#编程第十三章 异常和异常处理

    2010-03-02 15:35:12

    第十三章 异常和异常处理

    C# 语言的异常处理功能提供了处理程序运行时出现的任何意外或异常情况的方法。异常处理使用 try、catch 和 finally 关键字来尝试可能未成功的操作,处理失败,以及在事后清理资源。异常可以由公共语言运行库 (CLR)、第三方库或使用 throw 关键字的应用程序代码生成。

    ==========================
    一 throw 语句
    ==========================

    语法

    throw 表达式;

    说明

    throw 语句用于发出在程序执行期间出现反常情况(异常)的信号。其中的表达式必须是一个对象,该对象的类是从 System.Exception 派生的。

    备注:

    Exception 类是所有异常的基类。当发生错误时,系统或当前正在执行的应用程序通过引发包含关于该错误的信息的异常来报告错误。下面是几个常见的Exception派生类。

    ★ ArithmeticException类
       该类表示因算术运算、类型转换或转换操作中的错误而引发的异常。它包含如下三个子类。
    (1)DivideByZeroException:
         用零做除数时引发的异常。
    (2)NotFiniteNumberException
         当浮点值为无穷大或NaN时引发的异常。
    (3)OverflowException:
         算术运算、类型转换导致溢出时引发的异常。

    一般情况下,使用他们可以更精确地指出错误的精确特征。如果只对捕获一般的算法错误感兴趣,就引发一个 ArithmeticException 。

    ★ ArgumentException 类
       该类表示在向方法提供的其中一个参数无效时引发的异常。它的派生类主要有两个。

    (1)ArgumentNullException 类
         当将空引用传递给不接受它作为有效参数的方法时引发的异常。
    (2)ArgumentOutOfRangeException 类
    当参数值超出调用的方法所定义的允许取值范围时引发的异常。

    ★ ApplicationException 类
     
    发生非致命应用程序错误时引发的异常。通常由用户程序引发,而不是由公共语言运行库引发。 ApplicationException 不提供有关异常的原因的信息。大多数情况下都不应引发此类的实例。如果此类被实例化,则描述该错误的可读消息应传递给构造函数。如果打算设计需要创建自己的异常的应用程序,请从 ApplicationException 类派生。

    示例1

    程序在进行算术运算时发生了异常,主要是因为做除法运算时除数为0,所以程序无法运行,此时就会产生一个DivideByZeroException类的对象,由该对象显示错误信息。

    using System;
    public class ThrowTest
    {
        static void Main()
        {
            int a=100;
            int b=0;
            int c;
            c=a/b;
            Console.Write(c);
        }
    }

    输出:
    未处理的异常:  System.DivideByZeroException: 试图除以零。

    示例2

    当程序没有发生错误时,你也想抛出一个异常就使用throw去实现。

    using System;
    public class ThrowTest
    {
        static void Main()
        {
            int a = 100;
            int b = 0;
            int c = a+b;
            throw new DivideByZeroException();
        }
    }

    示例3

    using System;
    public class ThrowTest
    {
        static void Main()
        {
            string s = null;

            if (s == null)
            {
                throw new ArgumentNullException();
            }

            Console.Write("The string s is null");
        }
    }

    ==========================
    二 try-catch 语句
    ==========================

    语法

    try
    {
        ......
    }
    catch (System.Exception ex)
    {
        ......
    }

    说明
    try 块来对可能受异常影响的代码进行分区,并使用 catch 块来处理所产生的任何异常。一个try语句可以包含数个catch语句用于处理不同的语句情况。

    示例4

    在 try 里调用TestThrow()方法抛出一个异常,在与其相关联的 catch 块中捕获该异常。

    public class ThrowTryCatch
    {
        private static void TestThrow()
        {
            System.ApplicationException ex = new System.ApplicationException("非致命应用程序错误");
            throw ex;
        }
        static void Main()
        {
            try
            {
                TestThrow();
            }
            catch (System.ApplicationException ex)
            {
                System.Console.WriteLine(ex.ToString());
            }
        }
    }

    示例5

    using System;
    public class ThrowTryCatch
    {
        static void Main()
        {
            try
            {
                //throw new DivideByZeroException();
                throw new ArgumentNullException();
            }
            catch (DivideByZeroException d)
            {
                System.Console.WriteLine("捕获到第一个异常");
            }
            catch (ArgumentNullException e)
            {
                System.Console.WriteLine("捕获到第二个异常");
            }
        }
    }

    示例6

    Exception类的Message方法,用来获取描述当前异常的消息。

    using System;
    public class ThrowTryCatch
    {
        static void Main()
        {
            int a = 100;
            int b = 0;
            int c;
            try
            {
                c = a / b;
            }
            catch (Exception e)
            {
                System.Console.WriteLine("表达式:"+e.Message);
                return;
            }

        }
    }

    ==========================
    三 try-Finally语句
    ==========================

    异常发生时,执行将终止,并且控制交给最近的异常处理程序。这通常意味着不执行希望总是调用的代码行。有些资源清理(如关闭文件)必须总是执行,即使有异常发生。为实现这一点,可以使用 Finally 块。Finally 块总是执行,不论是否有异常发生。

    示例7

    在此例中,有一个导致异常的无效转换语句。当运行程序时,您收到一条运行时错误信息,但 finally 子句仍继续执行并显示输出。

    using System;
    public class MainClass
    {
        static void Main()
        {
            int i = 123;
            string s = "Some string";
            object o = s;

            try
            {
                i = (int)o;
            }
            finally
            {
                Console.WriteLine("i = {0}", i);
            }
        }
    }

    示例8

    using System;
    class ArgumentOutOfRangeExample
       {
       static public void Main()
          {
          int[] array1={1,2};
          int[] array2={3,4};
             try
             {
             Array.Copy(array1,array2,-1);
             }
             catch (ArgumentOutOfRangeException e)
             {
             Console.WriteLine("Error: {0}",e);
             }
             finally
             {
                 Console.WriteLine("Finally 块总是执行");
             }
          }
       }

    示例解读:

    程序使用 Try/Catch 块捕捉ArgumentOutOfRangeException。Main 方法创建两个数组并试图将一个数组复制到另一个数组。该操作生成 ArgumentOutOfRangeException,同时错误被写入控制台。Finally 块执行,不论复制操作的结果如何。

    注释:Array.Copy 方法

    语法:
     
    Array.Copy (Array1, Array2, Int32)

    参数:

    Array1:包含要复制的数据
    Array2:它接收数据
    Int32:一个 32 位整数,它表示要复制的元素数目

    说明:

    从第一个元素开始复制 Array 中的一系列元素,将它们粘贴到另一 Array 中(从第一个元素开始)。长度指定为 32 位整数。

  • C#运算符重载

    2010-02-26 17:07:09

    C# 也允许您重载运算符,以供您自己的类使用。这样做,可以使使用用户定义的数据类型就像使用基本数据类型一样自然、合理。例如,您可以创建一个名为 ComplexNumber 的新数据类型来表示一个复杂的数字,并提供使用标准算术运算符对此类数字执行数学运算的方法,如使用 + 运算符将两个复杂数字相加。

    若要重载某个运算符,可以编写一个函数,在其命名运算符之后加上要重载的运算符的符号。例如,可按以下方法重载 + 运算符:

    public static ComplexNumber operator+(ComplexNumber a, ComplexNumber b)

    所有运算符重载均为类的静态方法。此外还应注意,重载相等运算符 (==) 时,还必须重载不相等运算符 (!=)。< 和 > 运算符以及 <= 和 >= 运算符也必须成对重载。

    以下是可重载的运算符的完整列表:

    •   一元运算符:+、-、!、~、++、--、true、false
    •   二元运算符:+, -, *, /, %, &, |, ^, <<, >>, ==, !=, >, <, >=, <=

    下面的代码示例创建一个重载 + 和 - 运算符的 ComplexNumber 类:

     

    代码
    public class ComplexNumber
    {
    private int real;
    private int imaginary;

    public ComplexNumber() : this(0, 0) // constructor
    {
    }

    public ComplexNumber(int r, int i) // constructor
    {
    real
    = r;
    imaginary
    = i;
    }

    // Override ToString() to display a complex number in the traditional format:
    public override string ToString()
    {
    return(System.String.Format("{0} + {1}i", real, imaginary));
    }

    // Overloading '+' operator:
    public static ComplexNumber operator+(ComplexNumber a, ComplexNumber b)
    {
    return new ComplexNumber(a.real + b.real, a.imaginary + b.imaginary);
    }

    // Overloading '-' operator:
    public static ComplexNumber operator-(ComplexNumber a, ComplexNumber b)
    {
    return new ComplexNumber(a.real - b.real, a.imaginary - b.imaginary);
    }
    }

    使用这个类,您就可以用以下代码创建和操作两个复杂的数字:

     

    代码
    class TestComplexNumber
    {
    static void Main()
    {
    ComplexNumber a
    = new ComplexNumber(10, 12);
    ComplexNumber b
    = new ComplexNumber(8, 9);

    System.Console.WriteLine(
    "Complex Number a = {0}", a.ToString());
    System.Console.WriteLine(
    "Complex Number b = {0}", b.ToString());

    ComplexNumber sum
    = a + b;
    System.Console.WriteLine(
    "Complex Number sum = {0}", sum.ToString());

    ComplexNumber difference
    = a - b;
    System.Console.WriteLine(
    "Complex Number difference = {0}", difference.ToString());
    }
    }

    如程序所示,您现在可以用非常直观的方式对属于 ComplexNumber 类的对象使用加减运算符。结果如下:

    Complex Number a = 10 + 12i

    Complex Number b = 8 + 9i

    Complex Number sum = 18 + 21i

    Complex Number difference = 2 + 3i

  • c#编程第十二章 事 件

    2010-02-26 16:03:17

    第十二章 事 

    对于“事件”这个词,C#的帮助从各个角度进行了描述,比如“事件是类在发生其关注的事情时用来提供通知的一种方式”,“事件是可以通过代码响应或“处理”的操作”,“事件是操作发生时允许执行特定于应用程序的代码的机制”,“事件是对象发送的消息,以发信号通知操作的发生”,“事件是当对象发生用户关心的情况时,类将此对象通知用户的方法”等等,概括起来就是一句话“事件是软件或硬件发生的某些事情,它要求应用程序的响应”。

    ======================
    一 不包含附加信息的事件
    ======================
    1 定义事件委托

    要使用事件首先要定义一个与事件相关的委托,因为事件发生后一定要有某个类中的方法来处理它,而委托正是由对象引用以及该对象的方法引用组成的,所以委托能够将事件绑定到用来处理它们的方法上。

    ★ 事件委托准则:

    .NET Framework 指南指示用于事件的委托类型应采用两个参数:“对象源”参数(用于指示事件源)和特定于事件的参数(它封装有关事件的其他任何信息)。特定于事件的参数应从 EventArgs 类派生。对于不使用任何附加信息的事件,.NET Framework 提供了 EventHandler 类。

    准则告诉我们用于事件的委托一定要有两个参数,第一个参数用于指示事件源,第二个参数用于指示特定事件,所谓特定事件指的是具有附加信息的事件和没有附加信息的事件,如果使用没有附加信息的事件,委托参数就使用内置的EventHandler类的形式。

    ★ EventHandler

    EventHandler 是命名空间System中的一个预定义的委托,是专用于表示不生成数据的事件的事件处理程序方法。

    语法

    public delegate void EventHandler (
     Object sender,
     EventArgs e
    );

    参数
    sender:事件源,类型为 Object,用于引发事件的实例。
    e:不包含任何事件数据的 EventArgs。

    解读:

    语法给出了事件委托的基本形式,其中EventHandler是一个委托的名称,可以根据习惯任意更改,第一个参数中的Object表示数据类型,sender是使用该类型创建的实例名,第二个参数EventArgs是一个内置类,e 是该类的一个实例。

    通过上面的学习,我们懂得了如何为事件定义一个委托,下面就定义这样一个委托。

    //定义委托
    public delegate void TestEventDelegate(object sender, System.EventArgs e);

    2 声明事件

    事件和方法一样具有签名,签名包括名称和参数列表。事件的签名使用委托的签名来表示。

    声明事件需要使用 event 关键字。

    语法

    修饰符 event 委托名称 事件名称

    public event TestEventDelegate TestEvent;

    事件要放在引发事件的类中,定义一个事件引发类。

    //定义事件引发类
    public class EventSource
    {
        public event TestEventDelegate TestEvent;
        private void RaiseTestEvent() { }
    }

    3 引发事件

    若要引发事件,类可以调用委托,并传递所有与事件有关的参数。

    private void RaiseTestEvent()
    {
        TestEventDelegate temp = TestEvent;

        if (temp != null)
        {
            temp(this, new System.EventArgs());
        }
    }

    4 订阅事件

    要接收某个事件的类可以创建一个方法来接收该事件,然后向类事件自身添加该方法的一个委托。这个过程称为“订阅事件”。

    首先,接收类必须具有与事件自身具有相同签名(如委托签名)的方法。然后,该方法(称为事件处理程序)可以采取适当的操作来响应该事件。

    //定义事件接收类
    public class EventReceiver
    {
        //定义事件处理方法
        public void ReceiveTestEvent(object sender, System.EventArgs e)
        {
            System.Console.Write("Event received from ");
            System.Console.WriteLine(sender.ToString());
        }
    }

    若要订阅事件,接收器必须创建一个与事件具有相同类型的委托,并使用事件处理程序作为委托目标。然后,接收器必须使用加法赋值运算符 (+=) 将该委托添加到源对象的事件中。

    //事件订阅方法
    public void Subscribe(EventSource source)
    {
        TestEventDelegate temp = ReceiveTestEvent;
        source.TestEvent += temp;
    }

    若要取消订阅事件,接收器可以使用减法赋值运算符 (-=) 从源对象的事件中移除事件处理程序的委托。例如:

    //取消订阅方法 
    public void UnSubscribe(EventSource source)
    {
        TestEventDelegate temp = ReceiveTestEvent;
        source.TestEvent -= temp;
    }

    5 整合

    前面的代码不会运行,必需在入口方法中将他们整合。

     static void Main()
        {
            EventReceiver receiver = new EventReceiver();
            EventSource source = new EventSource();
            receiver.Subscribe(source);
            source.RaiseTestEvent();
        }

    完整代码

    //定义委托
    public delegate void TestEventDelegate(object sender, System.EventArgs e);
    //事件引发类
    public class EventSource
    {
        //声明事件
        public event TestEventDelegate TestEvent;
        //事件引发方法
        private void RaiseTestEvent()
        {
            TestEventDelegate temp = TestEvent;

            if (temp != null)
            {
                temp(this, new System.EventArgs());
            }
        }

        static void Main()
        {
            EventReceiver receiver = new EventReceiver();
            EventSource source = new EventSource();
            receiver.Subscribe(source);
            source.RaiseTestEvent();
        }

    }

    //事件接收类
    public class EventReceiver
    {
        //事件处理方法
        public void ReceiveTestEvent(object sender, System.EventArgs e)
        {
            System.Console.WriteLine("Event received from");
            System.Console.WriteLine(sender.ToString());
        }
        //事件订阅方法
        public void Subscribe(EventSource source)
        {
            TestEventDelegate temp = ReceiveTestEvent;
            source.TestEvent += temp;
        }
    }

    事件使用过程小结:

    1 定义委托
    2 定义事件引发类
    (1)定义事件
    (2)定义引发方法,在这个方法中,将事件分配给一个委托实例,然后调用委托。
          
    3 定义事件接收类
    (1)定义事件处理方法
    (2)定义事件订阅方法,在这个方法中,将事件处理方法分配给委托的一个实例,然后将委托绑定到事件。      

    =======================
    二 包含附加信息的事件
    =======================

    如果使用包含附加信息的事件,委托的第二个参数应从 EventArgs 类派生。vs2005对EventArgs 类有如下说明:

    ★ EventArgs

    EventArgs 是包含事件数据的类的基类。此类不包含事件数据,在事件引发时不向事件处理程序传递状态信息的事件会使用此类。如果事件处理程序需要状态信息,则应用程序必须从此类派生一个类来保存数据。

    下面我们以台风登陆事件为例来说明,06年第8号超强台风“桑美”,中心风力17级,以每小时20公里的速度向我国东部沿海袭来,登陆地点是福建和浙江。
     
    ★ 定义信息保存类

    台风登陆时需要说明登陆地点和风力,因此设计一个类用来保存这两项数据。

    using System;
    public class Typhoonclass : EventArgs
    {
        public string place;
        public int level;
        public Typhoonclass(string place, int level)
        {
            this.place = place;
            this.level = level;
        }
    }

    ★ 事件引发类

    public delegate void TyphoonDelegate
    (object sender,Typhoonclass e);
    public class EventSource
     
        public event TyphoonDelegate TyphoonEvent;
        public void Raise(string place, int level)
       {
          TyphoonDelegate temp = TyphoonEvent;

           if (temp != null)
         {
          temp(this, new Typhoonclass(place,level));
         }
       }
    }

    ★ 事件接收类

    public class EventReceiver
    {
          public void EventHandling(object sender,Typhoonclass e)
        {
            Console.WriteLine("订阅方法被" +
                   sender.ToString() + "调用。");
            if (e.level < 5)
                Console.WriteLine("登陆" + e.level +
                    "的风力一般,要稍加防范");
            else if (e.level < 10)
                Console.WriteLine("登陆" + e.level +
                    "的风力较大,船只回港避风");
            else
                Console.WriteLine("登陆" + e.level +
                    "的风力很大,船只回港,人员疏散避风");
        }

       public void Subscribe(EventSource source)
        {
            TyphoonDelegate temp  = EventHandling;
            source.TyphoonEvent += temp;
        }
    }

    public class Mainclass
    {
        public static void Main()
        {
        EventSource source = new EventSource();
        EventReceiver receiver = new EventReceiver();
        receiver.Subscribe(source);
        source.Raise("浙江",7);

        }
    }

    //=============完整代码=====================
    using System;
    public delegate void TyphoonDelegate(object sender, Typhoonclass e);
    public class Typhoonclass : EventArgs
    {
        public string place;
        public int level;
        public Typhoonclass(string place, int level)
        {
            this.place = place;
            this.level = level;
        }
    }

    public class EventSource
     
        public event TyphoonDelegate TyphoonEvent;
        public void Raise(string place, int level)
       {
          TyphoonDelegate temp = TyphoonEvent;
           if (temp != null)
         {
          temp(this, new Typhoonclass(place,level));
         }
       }
    }

    public class EventReceiver
    {
        public void EventHandling(object sender, Typhoonclass e)
        {
            if (e.level < 5)
                Console.WriteLine("登陆‘" +e.place+"’的风力为:" +e.level +"级,要稍加防范");
            else if (e.level < 10)
                Console.WriteLine("登陆‘" +e.place+"’的风力为:" +e.level +"级,船只要回港避风");
            else
                Console.WriteLine("登陆‘" +e.place+"’的风力为:" +e.level +"级,船只回港人员紧急疏散");
        }

       public void Subscribe(EventSource source)
        {
            TyphoonDelegate temp  = EventHandling;
            source.TyphoonEvent += temp;
        }
    }

    public class Mainclass
    {
        public static void Main()
        {
        EventSource source = new EventSource();
        EventReceiver receiver = new EventReceiver();
        receiver.Subscribe(source);
        source.Raise("上海", 3);
        source.Raise("浙江",8);
        source.Raise("福建", 12);
        }
    }

    ======================
    三 匿名方法
    ======================

    在下面的示例中,类 TestButton 包含事件 OnClick。派生自 TestButton 的类可以选择响应 OnClick 事件,并且定义了处理事件要调用的方法。可以以委托和匿名方法的形式指定多个处理程序,匿名方法仅适用于C#2.0语法。

    //--------------匿名方法1------------------
    public delegate void ButtonEventHandler();
    class TestButton
    {
        public event ButtonEventHandler OnClick;
        public void Click()
        {
            OnClick();
        }
    }

    class myclass:TestButton
    {
    static void Main()
        {
            TestButton mb = new TestButton();
            mb.OnClick += delegate { System.Console.WriteLine("Hello, World!"); };
            mb.Click();  
        }
    }

    //--------------匿名方法2------------------
    public delegate void ButtonEventHandler();
    class TestButton
    {
        public event ButtonEventHandler OnClick;
        public void Click()
        {
            ButtonEventHandler mb = OnClick;
            mb();
        }
    }

    class myclass : TestButton
    {
        static void Main()
        {
            TestButton sb = new TestButton();
            ButtonEventHandler mb = delegate{System.Console.WriteLine("Hello, World!");};
            sb.OnClick += mb;
            sb.Click();
        }
    }

    ======================
    四 在接口中声明事件
    ======================
    此示例说明可以在接口中声明一个事件,然后在类中实现它。

    public delegate void TestDelegate();
    public interface ITestInterface
    {
        event TestDelegate TestEvent;
        void FireAway();
    }

    public class TestClass : ITestInterface
    {
        public event TestDelegate TestEvent;
        public void FireAway()
        {
            if (TestEvent != null)
            {
                TestEvent();
            }
        }
    }

    public class MainClass
    {
        static private void F()
        {
            System.Console.WriteLine("This is called when the event fires.");
        }
        static void Main()
        {
            ITestInterface i = new TestClass();
            i.TestEvent += F;
            i.FireAway();
        }
    }

    ======================
    五 事件访问器
    ======================

    可以使用事件访问器声明事件。事件访问器使用的语法非常类似于属性访问器,它使用 add 关键字和代码块添加事件的事件处理程序,使用 remove 关键字和代码块移除事件的事件处理程序。

    public delegate void TestEventDelegate();
    public class EventSource
    {
        private TestEventDelegate TestEventHandlers;
        public event TestEventDelegate TestEvent
        {
            add
             
                    TestEventHandlers += value; 
            }
            remove
            {
                    TestEventHandlers -= value;   
            }
        }
        public void RaiseTestEvent()
        {
            TestEventDelegate temp = TestEventHandlers;
            if (temp != null)
            {
                temp();
            }

        }
    }

    public class testMain
    {
        public void DelegateMethod()
        {
            System.Console.WriteLine("事件访问器");
        }
        static void Main()
        {
            testMain a = new testMain();
            EventSource b = new EventSource();
            b.TestEvent += a.DelegateMethod;
            b.RaiseTestEvent();
        }
    }

    引发事件的类必须具有存储和检索处理程序的机制,才能使用事件访问器。前面的示例使用一个私有委托字段 TestEventHandlers,以及加法和减法赋值运算符在列表中添加和移除处理程序。这与没有使用访问器声明的事件的工作方式极为类似。如果事件接收器使用加法赋值运算符 (+=) 添加事件处理程序,则调用 add 访问器,并且新的处理程序在访问器中可作为局部变量命名值使用。如果使用减法赋值运算符 (-=),则调用 remove 访问器,并且要移除的处理程序可作为局部变量命名值使用。两个访问器都返回 void,因此所有返回语句都不能返回值。无论类是否声明了事件访问器,事件的订阅和取消订阅都使用相同的语法。

  • c#编程第十一章 委托

    2010-02-26 13:51:10

    第十一章 委托

    在学习委托之前我们首先看看一个简单的类,在这个类中定义了一个计算两数之和的方法,如下所示。

    public class Sumclass
    {
        public int Sum(int a, int b)
        {
            return a + b;
        }
    }
    class MainClass
    {
        static void Main()
        {
          Sumclass example = new Sumclass();
          int sum=example.Sum(1,2);
          System.Console.WriteLine("{0}", sum);
        }
    }

    在上面的示例中,当你在实例上调用方法并为方法传递了正确的参数后,就会有正确的输出。

    什么是委托?

    通常当我们没有时间去完成一件事情的时候,可以委托给另一个人去做,另一个人就是我们的代理。同样的道理当一个类中的方法调用需要让别人执行时就要创建委托。

    ==========================
    一 使用委托
    ==========================

    ★ 委托的概念

    委托是一种引用方法的类型,或者说委托是一种安全地封装方法的类型。一旦为委托分配了方法,委托将与该方法具有完全相同的行为。委托方法的使用可以像其他任何方法一样,具有参数和返回值。

    上面的定义包含了三个要点。

    1 委托是一种数据类型。
    2 委托能够引用方法,或者说委托封装方法并代理它。
    3 委托是安全的。

    ★ 声明委托

    声明委托类型请使用delegate关键字,如下;

    public delegate void myDelegate(string message);

    说明:

    上式中声明了一个委托myDelegate,这个委托的返回值为void类型,只有一个参数,参数的类型及数量统称为参数列表。返回类型与参数列表合称为签名,当使用委托代理方法时委托的签名必须与方法的签名一致。

    示例

    public delegate int myDelegate(int a, int b);
    public class Sumclass
    {
        public int Sum(int a, int b)
        {
            return a + b;
        }
    }
    class MainClass
    {
        static void Main()
        {
            Sumclass A = new Sumclass();
            //实例化委托并向其分配方法
            myDelegate B = A.Sum;
            //调用委托并将返回值保存到变量sum中
            int sum = B(1, 2);
            System.Console.WriteLine("{0}", sum);
        }
    }

    示例解读:

    示例为类Sumclass定义了一个委托,注意其签名与类中的方法签名要一致,在用于输出的类中创建该类的一个实例,然后将实例方法分配给委托实例B,这时委托实例B就完全可以替代类中的方法了。

    ★实例化委托

    C#2.0 允许我们在进行委托实例化时,省略掉委托类型,而直接采用方法名,C#编译器会做出合理的推断。

    C# 1.0中实例化委托时要用关键字New,例如上例中委托实例应该这样来写:

    Mydelegate B=New Mydelegate(A.Sum)

    C# 2.0中直接采用方法名,如上例中

    myDelegate B = A.Sum

    注意:用委托代理方法时,方法名的后边一定不能带有圆括号()。

    示例

    public delegate void Del(string message);
    public class myclass
    {
    public static void DelegateMethod(string message)
    {
        System.Console.WriteLine(message);
    }
    static void Main()
        {
          Del handler = DelegateMethod;
          handler("Hello World");
         }
    }

    示例

    public delegate void Del(string message);
    public class myclass
    {
        public static void DelegateMethod(string message)
        {
            System.Console.WriteLine(message);
        }
       
    }
    class MainClass
    {
        static void Main()
        {
            Del handler = myclass.DelegateMethod;
            handler("Hello World");
        }
    }

    ★ 异步回调

    委托类型派生自 .NET Framework 中的 Delegate 类。默认是密封的,不能从其中再派生新的委托类型,也不可能从中派生自定义类。由于实例化委托是一个对象,所以可以将其作为参数进行传递,也可以将其赋值给属性。这样,方法便可以将一个委托作为参数来接受,并且以后可以调用该委托。这称为异步回调,是在较长的进程完成后用来通知调用方的常用方法。以这种方式使用委托时,使用委托的代码无需了解有关所用方法的实现方面的任何信息。此功能类似于接口所提供的封装。

    示例:

    public delegate void myD(string message);
    public class aClass
    {
        public void aMethod(string message)
        {
            System.Console.WriteLine(message);
        }

        public void bMethod(myD s)
        {
            s("ok! delegate");
        }

    }
    public class textclass
    {
        static void Main()
        {
            aClass A = new aClass();
            myD B = A.aMethod;
            B("Hello World");
            A.bMethod(B);
        }
    }

    回调的另一个常见用法是定义自定义的比较方法并将该委托传递给排序方法。它允许调用方的代码成为排序算法的一部分。

    示例:

    public delegate void myDelegate(string message);
    public class DelClass
    {
        public void DelegateMethod(string message)
        {
            System.Console.WriteLine(message);
        }
       
        public void MethodWithCallback(int param1, int param2, myDelegate callback)
        {
            callback("The number is: " + (param1 + param2).ToString());
        }

    }
    public class textclass
    {
        static void Main()
        {
           DelClass bj = new DelClass();
           myDelegate d = obj.DelegateMethod;
           d("Hello World");
           obj.MethodWithCallback(1,2,d);
        }
    }

    说明:

    在控制台中将收到下面的输出:The number is: 3 ,示例中
    MethodWithCallback 的作用只是准备字符串并将该字符串传递给其他方法。此功能特别强大,因为委托的方法可以使用任意数量的参数。

    ★ 委托特点:

    1 委托类似于 C++ 函数指针,但它是类型安全的。

    2 委托允许将方法作为参数进行传递。

    3 委托可用于定义回调方法。

    4 委托可以链接在一起;例如,可以对一个事件调用多个方法。

    5 方法不需要与委托签名精确匹配。(在协变和逆变里将讲到)

    6 C# 2.0 版引入了匿名方法的概念,此类方法允许将代码块作为参数传递,以代替单独定义的方法。

    一个委托delegate对象一次可以搭载多个方法,而不是一次一个。当我们唤起一个搭载了多个方法的委托,所有方法将以其被搭载到委托对象的顺序被依次唤起。一个委托delegate对象搭载的方法不需要属于同一个类,但它搭载的所有方法必须具有相同的签名。

    ==========================
    二 命名方法
    ==========================

    委托可以与命名方法关联。使用命名的方法对委托进行实例化时,该方法将作为参数传递,例如:

    delegate void Del(int x);
    void DoWork(int k) { }
    Del d = obj.DoWork;

    使用命名方法构造的委托可以封装静态方法或实例方法。作为委托参数传递的方法必须与委托声明具有相同的签名(由返回类型和参数组成)。委托实例可以封装静态或实例方法。

    示例 1

    以下是声明及使用委托的一个简单示例。注意,委托 Del 和关联的方法 MyNumbers 具有相同的签名

    delegate void Del(int i, double j);
    class MathClass
    {
        static void Main()
        {
            MathClass m = new MathClass();
            Del d = m.MyNumbers;
            System.Console.WriteLine("MyNumbers:");
            for (int i = 1; i <= 5; i++)
            {
                d(i, 2);
            }
        }
        void MyNumbers(int m, double n)
        {
            System.Console.Write(m * n + " ");
        }
    }

    示例 2

    在下面的示例中,一个委托被同时映射到静态方法和实例方法,并分别返回特定的信息。

    delegate void Del();
    class SampleClass
    {
        public void InstanceMethod()
        {
            System.Console.WriteLine("instance method.");
        }
        public static void StaticMethod()
        {
            System.Console.WriteLine("static method.");
        }
    }

    class TestSampleClass
    {
        static void Main()
        {
            SampleClass sc = new SampleClass();
            Del d = sc.InstanceMethod;
            d();
            d = SampleClass.StaticMethod;
            d();
        }
    }

    ==========================
    三 匿名方法
    ==========================

    在 2.0 之前的 C# 版本中,声明委托的唯一方法是使用命名方法。C# 2.0 引入了匿名方法。匿名方法允许我们以一种“内联”的方式来编写方法代码,将代码直接与委托实例相关联,从而使得委托实例化的工作更加直观和方便。创建匿名方法的语法如下:

    delegate void Del(int x);
    Del d = delegate(int k) { };

    当你要将代码块传递为委托参数时,创建匿名方法则是唯一的方法。使用匿名方法的好处是,不必创建单独的方法,因此减少了实例化委托所需的编码系统开销。注意大括号后边要加分号。

    示例

    下面的示例演示实例化委托的两种方法:使委托与匿名方法关联。使委托与命名方法 (DoWork) 关联。两种方法都会在调用委托时显示一条消息。

    delegate void Printer(string s);
    class TestClass
    {
        static void Main()
        {
            Printer p = delegate(string j)
            {
                System.Console.WriteLine(j);
            };
            p("a_ called.");
            Printer S = new Printer(TestClass.DoWork);
            S("b_called.");
        }
        static void DoWork(string k)
        {
            System.Console.WriteLine(k);
        }
    }

    ★匿名方法的几个相关问题:

    1匿名方法的参数列表

    ·匿名方法可以在delegate关键字后跟一个参数列表(可以不指定),后面的代码块则可以访问这些参数:

    Del d = delegate(int k) { };

    ·注意“不指定参数列表”与“参数列表为空”的区别

    Del d = delegate{ }; // 正确!
    Del d = delegate() { };// 错误!

    2 匿名方法的返回值

    ·如果委托类型的返回值类型为void,匿名方法里
    便不能返回任何值;

    delegate void MyDelegate();
    MyDelegate d = delegate{
    ……
    return;
    };

    ·如果委托类型的返回值类型不为void,匿名方法
    里返回的值必须和委托类型的返回值兼容:

    delegate int MyDelegate();
    MyDelegate d = delegate{
      ……
    return 100;
    };

    3 匿名方法的外部变量

    ·一些局部变量和参数有可能被匿名方法所使用,
    它们被称为“匿名方法的外部变量”。

    ·外部变量的生存期会由于“匿名方法的捕获效益”
    而延长——一直延长到委托实例不被引用为止。

    static void Foo(double factor) {
    MyDelegate f=delegate(int x) {
    factor +=0. 2; // factor为外部变量
    return x * factor;
    };
    Invoke(f); // factor的生存期被延长
    }

    ==========================
    四 多路广播委托
    ==========================
    委托对象的一个用途在于,可以使用 +或+= 运算符将它们分配给一个要成为多路广播委托的委托实例。组合的委托可调用组成它的那两个委托。只有相同类型的委托才可以组合。- 运算符可用来从组合的委托移除组件委托。

    示例

    delegate void Del(string s);

    class TestClass
    {
        static void Hello(string s)
        {
            System.Console.WriteLine("  Hello, {0}!", s);
        }

        static void Goodbye(string s)
        {
            System.Console.WriteLine("  Goodbye, {0}!", s);
        }

        static void Main()
        {
            Del a, b, c, d;
            a = Hello;
            b = Goodbye;
            c = a + b;
            d = c - a;

            System.Console.WriteLine("Invoking delegate a:");
            a("A");
            System.Console.WriteLine("Invoking delegate b:");
            b("B");
            System.Console.WriteLine("Invoking delegate c:");
            c("C");
            System.Console.WriteLine("Invoking delegate d:");
            d("D");
        }
    }

    ==========================
    五 协变和逆变
    ==========================

    将委托方法与委托签名匹配时,协变和逆变提供了一定程度的灵活性。协变允许将带有派生返回类型的方法用作委托,逆变允许将带有派生参数的方法用作委托。这使委托方法的创建变得更为灵活,并能够处理多个特定的类或事件。

    ★协变

    当委托方法的返回类型具有的派生程度比委托签名更大时,就称为协变委托方法。因为方法的返回类型比委托签名的返回类型更具体,所以可对其进行隐式转换。这样该方法就可用作委托。协变使得创建可被类和派生类同时使用的委托方法成为可能。

    示例 1

    此示例演示委托如何使用派生的返回类型。SecondHandler 返回的数据类型为 Dogs 类型,是委托签名返回类型 Mammals 的子集。因此,SecondHandler 返回的所有可能值都可以存储在 Mammals 类型的变量中。

    class Mammals
    {
    }

    class Dogs : Mammals
    {
    }

    class Program
    {
        public delegate Mammals HandlerMethod();
        public static Mammals FirstHandler()
        {
            return null;
        }
        public static Dogs SecondHandler()
        {
            return null;
        }

        static void Main()
        {
            HandlerMethod handler1 = FirstHandler;
            HandlerMethod handler2 = SecondHandler;
        }
    }

    示例解读:

    1 创建两个类Mammals和Dogs,其中Dogs继承Mammals。
    2 在类Program中定义了一个委托HandlerMethod,返回类型为Mammals
    ,在这个类中还定义了两个方法,其中FirstHandler的签名和委托是一致的,可以认为是我们前面讲过的签名方法。而SecondHandler方法与委托签名不一致,其返回类型Dogs是继承Mammals的,也就是说其返回类型具有的派生程度比委托签名更大。更具体,所以SecondHandler是协变委托方法,所以在将这个方法分配给委托实例时可以对其进行隐式转换。式子HandlerMethod handler2 = SecondHandler,中的Dogs类型方法SecondHandler被隐式的转换为Mammals类型了。

    ★逆变

    当委托方法签名具有一个或多个参数,并且这些参数的类型派生自方法参数的类型时,就称为逆变委托方法。因为委托方法签名参数比方法参数更具体,因此可以在传递给处理程序方法时对它们进行隐式转换。这样逆变使得可由大量类使用的更通用的委托方法的创建变得更加简单。

    示例 2

    此示例演示委托如何使用从委托签名参数类型派生的类型的参数。因为要求委托支持 Dogs 类型的参数,我们知道带有 Mammals 类型的参数的处理程序一定是可接受的,因为 Dogs 是 Mammals 的子集。

    class Mammals
    {
    }

    class Dogs : Mammals
    {
    }

    class Program
    {
        public delegate void HandlerMethod(Dogs sampleDog);
        public static void FirstHandler(Mammals elephant)
        {
        }

        public static void SecondHandler(Dogs sheepDog)
        {
        }

        static void Main(string[] args)
        {
            HandlerMethod handler1 = FirstHandler;
            HandlerMethod handler2 = SecondHandler;
        }
    }

    ==========================
    六 使用委托与使用接口
    ==========================
    委托和接口都允许类设计器分离类型声明和实现。接口和委托有很多相似性,它们都可由不了解实现该接口或委托方法的类的对象使用。

    ★在以下情况中使用委托:

    1 当使用事件设计模式时。

    2 当封装静态方法可取时。

    3 当调用方不需要访问实现该方法的对象中的其他属性、方法或接口时。

    4 需要方便的组合。

    5 当类可能需要该方法的多个实现时。

    ★在以下情况中使用接口:

    1 当存在一组可能被调用的相关方法时。

    2 当类只需要方法的单个实现时。

    3 当使用接口的类想要将该接口强制转换为其他接口或类类型时。

    4 当正在实现的方法链接到类的类型或标识时。

  • c#编程第十章 命名空间

    2010-02-25 23:34:00

    第十章 命名空间

    ★ Windows平台

    我们经常使用的操作系统Windows是一个平台,它为Windows应用程序提供运行环境,在这个平台上包含了很多具有各种功能的代码块(类,函数等),这些代码块组成了一个很大的库,叫 Windows API,我们经常使用的对话框窗口,文档接口窗口,组件服务等等都是Windows API的一部分。

    ★ .NET Framwork平台

    .NET Framwork也是一个平台,它建立在Windows平台基础之上,在这个平台上除了可以应用Windows平台上的部分功能接口外,还提供了大量具有自己特色的功能块,这些功能块主要有类,接口等组成,为了更好的组织这些代码块,.NET Framwork对他们进行了分组管理,按照不同的需求把它们放到不同的容器中,这种包含特定代码块的容器叫做命名空间。

    使用 C# 编程时,通过两种方式来大量使用命名空间。其一是使用命名空间来组织它的众多类,也就是在C#中预定义了许多命名空间,其二是在较大的编程项目中,声明自己的命名空间,用于帮助控制类名称和方法名称的范围,也就是可以自定义命名空间。

    ★ 访问命名空间

    大多数 C# 应用程序从一个 using 指令节开始。该节列出应用程序将会频繁使用的命名空间,避免程序员在每次使用其中包含的方法时都要指定完全限定的名称。

    示例

    using System;
    class Hello
    {
        static void Main()
        {
            Console.WriteLine("Hello World!");
        }
    }

    说明:

    在访问命名空间时,要使用using指令进行指定并且放在程序的开头,如果不在第一行指定命名空间,在输出语句中就必须书写命名空间的名称。

    例如:

    class Hello
    {
        static void Main()
        {
            System.Console.WriteLine("Hello World!");
        }
    }

    ★ 命名空间别名

    从上面的示例中我们学会了使用using指令为程序指定要使用的命名空间,除此以外using指令还有一个功能,就是建立命名空间别名而引用命名空间,其格式如下:

    using 别名 = 命名空间
    using 别名 = 命名空间.类名

    示例

    现在我们为System.Console起一个别名,假如叫做Alias,则程序可以写为如下形式:

    using Alias = System.Console;
    class Hello
     
        static void Main()
          
           Alias.WriteLine("Hello World!");
        }
    }

    ★ 自定义命名空间

    除了上面使用 using 指定使用内置命名空间外,在较大的编程项目中,声明自己的命名空间可以帮助控制类名称和方法名称的范围。命名空间声明使用namespace关键字,格式如下所示:

    namespace 名称
    {
       类型声明
    }

    示例

    namespace SampleNamespace
    {
        class SampleClass
        {
            public void SampleMethod()
            {
                System.Console.WriteLine("SampleNamespace");
            }
        }
    }

    ★ 命名空间嵌套

    实际上命名空间是用来表示一个范围的。在项目中创建范围的能力有助于组织代码,并提供创建全局唯一的类型的途径。

    示例

    示例中名为 myClass 的类在两个命名空间中定义,其中一个命名空间嵌套在另一个之内。. 运算符用于区分所调用的方法。

    namespace N1
    {
        class myClass
        {
            public void SampleMethod()
            {
                System.Console.WriteLine("SampleMethod1");
            }
        }

        namespace N2
        {
            class myClass
            {
                public void SampleMethod()
                {
                    System.Console.WriteLine("SampleMethod2");
                }
            }
        }

        class Mainclass
        {
            static void Main(string[] args)
            {
                myClass uter = new myClass();
                outer.SampleMethod();
                N1.myClass outer2 =new myClass();
                outer2.SampleMethod();
                N2.myClass inner = new N2.myClass();
                inner.SampleMethod();

            }
        }
    }

    输出
    SampleMethod1
    SampleMethod1
    SampleMethod2

    ★ 完全限定名

    命名空间和类型的名称必须唯一,由指示逻辑层次结构的完全限定名描述。例如,语句 A.B 表示 A 是命名空间或类型的名称,而 B 则嵌套在其中。

    下面的示例中有嵌套的类和命名空间。在每个实体的后面,需要完全限定名作为注释。

    namespace N1     // N1
    {
        class C1      // N1.C1
        {
            class C2   // N1.C1.C2
            {
            }
        }
        namespace N2  // N1.N2
        {
            class C2   // N1.N2.C2
            {
            }
        }
    }

    在以上代码段中要注意:

    1 命名空间 N1 是全局命名空间的成员。它的完全限定名是 N1。

    2 命名空间 N2 是命名空间 N1 的成员。它的完全限定名是 N1.N2。

    3 类 C1 是 N1 的成员。它的完全限定名是 N1.C1。

    在此代码中使用了两次 C2 类名。但是,完全限定名是唯一的。第一个类名在 C1 内声明;因此其完全限定名是:N1.C1.C2。第二个类名在命名空间 N2 内声明;因此其完全限定名是:N1.N2.C2。

    使用以上代码段,可以用以下方法将新的类成员 C3, 添加到命名空间 N1.N2 内:

    namespace N1.N2
    {
        class C3   // N1.N2.C3
        {
        }
    }

    ★ 隐藏全局命名空间

    位于.NET Framwork根部的命名空间都是全局命名空间,当命名空间中的成员与全局命名空间同名时,就会隐藏全局命名空间的成员。

    示例

    在下面的代码中,Console 在 System 命名空间中解析为 TestApp.Console 而不是 Console 类型。

    using System;
    class TestApp
    {
        public class System { }
        const int Console = 7;
        const int number = 66;
        static void Main()
        {
          
            //System.Console.WriteLine(number);
        }
    }

    由于类 TestApp.System 隐藏了 System 命名空间,因此使用 System.Console 会导致错误。

    ★ 引用全局命名空间

    要将隐藏的全局命名空间显现出来,可以使用global:: 来引用全局命名空间。

    示例

    using System;
    class TestApp
    {
        public class System { }
        const int Console = 7;
        const int number = 66;
        static void Main()
        {
            global::System.Console.WriteLine(number);
        }
    }

    示例

    使用::也可以引用命名空间别名

    using Alias = System;
    class TestApp
    {
        public class System { }
        const int Console = 7;
        const int number = 66;
        static void Main()
        {
            Alias.Console.WriteLine(number);
            Alias::Console.WriteLine(number);
        }
    }

    ★ 命名空间具有以下属性:

    1 组织大型代码项目。

    2 以 . 运算符分隔。

    3 using directive 意味着不需要为每个类指定命名空间的名称。

    4 global 命名空间是“根”命名空间:global::system 始终引用 .NET Framework 命名空间 System。

  • C#静态类

    2010-02-25 15:18:17

    静态类与非静态类基本相同,但存在一个区别:静态类不能实例化。也就是说,不能使用 new 关键字创建静态类类型的变量。

    因为没有实例变量,所以要使用类名本身访问静态类的成员。

    例:

    static class CompanyInfo

    {

       public static string GetCompanyName() { return "CompanyName"; }

        public static string GetCompanyAddress() { return "CompanyAddress"; }

        //...

    }

    和所有类类型一样,当加载引用静态类的程序时,.NET Framework 公共语言运行时 (CLR) 将加载该静态类的类型信息。程序不能指定加载静态类的确切时间。但是,可以保证在程序中首次引用该类前加载该类,并初始化该类的字段并调用其静态构造函数。静态构造函数仅调用一次,在程序驻留的应用程序域的生存期内,静态类一直保留在内存中。

    对于只对输入参数进行运算而不获取或设置任何内部实例字段的方法集,静态类可以方便地用作这些方法集的容器。例如,在 .NET Framework 类库中,静态类 System.Math 包含的一些方法只执行数学运算,而无需存储或检索特定 Math 类实例特有的数据。

     

    静态类的主要特性:

    仅包含静态成员。

    无法实例化。

    是密封的。

    不能包含实例构造函数。

    创建静态类(A)与创建仅包含静态成员和私有构造函数的类(B)基本相同,私有构造函数阻止类被实例化。出现B这种情况易选用A。

    使用静态类的优点在于,编译器能够执行检查以确保不致偶然地添加实例成员。编译器将保证不会创建此类的实利。

    静态类是密封的,因此不可被继承。它们不能从除 Object 外的任何类中继承。静态类不能包含实例构造函数,但可以包含静态构造函数。如果非静态类包含需要进行重要的初始化的静态成员,也应定义静态构造函数。

        下面是一个静态类的示例,它包含两个在摄氏温度和华氏温度之间执行来回转换的方法:

    public static class TemperatureConverter

    {

        public static double CelsiusToFahrenheit(string temperatureCelsius)

        {

            // Convert argument to double for calculations.

            double celsius = Double.Parse(temperatureCelsius);

            // Convert Celsius to Fahrenheit.

            double fahrenheit = (celsius * 9 / 5) + 32;

            return fahrenheit;

        }

        public static double FahrenheitToCelsius(string temperatureFahrenheit)

        {

            // Convert argument to double for calculations.

            double fahrenheit = Double.Parse(temperatureFahrenheit);

            // Convert Fahrenheit to Celsius.

            double celsius = (fahrenheit - 32) * 5 / 9;

            return celsius;

        }

    }

    class TestTemperatureConverter

    {

        static void Main()

        {

            Console.WriteLine("Please select the convertor direction");

            Console.WriteLine("1. From Celsius to Fahrenheit.");

            Console.WriteLine("2. From Fahrenheit to Celsius.");

            Console.Write(":");

            string selection = Console.ReadLine();

            double F, C = 0;

            switch (selection)

            {

                case "1":

                    Console.Write("Please enter the Celsius temperature: ");

                    F = TemperatureConverter.CelsiusToFahrenheit(Console.ReadLine());

                    Console.WriteLine("Temperature in Fahrenheit: {0:F2}", F);

                    break;

                case "2":

                    Console.Write("Please enter the Fahrenheit temperature: ");

                    C = TemperatureConverter.FahrenheitToCelsius(Console.ReadLine());

                    Console.WriteLine("Temperature in Celsius: {0:F2}", C);

                    break;

                default:

                    Console.WriteLine("Please select a convertor.");

                    break;

            }

            // Keep the console window open in debug mode.

            Console.WriteLine("Press any key to exit.");

            Console.ReadKey();

        }

    }

    /* Example Output:

        Please select the convertor direction

        1. From Celsius to Fahrenheit.

        2. From Fahrenheit to Celsius.

        :2

        Please enter the Fahrenheit temperature: 20

        Temperature in Celsius: -6.67

        Press any key to exit.

     */

     

    静态成员

    非静态类可以包含静态的方法、字段、属性或事件。即使没有创建类的实例,也可以调用该类中的静态成员。始终通过类名而不是实例名称访问静态成员。

    无论对一个类创建多少个实例,它的静态成员都只有一个副本。

    静态方法和属性不能访问其包含类型中的非静态字段和事件,并且不能访问任何对象的实例变量(除非在方法参数中显式传递)。

     

    更常见的做法是声明具有一些静态成员的非静态类,而不是将整个类声明为静态类。静态字段有两个常见的用法:一是记录已实例化对象的个数,二是存储必须在所有实例之间共享的值。

    静态方法可以被重载但不能被重写,因为它们属于类,不属于类的任何实例。

    虽然字段不能声明为 static const,但 const 字段的行为在本质上是静态的。这样的字段属于类型,不属于类型的实例。因此,可以同对待静态字段一样使用 ClassName.MemberName 表示法来访问 const 字段。不需要对象实例。

    C# 不支持静态局部变量(在方法范围内声明的变量)。

    通过在成员的返回类型之前使用 static 关键字可以声明静态类成员,如下面的示例所示:

    public class Automobile

    {

        public static int NumberOfWheels = 4;

        public static int SizeOfGasTank

        {

            get

            {

                return 15;

            }

        }

        public static void Drive() { }

        public static event EventType RunOutOfGas;

        // Other non-static fields and properties...

    }

     

    静态成员在第一次被访问之前并且在调用静态构造函数(如有存在)之前进行初始化。若要访问静态类成员,应使用类名而不是变量名来指定该成员的位置,如下面的示例所示:

    Automobile.Drive();

    int i = Automobile.NumberOfWheels;

    如果类包含静态字段,请提供在加载类时初始化这些字段的静态构造函数。

    对静态方法的调用以 Microsoft 中间语言 (MSIL) 生成调用指令,而对实例方法的调用生成 callvirt 指令,该指令还检查 null 对象引用。但是,两者之间的性能差异在大多数时候并不明显。

  • c#其他结构中变量的作用域

    2010-02-25 14:21:39

    变量的作用域包含定义它们的代码块和直接嵌套在其中的代码块。这也可以应用到其他代码块上,例如分支和循环结构的代码块。考虑下面的代码:
    int i;
    for (i = 0; i < 10; i++)
    {
       string text = "Line " + Convert.ToString(i);
       Console.WriteLine("{0}", text);
    }
    Console.WriteLine("Last text output in loop: {0}", text);
    

    字符串变量text是for循环的局部变量,这段代码不能编译,因为在该循环外部调用的Console.WriteLine()试图使用该变量text,这超出了循环的作用域。修改代码,如下所示:

    int i;
    string text;
    for (i = 0; i < 10; i++)
    {
       text = "Line " + Convert.ToString(i);
       Console.WriteLine("{0}", text);
    }
    Console.WriteLine("Last text output in loop: {0}", text);
    

    这段代码也会失败,原因是变量必须在使用前声明和初始化,而text是在for循环中初始化的。赋给text的值在循环块退出时就丢失了。但是还可以进行如下修改:

    int i;
    string text = "";
    for (i = 0; i < 10; i++)
    {
       text = "Line " + Convert.ToString(i);
       Console.WriteLine("{0}", text);
    }
    Console.WriteLine("Last text output in loop: {0}", text);
    

    这次text是在循环外部初始化的,可以访问它的值。这段简单代码的结果如图6-6所示。

    图 6-6

    在循环中最后赋给text的值可以在循环外部访问。

    可以看出,这个主题的内容需要花一点时间来掌握。在前面的示例中,循环之前赋给text空字符串,而在循环之后的代码中,该text就不会是空字符串了,其原因不能立即看出。

    这种情况的解释涉及到分配给text变量的内存空间,实际上任何变量都是这样。只声明一个简单的变量类型,并不会引起其他的变化。只有在给变量赋值后,这个值才占用一块内存空间。如果这种占据内存空间的行为在循环中发生,该值实际上定义为一个局部值,在循环的外部会超出了其作用域。

    即使变量本身没有局部化到循环上,循环所包含的值也局部化到该循环上。但是,在循环外部赋值可以确保该值是主体代码的局部值,在循环内部它仍处于其作用域中。这意味着变量在退出主体代码块之前是没有超出作用域的,所以可以在循环外部访问它的值。

    幸而,C#编译器可检测变量作用域的问题,它生成的响应错误信息可以帮助我们理解变量作用域的问题。

    最后一个要注意的问题是,应采用“最佳实践”。一般情况下,最好在声明和初始化所有的变量后,再在代码块中使用它们。一个例外是把循环变量声明为循环块的一部分,例如:

    for (int i = 0; i < 10; i++)
    {
       ...
    }
    

    其中i局部化于循环代码块中,但这是可以的,因为我们很少需要在外部代码中访问这个计数器。

  • c#: 域(field)

    2010-02-24 22:31:18

    域(field)

    -域表示与对象或类相关联的变量。

    -域的声明中如果加上了readonly修饰符,表明该域为只读域。对于只读域我们只能在域的定义中和它所属类的构造函数中进行修改。在其他情况下,域是“只读”的。

    static readonly的作用和#defineconst的作用类似。区别是:const型表达式的值是在编译时形成的,而static readonly表达式的值直到程序运行时才形成。如:

    public class A

    {

    public static readonly int X = 1;

    }

    C/C++中未经初始化的变量是不能使用的。在C#中,系统将为每个未经初始化的变量提供一个默认值。对于所有引用类型的变量,默认值是null。所有值类型的变量的默认值是固定的。对于静态域,类在装载时对其进行初始化;对于非静态域,在类的实例创建时进行初始化。在默认的初始化之前,域的值是不可预测的。

    例如下面的代码是合法的:

    class Test

    {

           static int a = b+ 1;

           static int b = a+ 1;

    }

    实际上等价于:a = 1; b = 2;

    而下面的代码则是非法的:

    class A

    {

           int x = 1;

           int y = x + 1;

    }

    因为非静态变量x在类A实例化以前并没有初始化,代码y = x + 1无法得到正确的x的值。变量在类里只能定义。

  • C Sharp中类的私有构造函数作用

    2010-02-24 17:15:01

    如果类成员有private修饰符,就不允许在类范围以外访问这个类成员。对类构造函数应用private修饰符时,则禁止外部类创建该类的实例。尽管看上去有些不好理解(既然不能实例化,那么这个类还有什么用处?),但实际上这是一个功能极其强大的特性。
    最明显的是,如果类只通过静态方法和字段来提供功能,那么就常常使用私有构造函数。框架类库FCL中的System.Math类就是一个很经典的例子。
    System.Math类有两个静态字段:pi和e(自然对数底数),还有一些返回三角函数值的方法。这些方法都作为内置函数,所以,程序没有必要为使用这些字段和方法而创建Math类的实例。
    在前面关于静态方法的讨论中,我们给出了一个完成数制转换的类(见代码清单3ˉ5)。这里,再在该类中增加了一个私有构造函数,如代码清单3ˉ10所示。
    代码清单3ˉ10 在包含静态方法的类中使用私有构造函数
     
    虽然这是一个简单的例子,但它展示了一个不需要实例化的类:其方法是静态的,并且没有与类实例相关的状态信息。
    现在可能会很自然地出现这样一个问题:要避免实例化,使用私有构造函数好呢,还是使用抽象类更好一些?答案在于要理解这二者的区别。首先来考虑继承,虽然抽象类不能实例化,但其真正的目的是用于作为基类,以便派生类(可实例化)创建自己的实现。使用私有构造函数的类不会被继承,而且也不能被继承。其次,私有构造函数只能禁止外部类对该类进行实例化,却不能禁止在该类内部创建实例。
    私有构造函数的特性也可以用于管理对象的创建。虽然私有构造函数不允许外部方法实例化这个类,但却允许此类中的公共方法(有时也称为工厂方法,factory method)创建对象。也就是说,类可以创建自身的实例、控制外界对它的访问,以及控制创建的实例个数

    注:本文来自51CTO.COM

  • C# : const和static readonly的差别

    2010-02-24 16:38:26

    我们都知道,const和static readonly的确非常像:通过类名而不是对象名进行访问,在程式中只读等等。在多数情况下能混用。
    二者本质的差别在于,const的值是在编译期间确定的,因此只能在声明时通过常量表达式指定其值。而static readonly是在运行时计算出其值的,所以还能通过静态构造函数来赋值。
    明白了这个本质差别,我们就不难看出下面的语句中static readonly和const能否互换了:
    1. static readonly MyClass myins = new MyClass();
    2. static readonly MyClass myins = null;
    3. static readonly A = B * 20;
       static readonly B = 10;
    4. static readonly int [] constIntArray = new int[] {1, 2, 3};
    5. void SomeFunction()
       {
          const int a = 10;
          ...
       }

    1:不能换成const。new操作符是需要执行构造函数的,所以无法在编译期间确定
    2:能换成const。我们也看到,Reference类型的常量(除了String)只能是Null。
    3:能换成const。我们能在编译期间非常明确的说,A等于200。
    4:不能换成const。道理和1是相同的,虽然看起来1,2,3的数组的确就是个常量。
    5:不能换成readonly,readonly只能用来修饰类的field,不能修饰局部变量,也不能修饰property等其他类成员。

    因此,对于那些本质上应该是常量,不过却无法使用const来声明的地方,能使用static readonly。例如C#规范中给出的例子:


    public class Color
    {
        public static readonly Color Black = new Color(0, 0, 0);
        public static readonly Color White = new Color(255, 255, 255);
        public static readonly Color Red = new Color(255, 0, 0);
        public static readonly Color Green = new Color(0, 255, 0);
        public static readonly Color Blue = new Color(0, 0, 255);

        private byte red, green, blue;

        public Color(byte r, byte g, byte b)
        {
            red = r;
            green = g;
            blue = b;
        }
    }


    static readonly需要注意的一个问题是,对于一个static readonly的Reference类型,只是被限定不能进行赋值(写)操作而已。而对其成员的读写仍然是不受限制的。
    public static readonly MyClass myins = new MyClass();

    myins.SomeProperty = 10;  //正常
    myins = new MyClass();    //出错,该对象是只读的

    不过,如果上例中的MyClass不是个class而是个struct,那么后面的两个语句就都会出错。

  • c#编程第七章 类与结构

    2010-02-24 16:04:21

    第七章 类与结构

    C# 是面向对象的编程语言,它使用类和结构来实现类型(如 Windows 窗体、用户界面控件和数据结构等)。典型的 C# 应用程序由程序员定义的类和 .NET Framework 的类组成。

    C# 提供了许多功能强大的类定义方式,例如,提供不同的访问级别,从其他类继承功能,允许程序员指定实例化的操作。

    ===========================
    一 类声明
    ===========================

    类是 C# 中功能最为强大的数据类型。像结构一样,类也定义了数据类型的数据和行为。然后,程序员可以创建作为此类的实例的对象。与结构不同,类支持继承,而继承是面向对象编程的基础部分。

    1 声明类

    类是使用 class 关键字来定义的,如下面的示例所示:

    public class Customer
    {
       private int x=10, y=15;
       public int ride()
        {
           return x*y;
        }
    }
    class mainclass
    {
    static void Main()
    {
    Customer s=new Customer();
    System.Console.WriteLine(s.ride());
    }
    }

    说明:
    class 关键字前面是访问修饰符。在该例中,使用了 public,这表示任何人都可以基于该类创建对象。类的名称位于 class 关键字的后面。一对花括号中的内容叫类体,用于定义行为和数据。类的字段、属性、方法和事件统称为“类成员”。

    常用的类访问修饰符如下所示:

    1 public
    public关键字是类型和类型成员的访问修饰符。公共访问是允许的最高访问级别。对访问公共成员没有限制。

    2 private

    private 关键字是一个成员访问修饰符。私有访问是允许的最低访问级别。私有成员只有在声明它们的类和结构体中才是可访问的

    3 protected

    protected 关键字是一个成员访问修饰符。受保护成员在它的类中可访问并且可由派生类访问

    4 internal
    internal 关键字是类型和类型成员的访问修饰符。只有在同一程序集的文件中,内部类型或成员才是可访问的。

    ===========================
    二 结构
    ===========================
    结构是使用 struct 关键字定义的,struct 类型是一种值类型,通常用来封装小型相关变量组,例如,矩形的坐标或库存商品的特征。下面是结构的声明语法。

    例如:
     
    public struct PostalAddress
    {
       ......
    }

    结构与类共享几乎所有相同的语法,结构适于表示 Point, Color 等轻量对象。尽管这些也可以表示为类,但在某些情况下,使用结构更有效。结构有如下特点:

    1 结构是值类型,而类是引用类型。但通过一个称为装箱的过程可以将值类型转换为引用类型。

    2 向方法传递结构时,结构是通过传值方式传递的,而不是作为引用传递的。

    3 与类不同,结构的实例化可以不使用 new 运算符。

    4 结构不能声明默认构造函数或析构函数。结构可以声明带有参数的构造函数。

    5 一个结构不能从另一个结构或类继承,而且不能作为一个类的基。

    6 所有结构都直接继承自 System.ValueType,后者继承自System.Object。

    7 结构可以实现接口。

    8 在结构中初始化实例字段是错误的。

    ★结构示例1

    public struct CoOrds
    {
        public int x, y;

        public CoOrds(int p1, int p2)
        {
            x = p1;
            y = p2;
        }
    }

    class TestCoOrds
    {
        static void Main()
        {
            CoOrds coords1 = new CoOrds();
            CoOrds coords2 = new CoOrds(10, 10);
            System.Console.Write("CoOrds 1: ");
            System.Console.WriteLine("x = {0}, y = {1}",coords1.x, coords1.y);
            System.Console.Write("CoOrds 2: ");
            System.Console.WriteLine("x = {0}, y = {1}",coords2.x, coords2.y);
        }
    }

    ★结构示例2

    本示例说明了结构特有的一种功能。它在不使用 new 运算符的情况下创建 CoOrds 对象。如果将 struct 换成 class,程序将不会编译。

    using System;
    public struct CoOrds
    {
        public int x, y;
        public CoOrds(int p1, int p2)
        {
            x = p1;
            y = p2;
        }
    }

    class TestCoOrdsNoNew
    {
        static void Main()
        {
            CoOrds coords1;
            coords1.x = 10;
            coords1.y = 20;
            Console.Write("CoOrds 1: ");
            Console.WriteLine("x={0},y={1}", coords1.x, coords1.y);
        }
    }

    ★结构示例3

    public struct CoOrds
    {
        public string x;

        public CoOrds(string p)
        {
            x = p;
        }
    }

    class TestCoOrds
    {
        static void Main()
        {
            CoOrds a = new CoOrds("ssssss");
            System.Console.Write(a.x);
           
        }
    }

    ===========================
    三 对象
    ===========================

    对象是具有数据、行为和标识的编程结构。对象数据包含在对象的字段、属性和事件中,对象行为则由对象的方法和接口定义。对象具有以下特点:

    1 C# 中使用的全都是对象,包括 Windows 窗体和控件。

    2 对象是实例化的;对象是从类和结构所定义的模板中创建的。

    3 对象使用属性获取和更改它们所包含的信息。

    4 对象通常具有允许它们执行操作的方法和事件。

    5 所有 C# 对象都继承自 Object。

    6 使用“属性”窗口可以更改对象的属性。

    7 使用对象浏览器可以检查对象的内容。

    ★ 对象的创建

    尽管有时类和对象可互换,但它们是不同的概念。类定义对象的类型,但它不是对象本身。对象是基于类的具体实体,有时称为类的实例。创建对象的语法如下:

    Customer object1 = new Customer();

    创建类的实例后,将向程序员传递回对该对象的引用。在上例中,object1 是对基于 Customer 的对象的引用。此引用引用新对象,但不包含对象数据本身。实际上,有时可以在根本不创建对象的情况下创建对象引用,如下所示:
     
    Customer object2;

    但是对于类而言建议不要创建像这样的不引用对象的对象引用,因为在运行时通过这样的引用来访问对象的尝试将会失败。可以如下所示创建这样的引用来引用对象,方法是创建新对象,或者将它分配给现有的对象:

    Customer object3 = new Customer();
    Customer object4 = object3;

    此代码创建了两个对象引用,它们引用同一个对象。因此,通过 object3 对对象所做的任何更改都将反映在随后使用的 object4 中。这是因为基于类的对象是按引用来引用的,因此类称为引用类型。

    ===========================
    四 类的成员
    ===========================
    类和结构具有表示其数据和行为的成员。这些成员包括:字段,属性,方法,事件,索引器,构造函数,析构函数等,下面我们就学习在类中定义这些成员的方法。

    ------------------------
    1 在类中定义字段
    ------------------------
    字段是包含在类或结构中的对象或值。字段存储类要满足其设计所需要的数据。

    示例

    示例中定义了一个表示日历日期的类,它有三个整数字段:一个表示月份,一个表示日期,还有一个表示年份。在类块中声明字段时指定了字段的访问级别,然后指定字段的类型,最后指定字段的名称。对于访问级别的指定,本来应该使用private,但为了便于测试这里使用了public,在实际应用时建议不要这样做,而应该通过方法,属性等间接访问字段。

    using System;
    public class CalendarDate
    {
        public int month = 6;
        public int day = 1;
        public int year = 2006;
    }

    class myWrite
    {

        public static void Main()
        {
            CalendarDate birthday = new CalendarDate();
            birthday.month = 7;
            Console.Write(birthday.year);
            Console.Write(".");
            Console.Write(birthday.month);
            Console.Write(".");
            Console.WriteLine(birthday.day);
        }
    }

    示例解读:

    1 首先创建一个表示日历日期的类CalendarDate,在该类中定义了三个字段month,day和year。在定义字段时使用了字段修饰符,并且在对字段声明时进行了初始化。

    2 定义一个用来输出数据的类myWrite,在其Main()方法中调用CalendarDate类的构造函数创建一个对象birthday,并在该对象上重新初始化了字段month。最后使用输出语句将各个字段输出。

    3 本例定义的字段均为对象实体字段,每个对象都会从类中拷贝属于自己的一份字段,其值可以各自不同,如语句birthday.month = 7;只能改变自身的值,如果字段前面添加修饰符static,则该字段为类字段(静态字段),类字段为该类中所有对象实例所共同拥有,无论你创建多少对象实例,属于类的字段只有一份。

    ------------------------
    2 在类中定义常量
    ------------------------
    类和结构可以将常数声明为成员。常数是在编译时已知并保持不变的值。(若要创建在运行时初始化的常数值,请使用 readonly 关键字。)常数被声明为字段,声明时在字段的类型前面使用 const 关键字。常数必须在声明时初始化。

    using System;
    public class myconst
    {
        public const double x = 1;
        public const double y = 2;
    }
    class myWrite
    {
        public static void Main()
        {
            Console.WriteLine(myconst.x);
            Console.WriteLine(myconst.y);

        }
    }

    说明:在类中定义的常量都隐含着静态属性static。

    ------------------------
    3 在类中定义方法
    ------------------------
    方法是包含一系列语句的代码块。在 C# 中,每个执行指令都是在方法的上下文中完成的。方法在类或结构中声明,声明时需要指定访问级别、返回值、方法名称以及任何方法参数。方法参数放在括号中,并用逗号隔开。空括号表示方法不需要参数。

    示例

    调用对象的方法类似于访问字段。在对象名称之后,依次添加句点、方法名称和括号。参数在括号内列出,并用逗号隔开。

    using System;
    class rounder
    {
        public const double pi = 3.14;
        public double radius;
        public static int getpi()
        {
            return (int)pi;
        }
        public void setradius(double r)
        {
            radius = r;
        }
        public double area()
        {
            return pi * radius * radius;
        }

    }
    class myWrite
    {
        public static void Main()
        {
            rounder circle = new rounder();
            Console.WriteLine("{0}", rounder.getpi());
            circle.setradius(10);
            Console.WriteLine("{0}", circle.area());

        }
    }

    ------------------------
    4 在类中定义指针
    ------------------------
    指针是一种低级的数据类型,他存储的是某一数据的内存地址,该地址中存放的可能是任何一种数据类型,利用指针可以直接访问某一内存单元,也可以进行内存动态分配。指针属于不安全的类型,定义语法如下:

    语法:

    类型 *
    void *

    void关键字表示没有类型,在使用指针类型时必须使用关键字unsafe标明其属于不安全类型。

    示例

    //ss.cs
    using System;
    class inttest
    {
        public unsafe static void Main()
        {
             int i=5;
             int* s=&i;
             Console.WriteLine("指针十进制值={0}",(uint)s);
        }
    }

    编译
    将文件放在d盘根目录中则
    d\>:csc /unsafe ss.cs

    运行
    d\>:ss
    指针十进制值=1242232

    示例解读:

    因为指针属于不安全类型,在编译时必须前面加/unsafe  
    式子int* s=&i;中,int*表示声明一个整形指针变量s,&表示指向整形变量i的地址。

    ------------------------
    5 在类中定义属性
    ------------------------
    属性是类中可以像类中的字段一样访问的方法。属性可以为类字段提供保护,避免字段在对象不知道的情况下被更改。属性提供灵活的机制来读取、编写或计算私有字段的值。可以像使用公共数据成员一样使用属性,但实际上它们是称为“访问器”的特殊方法。这使得数据在可被轻松访问的同时,仍能提供方法的安全性和灵活性。

    示例

    在本示例中,类 TimePeriod 存储了一个时间段。类内部以秒为单位存储时间,但提供一个称为 Hours 的属性,它允许客户端指定以小时为单位的时间。Hours 属性的访问器执行小时和秒之间的转换。注意其中的value是一个关键字。

    class TimePeriod
    {
        private double seconds;
        public double Hours
        {
            get { return seconds / 3600; }
            set { seconds = value * 3600; }
        }
    }

    class Program
    {
        static void Main()
        {
            TimePeriod t = new TimePeriod();
            t.Hours = 24;
            System.Console.WriteLine("Time in hours: " + t.Hours);
        }
    }

    ------------------------
    6 构造函数
    ------------------------
    构造函数是在创建给定类型的对象时执行的类方法。构造函数具有与类相同的名称,它通常初始化新对象的数据成员。

    ★使用构造函数

    在下面的示例中,定义了一个具有一个简单的构造函数,名为 Taxi 的类。然后使用 new 运算符来实例化该类。在为新对象分配内存之后,new 运算符立即调用 Taxi 构造函数。

    示例

    public class Taxi
    {
        public bool isInitialized;
        public Taxi()
        {
            isInitialized = true;
        }
    }

    class TestTaxi
    {
        static void Main()
        {
            Taxi t = new Taxi();
            System.Console.WriteLine(t.isInitialized);
        }
    }

    不带参数的构造函数称为“默认构造函数”。无论何时,只要使用 new 运算符实例化对象,并且不为 new 提供任何参数,就会调用默认构造函数。除非类是 static 的,否则 C# 编译器将为无构造函数的类提供一个公共的默认构造函数,以便该类可以实例化。

    ★私有构造函数

    私有构造函数是一种特殊的实例构造函数。它通常用在只包含静态成员的类中。如果类具有一个或多个私有构造函数而没有公共构造函数,则不允许其他类(除了嵌套类)创建该类的实例。

    示例

    public class Counter
    {
        private Counter() { }
        public static int VCount;
        public static int MCount()
        {
            return ++VCount;
        }
    }

    class TestCounter
    {
        static void Main()
        {
            // Counter aCounter = new Counter();// Error
            Counter.VCount = 100;
            Counter.MCount();
            System.Console.WriteLine("{0}", Counter.VCount);
            //int s=Counter.MCount();
            //System.Console.WriteLine("{0}", s);
        }
    }

    声明空构造函数可阻止自动生成默认构造函数。注意,如果您不对构造函数使用访问修饰符,则在默认情况下它仍为私有构造函数。但是,通常显式地使用 private 修饰符来清楚地表明该类不能被实例化。

    ★静态构造函数

    静态构造函数用于初始化任何静态数据,或用于执行仅需执行一次的特定操作。在创建第一个实例或引用任何静态成员之前,将自动调用静态构造函数。静态构造函数具有以下特点:

    ○静态构造函数既没有访问修饰符,也没有参数。

    ○在创建第一个实例或引用任何静态成员之前,将自动调用静态构造函数来初始化类。

    ○无法直接调用静态构造函数。

    ○在程序中,用户无法控制何时执行静态构造函数。

    示例

    在此示例中,类 Bus 有一个静态构造函数和一个静态成员 Drive()。当调用 Drive() 时,将调用静态构造函数来初始化类。

    public class Bus
    {
        static Bus()
        {
            System.Console.WriteLine("The static constructor invoked.");
        }

        public static void Drive()
        {
            System.Console.WriteLine("The Drive method invoked.");
        }
    }

    class TestBus
    {
        static void Main()
        {
            Bus.Drive();
        }
    }

    ★复制构造函数

    与有些语言不同,C# 不提供复制构造函数。如果您创建了新的对象并希望从现有对象复制值,您必须自行编写适当的方法。

    示例

    在本示例中,Person类包含一个构造函数,该构造函数接受另一个 Person 类型的对象作为参数。然后此对象的字段中的内容将分配给新对象中的字段。

    class Person
    {
        private string name;
        private int age;
        public Person(Person previousPerson)
        {
            name = previousPerson.name;
            age = previousPerson.age;
        }
        public Person(string name, int age)
        {
            //如果去掉this则参数名就不能和字段名相同
            this.name = name;
            this.age = age;
        }
        public string Details
        {
            get
            {
                return name + " is " + age.ToString();
            }
        }
    }

    class TestPerson
    {
        static void Main()
        {
            Person person1 = new Person("George", 40);
            Person person2 = new Person(person1);
            System.Console.WriteLine(person2.Details);
        }
    }

    ------------------------
    7 析构函数
    ------------------------
    析构函数用于析构类的实例。不能在结构中定义析构函数。只能对类使用析构函数。析构函数有如下特点:

    ○ 一个类只能有一个析构函数。

    ○ 无法继承或重载析构函数。

    ○ 无法调用析构函数。它们是被自动调用的。

    ○ 析构函数既没有修饰符,也没有参数。

    ○ 声明析构函数时前面加一个“~”

    示例:

    下面的示例创建三个类,这三个类构成了一个继承链。类 First 是基类,Second 是从 First 派生的,而 Third 是从 Second 派生的。这三个类都有析构函数。在 Main() 中,创建了派生程度最大的类的实例。注意:程序运行时,这三个类的析构函数将自动被调用,并且是按照从派生程度最大的到派生程度最小的次序调用。

    class First
    {
        ~First()
        {
            System.Console.WriteLine("First析构函数");
        }
    }

    class Second: First
    {
        ~Second()
        {
            System.Console.WriteLine("Second析构函数");
        }
    }

    class Third: Second
    {
        ~Third()
        {
            System.Console.WriteLine("Third析构函数");
        }
    }

    class TestDestructors
    {
        static void Main()
        {
            Third t = new Third();
        }
    }

    ------------------------
    8 索引器
    ------------------------
    索引器允许类或结构的实例按照与数组相同的方式进行索引。索引器类似于属性,不同之处在于它们的访问器采用参数。要声明类或结构上的索引器,请使用 this 关键字,如下例所示:

    public int this[int index]
    {
        get {...}
        set {...}
    }

    示例 1

    using System;
    class IndexerClass
    {
        private int[] arr = new int[100];
        public int this[int index]
        {
            get
            {
                if (index < 0 || index >= 100)
                {
                    return 0;
                }
                else
                {
                    return arr[index];
                }
            }
            set
            {
                if (!(index < 0 || index >= 100))
                {
                    arr[index] = value;
                }
            }
        }
    }

    class MainClass
    {
        static void Main()
        {
            IndexerClass test = new IndexerClass();
            test[3] = 256;
            test[5] = 1024;
            for (int i = 0; i <= 10; i++)
            {
             Console.WriteLine("Element #{0} = {1}", i, test[i]);
            }
        }
    }

    Element #0 = 0
    Element #1 = 0
    Element #2 = 0
    Element #3 = 256
    Element #4 = 0
    Element #5 = 1024
    Element #6 = 0
    Element #7 = 0
    Element #8 = 0
    Element #9 = 0
    Element #10 = 0
    请按任意键继续. . .

    示例解读:

    个人理解:
    有了索引可以把类用作数组去操作!

    在类中定义了一个数组字段,然后定义了一个索引器,如下

    public int this[int index]{...}

    其中
    1 public为修饰符,还可以使用private等等。
    2 int 指定索引类型。
    3 this[int index]为索引声明,this表示当前对象,并且作为方括号的依附对象,方括号内容为索引值。
    4 {...}为访问声明,写入一些可运行的语句,用于设置读取元素的值。

    示例说明了如何声明私有数组字段arr 和索引器。使用索引器可直接访问实例 test[i]。另一种使用索引器的方法是将数组声明为 public 成员并直接访问它的成员 arr[i]。

    示例 2

    在此例中,声明了存储星期几的类。声明了一个 get 访问器,它接受字符串(天名称),并返回相应的整数。例如,星期日将返回 0,星期一将返回 1,等等。

    class DayCollection
    {
        string[] days = { "Sun", "Mon", "Tues", "Wed", "Thurs", "Fri", "Sat" };
    //这个方法用于输出准确的日期或-1
        private int GetDay(string testDay)
        {
            int i = 0;
            //枚举数组对象days中的元素保存到day中
            foreach (string day in days)
            {
                //比较day中某个元素是否和testDay相等
                if (day == testDay)
                {
                    return i;
                }
                i++;
            }
            return -1;
        }

    // 在索引器中附加get方法用于获取字符串
        public int this[string day]
        {
            get
            {
                return (GetDay(day));
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            DayCollection week = new DayCollection();
            System.Console.WriteLine(week["Fri"]);
            System.Console.WriteLine(week["Made-up Day"]);
        }
    }

    5
    -1
    请按任意键继续. . .
    ===========================
    五 装箱和取消装箱
    ===========================
    装箱和取消装箱使值类型能够被视为对象。对值类型装箱将把该值类型打包到 Object 引用类型的一个实例中。这使得值类型可以存储于垃圾回收堆中。取消装箱将从对象中提取值类型。

    示例1

    //整型变量 i 被“装箱”并赋值给对象 o。
    int i = 123;
    object o = (object) i;  // boxing
    //对象 o 取消装箱并将其赋值给整型变量 i:
    o = 123;
    i = (int) o;  // unboxing

    装箱和取消装箱都是需要大量运算的过程。对值类型进行装箱时,必须创建一个全新的对象。此操作所需时间可比赋值操作长 20 倍。取消装箱时,强制转换过程所需时间可达赋值操作的四倍。

    示例2

    public class myStack
    {
        object[] array;
        public void Push(object t)
      {
          array = new object[] {t};
      }
        public object Pop()
      {
          return array[0];
      }
    }

    class Mainclass
    {
        static void Main()
        {
            myStack stack = new myStack();
            stack.Push(100);
            int number = (int)stack.Pop();
            System.Console.WriteLine(number);
        }
    }

    示例解读

    在实例stack上调用Push方法时,我们给他传递的参数是一个数字,注意到在类定义时,该参数应该是一个object类型的数据,这时程序会对数字100进行装箱操作,将其转换为object类型。同样的道理在调用Pop方法时程序会进行取消装箱操作。因为装箱和取消装箱操作需要大量运算的过程,因此系统性能会降低。

  • c#编程第八章 继承

    2010-02-24 15:55:24

    第八章 继承

    ==============================
    一 继承
    ==============================
    类可以从其他类中继承。这是通过以下方式实现的:在声明类时,在类名称后放置一个冒号,然后在冒号后指定要从中继承的类(即基类),新声明的类叫派生类,它能够获取基类的所有非私有数据和行为以及新类为自己定义的所有其他数据或行为。因此,新类具有两个有效类型:新类的类型和它继承的类的类型。派生类的定义语法如下所示:

    //基类有时也叫超类
    public class A
    {
        public A() { }
    }
    //派生类有时也叫子类
    public class B : A
    {
        public B() { }
    }

    示例1

    public class Aclass
    {
        public int a1 = 10;
        private int a2 =20;
        private string name;
        private int age;
        public Aclass()
        {
            this.name = "小李";
            this.age = 16;
        }
        public string Anameage
        {
            get
            {
                return name + " is " + age.ToString();
            }
        }
    }

    public class Bclass : Aclass
    {
        public int b1;
        private int b2;
        private string name;
        private int age;
        public Bclass(string name, int age)
        {
            this.name = name;
            this.age = age;
        }
        public string Bnameage
        {
            get
            {
                return name + " is " + age.ToString();
            }
        }
    }

    class Mainclass
    {
        static void Main()
        {
            Bclass example = new Bclass("小王", 20);
            System.Console.WriteLine(example.a1);
            System.Console.WriteLine(example.b1);
            //System.Console.WriteLine(example.a2);
            //System.Console.WriteLine(example.b2);
            System.Console.WriteLine(example.Anameage);
            System.Console.WriteLine(example.Bnameage);
        }
    }

    示例解读:

    在基类Aclass和派生类Bclass中定义了一些私有成员和公共成员。然后在用于输出的Mainclass类中构造了派生类的新实例,因为Bclass继承Aclass,所以example除了可以访问Bclass的公共成员外还可以访问Aclass的公共成员,这说明example拥有两种数据类型,拥有多种数据类型的能力叫多态性。

    示例2

    public class Aclass
    {
        private string name;
        private int age;
        public Aclass(string name,int age)
        {
            this.name = name;
            this.age = age;
        }
        public string Again
        {
            get
            {
                return "姓名:"+name + "  年龄:" + age.ToString();
            }
        }

    }

    public class Bclass : Aclass
    {
        private string sex;
        public Bclass(string name,int age,string sex):base(name,age)
        {
            this.sex = sex;
        }
        public string Bgain
        {
            get
            {
                return "  性别:"+sex;
            }
        }
    }

    class Mainclass
    {
        static void Main()
        {
            Bclass example = new Bclass("小王", 20,"男");
            System.Console.Write(example.Again);
            System.Console.WriteLine(example.Bgain);
        }
    }

    示例解读:

    从这个示例我们可以体会到:

    1 基类Aclass提供了构造函数,用于初始化其自身的字段。

    2 派生类Bclass也提供了一个构造函数,用于初始化基类和其自身的字段。

    3 在派生类的构造函数中调用了基类的构造函数,这是通过base关键字实现的,base关键字用来指定从派生类中访问基类的成员,base的功能有两点,其一是调用基类上已被其他方法重写的方法,其二是指定创建派生类实例时应调用的基类构造函数。

    通过示例1和示例2我们可以看到,在撰写程序时基类Aclass()的初始化自身和基类的字段,是一种优良的程序设计方法,下面的两个示例进一步说明了这种思想,阅读时请仔细观察继承类之间的初值是如何设置的。

    示例3

    在本例中,基类 Person 和派生类 Employee 都有一个名为 Getinfo 的方法。通过使用 base 关键字,可以从派生类中调用基类的 Getinfo 方法。

    using System;
    public class Person
    {
        //受protected保护的成员其自身可访问并且可由派生类访问
        protected string ssn = "444-55-6666";
        protected string name = "John L. Malgraine";
       
        public virtual void GetInfo()
        {
            Console.WriteLine("Name: {0}", name);
            Console.WriteLine("SSN: {0}", ssn);
        }
    }
    class Employee : Person
    {
        public string id = "ABC567EFG";
       
        public override void GetInfo()
        {
            base.GetInfo();
            Console.WriteLine("Employee ID: {0}", id);
        }
    }

    class TestClass
    {
        static void Main()
        {
            Employee E = new Employee();
            E.GetInfo();
        }
    }

    示例4

    本示例显示如何指定在创建派生类实例时调用的基类构造函数。

    using System;
    public class BaseClass
    {
        int num;
        public BaseClass()
        {
            Console.WriteLine("in BaseClass()");
        }
        public BaseClass(int i)
        {
            num = i;
            Console.WriteLine("in BaseClass(int i)");
        }
        public int GetNum()
        {
            return num;
        }
    }

    public class DerivedClass : BaseClass
    {
        public DerivedClass()
            : base()
        {
        }
        public DerivedClass(int i)
            : base(i)
        {
        }

        static void Main()
        {
            DerivedClass md = new DerivedClass();
            DerivedClass md1 = new DerivedClass(1);
            Console.WriteLine(md.GetNum());
            Console.WriteLine(md1.GetNum());
        }
    }

    ★对象引用

    1、一般实例化方式:Aclass ss=new Aclass();
    2、可以用基类的对象引用来直接实例化子类的对象:
       Aclass ss=new Bclass()
    3、不能反过来,把基类的对象赋给子类的对象,如:
       Bclass dd=ss  但可以把基类的对象强制转换为子类对象,如: Bclass dd=(Bclass)ss

    ==============================
    二 抽象类
    ==============================
    可以将类声明为抽象类。方法是在类定义中将关键字 abstract 置于关键字 class 的前面。例如:

    public abstract class A
    {
       ......
    }

    抽象类不能实例化。抽象类的用途是提供多个派生类可共享的基类的公共定义。例如,类库可以定义一个作为其多个函数的参数的抽象类,并要求程序员使用该库通过创建派生类来提供自己的类实现。

    抽象类也可以定义抽象方法。方法是将关键字 abstract 添加到方法的返回类型的前面。例如:

    public abstract class A
    {
        public abstract void DoWork(int i);
    }

    抽象方法没有实现,它是一种虚方法,所以方法定义后面是分号,而不是常规的方法块。抽象类的派生类必须实现所有抽象方法。当抽象类从基类继承虚方法时,抽象类可以使用抽象方法重写该虚方法。例如:

    public class D
    {

        public virtual void DoWork(int i)
        {
            ......
        }
    }

    public abstract class E : D
    {

        public abstract override void DoWork(int i);
    }

    public class F : E
    {
        public override void DoWork(int i)
        {
           ......
        }
    }

    如果将虚方法声明为抽象方法,则它对于从抽象类继承的所有类而言仍然是虚的。继承抽象方法的类无法访问该方法的原始实现。在前面的示例中,类 F 上的 DoWork 无法调用类 D 上的 DoWork。在此情况下,抽象类可以强制派生类为虚方法提供新的方法实现。

    示例

    abstract class ShapesClass
    {
       public abstract  int Area();
    }

    class Square : ShapesClass
    {
        int x=10, y=10;
        public override int Area()
        {
            return x * y;
        }
    }

    class MainClass
    {
        static void Main()
        {
            Square m = new Square();
            System.Console.WriteLine(m.Area());
        }
    }

    ==============================
    三 密封类
    ==============================
    可以将类声明为密封类。方法是在类定义中将关键字 sealed 置于关键字 class 的前面。例如:

    public sealed class D
    {
        ......
    }

    密封类不能用作基类。因此,它也不能是抽象类。密封类主要用于防止派生。由于密封类从不用作基类,所以有些运行时优化可以使对密封类成员的调用略快。

    在对基类的虚成员进行重写的派生类上的类成员、方法、字段、属性或事件可以将该成员声明为密封成员。在用于以后的派生类时,这将取消成员的虚效果。方法是在类成员声明中将 sealed 关键字置于 override 关键字的前面。例如:

    public class D : C
    {
        public sealed override void DoWork() { }
    }

    示例

    using System;
    sealed class SealedClass
    {
        public int x;
        public int y;
    }

    class MainClass
    {
        static void Main()
        {
            SealedClass sc = new SealedClass();
            sc.x = 110;
            sc.y = 150;
            Console.WriteLine("x = {0}, y = {1}", sc.x, sc.y);
        }
    }

    如果试图从密封类继承将会收到一条错误信息。

    ==============================
    四 定义抽象属性
    ==============================

    抽象属性声明不提供属性访问器的实现,它只声明该类支持属性,而将访问器实现留给派生类。下面的示例演示如何实现从基类继承的抽象属性。

    //==================基类========================
    public abstract class Shape
    {
        private string m_id;
        public Shape(string s)
        {
            Id = s;
        }

        public string Id
        {
            get
            {
                return m_id;
            }

            set
            {
                m_id = value;
            }
        }
        public abstract double Area
        {
            get;
        }

        public override string ToString()
        {
            return Id + " Area = " + string.Format("{0:F2}",Area);
        }
    }

    String.Format 方法用来设置数值结果的格式,它通过格式字符串指定格式。"{0:F2}"表示输出格式为2位精度的浮点数,这样所有派生类都具有相同的输出格式,注意C#2.0以前的版本不支持这个方法。

    //==================派生类==========================
    public class Square : Shape
    {
        private int m_side;
        //base:调用基类构造函数并向其传递参数
        public Square(int side, string id)
            : base(id)
        {
            m_side = side;
        }

        //使用override扩展修改继承的属性Area
        public override double Area
        {
            get
            {
                return m_side * m_side;
            }
        }
    }

    //====================派生类====================

    public class Circle : Shape
    {
        private int m_radius;

        public Circle(int radius, string id)
            : base(id)
        {
            m_radius = radius;
        }

        public override double Area
        {
            get
            {
                return m_radius * m_radius * System.Math.PI;
            }
        }
    }

    //=====================派生类==========================

    public class Rectangle : Shape
    {
        private int m_width;
        private int m_height;

        public Rectangle(int width, int height, string id)
            : base(id)
        {
            m_width = width;
            m_height = height;
        }

        public override double Area
        {
            get
            {
                return m_width * m_height;
            }
        }
    }

    //=====================输出类========================
    class TestClass
    {
        static void Main()
        {
            Shape[] shapes =
            {
                new Square(5, "Square #1"),
                new Circle(3, "Circle #1"),
                new Rectangle( 4, 5, "Rectangle #1")
            };

            System.Console.WriteLine("Shapes Collection");
            foreach (Shape s in shapes)
            {
                System.Console.WriteLine(s);
            }
        }
    }

    基类shape解读:

    使用修饰符abstract定义一个抽象基类,其中包含了5个成员,它们是:
    1 私有字段m_id
    2 公共方法Shape(string s)是一个构造函数,用于初始化属性Id。
    3 公有属性Id,它用来设置获取私有字段m_id的值。
    4 抽象属性Area,注意其中有一个get访问器,但没有实现,这种只有获取方法的属性叫只读属性。
    5 公共方法ToString(),本来每个类或对象都有一个默认的ToString()方法,这里对这个方法进行了重写,这样就覆盖原来的。
    6 String.Format 方法用来设置数值结果的格式,它通过格式字符串指定格式。"{0:F2}"表示输出格式为2位精度的浮点数,这样所有派生类都具有相同的输出格式,注意C#2.0以前的版本不支持这个方法。

    派生类解读:

    这里创建了三个各自独立的派生类:Square, Circle, Rectangle 它们分别都继承自基类shape
    派生类Square继承基类shape,它包含三个成员:①私有字段m_side;②构造函数Square(int side,string id):base(id),用于初始化字段m_side,其中base(id)是调用基类的构造函数,并传参数给基类里的属性ID;③使用override重写基类的抽象属性Area。
    另两个派生类实现的效果和Square类基本相同。

    输出类解读:

    创建一个输出类TestClass,在主方法Main()里,首先声明shape类型的一个数组shapes,初始化为三个元素,每个元素使用new分别调用三个子类里的构造函数,并分别给了参数。最后用foreach循环语句来输出每个元素的值。

  • c#编程第九章 接口与多态性

    2010-02-24 12:39:59

    第九章 接口与多态性

    ==========================
    一 接口概述
    ==========================

    接口是对象之间通信的基本方式,在C#中接口是使用 interface 关键字定义的。例如:

    interface IComparable
    {
        int CompareTo(object obj);
    }

    接口描述可属于任何类或结构的一组相关行为。接口可由方法、属性、事件、索引器或这四种成员类型的任何组合构成。接口不能包含字段。接口成员一定是公共的。

    类和结构可以像类继承基类或结构一样从接口继承,但有两个例外:其一是类或结构可继承多个接口。其二是当类或结构继承接口时,它继承成员定义但不继承实现。例如:

    public class Minivan : Car, IComparable
    {
        public int CompareTo(object obj)
        {
            return 0;
        }
    }

    若要实现接口成员,类中的对应成员必须是公共的、非静态的,并且与接口成员具有相同的名称和签名。类的属性和索引器可以为接口上定义的属性或索引器定义额外的访问器。例如,接口可以声明一个带有 get 访问器的属性,而实现该接口的类可以声明同时带有 get 和 set 访问器的同一属性。但是,如果属性或索引器使用显式实现,则访问器必须匹配。

    接口和接口成员是抽象的;接口不提供默认实现。IComparable 接口向对象的用户宣布该对象可以将自身与同一类型的其他对象进行比较,接口的用户不需要知道相关的实现方式。

    接口可以继承其他接口。类可以通过其继承的基类或接口多次继承某个接口。在这种情况下,如果将该接口声明为新类的一部分,则类只能实现该接口一次。如果没有将继承的接口声明为新类的一部分,其实现将由声明它的基类提供。基类可以使用虚拟成员实现接口成员;在这种情况下,继承接口的类可通过重写虚拟成员来更改接口行为。

    接口具有下列属性:

    1 接口类似于抽象基类:继承接口的任何非抽象类型都必须实现接口的所有成员。

    2 不能直接实例化接口。

    3 接口可以包含事件、索引器、方法和属性。

    4 接口不包含方法的实现。

    5 类和结构可从多个接口继承。

    6 接口自身可从多个接口继承。

    ==========================
    二 显式接口实现
    ==========================

    如果类实现两个接口,并且这两个接口包含具有相同签名的成员,那么在类中实现该成员将导致两个接口都使用该成员作为它们的实现。例如:

    interface IControl
    {
        void Paint();
    }
    interface ISurface
    {
        void Paint();
    }
    class SampleClass : IControl, ISurface
    {
        public void Paint()
        {
        }
    }

    然而,如果两个接口成员执行不同的函数,那么这可能会导致其中一个接口的实现不正确或两个接口的实现都不正确。可以显式地实现接口成员 -- 即创建一个仅通过该接口调用并且特定于该接口的类成员。这是使用接口名称和一个句点命名该类成员来实现的。例如:

    public class SampleClass : IControl, ISurface
    {
        void IControl.Paint()
        {
            System.Console.WriteLine("IControl.Paint");
        }
        void ISurface.Paint()
        {
            System.Console.WriteLine("ISurface.Paint");
        }
    }

    类成员 IControl.Paint 只能通过 IControl 接口使用,ISurface.Paint 只能通过 ISurface 使用。两个方法实现都是分离的,都不可以直接在类中使用方法名Paint()。例如:

    SampleClass bj = new SampleClass();
    IControl c = (IControl)obj;
    c.Paint();

    ISurface s = (ISurface)obj;
    s.Paint();

    显式实现还用于解决两个接口分别声明具有相同名称的不同成员(如属性和方法)的情况:

    interface ILeft
    {
        int P { get;}
    }
    interface IRight
    {
        int P();
    }

    为了同时实现两个接口,类必须对属性 P 和/或方法 P 使用显式实现以避免编译器错误。例如:

    class Middle : ILeft, IRight
    {
        public int P() { return 0; }
        int ILeft.P { get { return 0; } }
    }

    示例

    本示例声明一个 接口IDimensions 和一个类 Box,该类显式实现接口成员 getLength 和 getWidth。通过接口实例 dimensions 访问这些成员。

    interface IDimensions
    {
        float getLength();
        float getWidth();
    }

    class Box : IDimensions
    {
        float lengthInches;
        float widthInches;
        Box(float length, float width)
        {
            lengthInches = length;
            widthInches = width;
        }
        float IDimensions.getLength()
        {
            return lengthInches;
        }
        float IDimensions.getWidth()
        {
            return widthInches;
        }

        static void Main()
        {
            Box mybox = new Box(30.0f, 20.0f);
            IDimensions dimensions = (IDimensions)mybox;
            System.Console.WriteLine("Length: {0}",dimensions.getLength());
            System.Console.WriteLine("Width: {0}",dimensions.getWidth());
        }
    }

    ==========================
    三 多态性概述
    ==========================
    多态性从字面上看是多种形态,在C#中多态性指的是对象具有多种数据类型,当一个类继承于某个基类或者接口时,则这个类就具有多态性,它可以是自己的类型,也可以是任何基类或接口类型。C# 中的每种类型都是多态的。类型可用作它们自己的类型或用作 Object 实例,因为任何类型都自动将 Object 当作基类型。

    当派生类从基类继承时,它会获得基类的所有方法、字段、属性和事件。若要更改基类的数据和行为,您有两种选择:可以使用新的派生成员替换基成员,或者可以重写虚拟的基成员。

    ★使用新的派生成员替换基成员:

    使用新的派生成员替换基类的成员需要使用 new 关键字。如果基类定义了一个方法、字段或属性,则 new 关键字用于在派生类中创建该方法、字段或属性的新定义。

    示例

    public class BaseClass

        public int WorkField;
        public int DoWork()
        {
            return 10;
        }
        public int WorkProperty
        {
            get { return 20; }
        }
    }

    public class DerivedClass : BaseClass
    {
        public new int WorkField;
        public new int DoWork()
        {
            return 100;
       
      public new int WorkProperty
        {
            get { return 200; }
        }

    }

    class Mainclass
    {
        static void Main()
        {
            DerivedClass ff = new DerivedClass();
            System.Console.WriteLine(ff.DoWork());
            System.Console.WriteLine(ff.WorkProperty);
        }
    }

    使用 new 关键字时,调用的是新的类成员而不是已被替换的基类成员。这些基类成员称为隐藏成员。如果将派生类的实例强制转换为基类的实例,就仍然可以调用隐藏类成员。

    示例

    public class BaseClass

        public int WorkField;
        public int DoWork()
        {
            return 10;
        }
        public int WorkProperty
        {
            get { return 20; }
        }
    }

    public class DerivedClass : BaseClass
    {
        public new int WorkField;
        public new int DoWork()
        {
            return 100;
       
      public new int WorkProperty
        {
            get { return 200; }
        }

    }

    class Mainclass
    {
        static void Main()
        {
            DerivedClass ff = new DerivedClass();
            System.Console.WriteLine(ff.DoWork());
            System.Console.WriteLine(ff.WorkProperty);
            //-----------------------------------------
            DerivedClass B = new DerivedClass();      
            BaseClass A = (BaseClass)B;
            System.Console.WriteLine(A.DoWork());
            System.Console.WriteLine(A.WorkProperty);
        }
    }

    ★ 重写虚拟的基成员

    为了使派生类的实例完全接替来自基类的类成员,基类必须将该成员声明为虚拟的。这是通过在该成员的返回类型之前添加 virtual 关键字来实现的。然后,派生类可以选择使用 override 关键字而不是 new,将基类实现替换为它自己的实现。

    字段不能是虚拟的,只有方法、属性、事件和索引器才可以是虚拟的。当派生类重写某个虚拟成员时,即使该派生类的实例被当作基类的实例访问,也会调用该成员。

    public class BaseClass
    {
        public virtual int DoWork()
        {
            return 10;
        }
        public virtual int WorkProperty
        {
            get { return 20; }
        }
    }

    public class DerivedClass : BaseClass
    {
        public override int DoWork()
        {
            return 100;
        }
        public override int WorkProperty
        {
            get { return 200; }
        }

    }

    class Mainclass
    {
        static void Main()
        {
            DerivedClass ff = new DerivedClass();
            System.Console.WriteLine(ff.DoWork());
            System.Console.WriteLine(ff.WorkProperty);
            //-----------------------------------------
            DerivedClass B = new DerivedClass();      
            BaseClass A = (BaseClass)B;
            System.Console.WriteLine(A.DoWork());
            System.Console.WriteLine(A.WorkProperty);
        }
    }

    使用虚拟方法和属性可以预先计划未来的扩展。由于在调用虚拟成员时不考虑调用方正在使用的类型,所以派生类可以选择完全更改基类的外观行为。

    ★ 停止虚拟继承

    通常虚拟成员会不断的继承下去,无论在派生类和最初声明虚拟成员的类之间已声明了多少个类,虚拟成员都将永远为虚拟成员。如果类 A 声明了一个虚拟成员,类 B 从 A 派生,类 C 从类 B 派生,则类 C 继承该虚拟成员,并且可以选择重写它,而不管类 B 是否为该成员声明了重写。若要停止这种虚拟继承,可以将重写声明为密封的来实现。这需要在类成员声明中将 sealed 关键字放在 override 关键字的前面。例如:

    public class A
    {
        public virtual int DoWork() { }
    }
    public class B : A
    {
        public override int DoWork() { }
    }
    public class C : B
    {
        public sealed override int DoWork() { }
    }

    在上面的示例中,方法 DoWork 对从 C 派生的任何类都不再是虚拟的。它对 C 的实例仍然是虚拟的 -- 即使将这些实例强制转换为类型 B 或类型 A。派生类可以通过使用 new 关键字替换密封的方法,如下面的示例所示:

    public class D : C
    {
        public new int DoWork() { }
    }

    在此情况下,如果在 D 中使用类型为 D 的变量调用 DoWork,被调用的将是新的 DoWork。如果使用类型为 C、B 或 A 的变量访问 D 的实例,对 DoWork 的调用将遵循虚拟继承的规则,即把这些调用传送到类 C 的 DoWork 实现。

    ★ base访问基类成员

    已替换或重写某个方法或属性的派生类仍然可以使用关键字base访问基类的该方法或属性。

    示例

    public class A
    {
        public virtual int DoWork() { return 10; }
    }
    public class B : A
    {
        public override int DoWork() { return 100; }
    }

    public class C : B
    {
        public override int DoWork()
        {
            return base.DoWork();
        }
    }

    class Mainclass
    {
        static void Main()
        {
            C myc = new C();
            System.Console.WriteLine(myc.DoWork());
        }
    }

    建议虚拟成员在它们自己的实现中使用 base 来调用该成员的基类实现。允许基类行为发生使得派生类能够集中精力实现特定于派生类的行为。未调用基类实现时,由派生类负责使它们的行为与基类的行为兼容。

    小结:

    本节中学习了在派生类中如何更改基类的数据和行为,当基类是一个普通类时,我们可以在派生类中使用new关键字,将基类成员隐藏起来,然后使用新的派生成员代替基成员,还有一种办法就是将类定义成虚拟类,其中的成员也定义成虚的,这是通过关键字virtual实现的,此时在派生类中可以使用new关键字和override关键字,使用override关键字修饰的成员会代替基类的相应成员,要注意的是override、virtual 和 new 关键字还可以用于属性、索引器和事件中,但不能应用于字段。

    谈到虚拟成员其实我们在上一章中已经接触过了,不过那个虚成员是定义在抽象类中的并且没有实现,这里的虚成员是定义在虚拟类中的并且实现是存在的。到底虚成员有什么用处呢,除了分散在课文中所谈到用处外,它还具有许多方面的意义,例如,在基类中引入与派生类中的某个成员具有相同名称的新成员在 C# 中是完全支持的,不会导致意外行为,还可以使不同库中的基类与派生类之间的版本控制不断向前发展,同时保持向后的兼容性。

    ==========================
    四 重写 ToString
    ==========================

    C# 中的每个对象都继承 ToString 方法,此方法返回该对象的字符串表示形式。例如,所有 int 类型的变量都有一个 ToString 方法,从而允许将变量的内容作为字符串返回:

    class Mainclass
    {
        static void Main()
        {
           
            int x = 42;
            System.Console.WriteLine(x.ToString());

        }

    通过重写 ToString 方法,您可以控制对象返回的默认字符串。这在调试或跟踪程序的执行时可能很有用:

    class SampleObject
    {
        int number = 42;

        public override string ToString()
        {
            return "Object: SampleObject. Value: " + number;
        }
    }

    class Mainclass
    {
        static void Main()
        {
            SampleObject o = new SampleObject();
            System.Console.WriteLine(o.ToString());
        }
    }

    产生的输出如下:

    Object: SampleObject. Value: 42

  • C#抽象类和接口的区别

    2010-02-24 00:04:00

    抽象类和接口的区别。

    区别一,两者表达的概念不一样。抽象类是一类事物的高度聚合,那么对于继承抽象类的子类来说,对于抽象类来说,属于“是”的关系;而接口是定义行为规范,因此对于实现接口的子类来说,相对于接口来说,是“行为需要按照接口来完成”。这些听起来有些虚,举个例子。例如,狗是对于所有狗类动物的统称,京哈是狗,牧羊犬是狗,那么狗的一般特性,都会在京哈,牧羊犬中找到,那么狗相对于京哈和牧羊犬来说,就属于这类事物的抽象类型;而对于“叫”这个动作来说,狗可以叫,鸟也可以叫。很明显,前者相当于所说的是抽象类,而后者指的就是接口。

    区别二,抽象类在定义类型方法的时候,可以给出方法的实现部分,也可以不给出;而对于接口来说,其中所定义的方法都不能给出实现部分。
    例如:
    public abstract class AbsTest
    {
    public virtual void Test()
    {
    Debug.WriteLine( "Test" );
    }
    public abstract void NewTest();
    }
    public interface ITest
    {
    void Test();
    void NewTest();
    }

    区别三,继承类对于两者所涉及方法的实现是不同的。继承类对于抽象类所定义的抽象方法,可以不用重写,也就是说,可以延用抽象类的方法;而对于接口类所定义的方法或者属性来说,在继承类中必须要给出相应的方法和属性实现。

    区别四,在抽象类中,新增一个方法的话,继承类中可以不用作任何处理;而对于接口来说,则需要修改继承类,提供新定义的方法。

    知道了两者的区别,再来说说,接口相对于抽象类的优势。
    好处一,接口不光可以作用于引用类型,也可以作用于值类型。而抽象类来说,只能作用于引用类型。

    好处二,.Net的类型继承只能是单继承的,也就是说一个类型只能继承一个类型,而可以继承多个接口。其实,我对于这一点也比较赞同,多继承会使继承树变的混乱。

    好处三,由于接口只是定义属性和方法,而与真正实现的类型没有太大的关系,因此接口可以被多个类型重用。相对于此,抽象类与继承类的关系更紧密些。

    好处四,通过接口,可以减少类型暴露的属性和方法,从而便于保护类型对象。当一个实现接口的类型,可能包含其他方法或者属性,但是方法返回的时候,可以返回接口对象,这样调用端,只能通过接口提供的方法或者属性,访问对象的相关元素,这样可以有效保护对象的其他元素。

    好处五,减少值类型的拆箱操作。对于Struct定义的值类型数据,当存放集合当中,每当取出来,都需要进行拆箱操作,这时采用Struct+Interface结合的方法,从而降低拆箱操作。
    相对于抽象类来说,接口有这么多好处,但是接口有一个致命的弱点,就是接口所定义的方法和属性只能相对于继承它的类型(除非在继承类中修改接口定义的函数标示),那么对于多层继承关系的时候,光用接口就很难实现。因为如果让每个类型都去继承接口而进行实现的话,首先不说编写代码比较繁琐,有时候执行的结果还是错误,尤其当子类型对象隐式转换成基类对象进行访问的时候。
    那么这时候,需要用接口结合虚方法来实现。参看IDisposable在继承类型中的实现方法。

    其实在继承中,到底使用接口还是抽象类。接口是固定的,约定俗成的,因此在继承类中必须提供接口相应的方法和属性的实现。而对于抽象类来说,抽象类的定义方法的实现,贯穿整个继承树,因此其中方法的实现或者重写都是不确定的。因此相对而言,抽象类比接口更灵活一些。


    总的来说,接口和抽象类是.Net为了更好的实现类型之间继承关系而提供的语言手段,而且两者有些相辅相成的关系。因此我并不强调用什么而不用什么,那么问题的关键在于,如何把这两种手段合理的应用到程序当中,这才是至关重要。
  • 方法的重写和隐藏

    2010-02-23 16:31:34

    最近正在学习c#,对其中的方法重写和隐藏的概念很是模糊,现在将其归纳如下:

    1:方法重写:就是在基类中的方法用virtual关键字来标识,然后在继承类中对该类进行重写(override),这样基类中的方法已经被重写了,已经失去了功能了。当让基类的对象的引用直接指向继承类的对象时(多态性),调用该方法则是调用的继承类的方法。

    2:方法隐藏:无论基类中的方法是否用了virtual关键字,继承类中都可以用new关键字(如果不用new的话,不会产生错误,但会生成一个编译警告)将基类中的方法隐藏,所谓隐藏就是隐藏,不像重写,重写就是原来的(基类中)已经不存在了,而隐藏是原来的还存在。所以当让基类的对象的引用直接指向继承类的对象时(多态性),调用该方法则是调用的基类的方法。

    代码如下:

     

      public class BaseClass
        {
            public void functionA()
            {
                Console.WriteLine("BaseFunctionA");
            }
            public virtual void functionB()
            {
                Console.WriteLine("BaseFunctionB");
            }
        }
       public class DerivedClass:BaseClass
        {
            public new void functionA()
            {
                Console.WriteLine("DerivedFunctionA");
            }
            public override void functionB()
            {
                Console.WriteLine("DerivedFunctionB");
            }
        }

    当利用多态性执行下面代码时:

    BaseClass baseFunction=new DerivedClass();
    baseFunction.functionA();
    baseFunction.functionB();
    得到的结果是:

    BaseFunctionA

    DerivedFunctionB

     

    例二:

    class A  

      {  

          public   virtual   void   F()   {   Console.WriteLine("A.F");   }  

      }  

      class B: A  

      {  

          public   override   void   F()   {   Console.WriteLine("B.F");   }  

      }  

      class C: B  

      {  

          new   public   virtual   void   F()   {   Console.WriteLine("C.F");   }  

      }  

      class D: C  

      {  

          public   override   void   F()   {   Console.WriteLine("D.F");   }  

      }  

      class Test  

      {  

          static   void   Main()  

      {  

                 

               D d = new D();  

               C c = d;

               B b = d;

               A a = d;                

               a.F();  //值为B.F 

               b.F();  //值为B.F

               c.F();  //值为D.F 

               d.F();  //值为D.F 

            }  

      }

     

     

    本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/zhuanshen712/archive/2008/12/27/3611573.aspx

  • c#中override和new的区别

    2010-02-23 12:22:51

    override与new都是重写基类的方法,但两者还是有区别:
      当用override   重写时;用强制父类调用此方法时,是调用子类的方法,  
      而用new重写时;用强制父类调用此方法时,是调用父类的方法;   
        
      class   A  
      {  
      public   virtual   void   mm();  
      }  
      class   B   :   A  
      {  
      public   override   void   mm();  
      }  
      class   C   :   A  
      {  
      public   new   void   mm();  
      }  
       
      class   main  
      {  
      public   void   m1()  
      {  
      A   aa   =   new   B();  
      aa.mm();//调用B.mm()  
      A   aa   =   new   C();   
      aa.mm();//调用A.mm()  
      B   bb   =   new   B();  
      ((A)bb).mm();//调用B.mm();  
      C   cc   =   new   C();  
      ((A)cc).mm();//调用A.mm();  
      }  
      }
     
    Override关键字主要是提供派生类对基类方法的新实现,重写的基类方法必须和Override的方法具有相同的签名,此关键字不可以用于重写非虚方法和静态方法,与其配套使用的关键字是Virtual、abstract、Override。与此同时,Override方法还不可以修改Virtual方法的可访问性,Override方法和Virtual方法必须具有相同的访问修饰符,不能使用修饰符 new、static、virtual 或 abstract 来修改 override 方法。
     
    new关键字可以在派生类中隐藏基类的方法,也就说在使用派生类的方法是调用的方法是New关键字新定义出来的方法,而不是基类的方法。在不使用New关键字来隐藏基类方法也是可以的,编译器会出现一个警告,提示如果有意去隐藏基类的方法,请使用New关键字修饰。
  • c#编译期多态vs运行时多态

    2010-02-23 12:10:42

    多态分编译期多态和运行时多态:
        
      前一种多态具体实现就是重载。(包括函数重载,运算符重载等)

      后一种才是真正强有力的多态,通常通过继承或实现(接口)来实现。这种方式下我们可以通过子类覆盖父类方法或实现接口方法来达到多态的目的。

    C#语言的运行是先编译后执行(运行的),编译多态就是编译的时候已经找到对应的函数或对应形态而运行多态的情况是:在编译的时候还没有确定(只是语法正确),在运行的时候 ,通过指针或其他形式找到对应的。

     

522/3<123>
Open Toolbar