<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:georss='http://www.georss.org/georss' xmlns:gd='http://schemas.google.com/g/2005' xmlns:thr='http://purl.org/syndication/thread/1.0'><id>tag:blogger.com,1999:blog-6509530088537976629</id><updated>2011-11-27T17:27:21.007-08:00</updated><category term='.net、Inherits'/><category term='c#、教程、委托、事件'/><category term='c#、教程、数组'/><category term='c#、教程、运算符重载'/><category term='.net、体系结构、公共语言库'/><category term='c#、教程、变量'/><category term='.NET、缓存'/><category term='C#、教程、字典'/><category term='C#、教程、集合'/><category term='c#、教程、预处理器指令'/><category term='c#、教程、Main'/><category term='c#、教程、运算符'/><category term='c#、教程、用户定义的数据类型转换'/><category term='c#、教程、类'/><category term='.net、asp.net'/><category term='.net、服务、时时备份'/><category term='ASP.NET;.NET'/><category term='c#、教程、释放未托管的资源'/><category term='c#、教程、控制台、I/O'/><category term='c#、教程、接口'/><category term='c#、教程、结构'/><category term='c#、教程、预定义数据类型'/><category term='.net'/><category term='C#、教程、正则表达式'/><category term='c#、教程、流控制'/><category term='.net、控件'/><category term='snmp、.net'/><category term='c#、教程、委托'/><category term='.net、GUID'/><category term='c#、教程、事件'/><category term='c#、教程、命名空间'/><category term='snmp、.net、MIB'/><category term='c#、教程、类、'/><category term='.net、XML、SOAP'/><category term='c#、教程、String'/><category term='c#、教程、指针'/><category term='c#、教程、修饰符'/><category term='.net;soap'/><category term='C#、教程、数组列表'/><category term='.net;网站;搭建;asp.net'/><category term='.NET、CIL'/><category term='ADO.NET、XML'/><category term='c#、教程、BubbleSorter、委托'/><category term='C#、教程、格式化字符串'/><category term='.net、LINQ'/><category term='ADO.NET'/><category term='jQuery、.net、javascript'/><category term='c#、教程、继承'/><category term='C#、教程、创建字符串'/><category term='c#、教程、枚举'/><category term='c#、教程、后台内存管理'/><category term='.net、socket'/><category term='三层架构'/><category term='c#、教程、类型、安全性'/><category term='.net、多线程'/><category term='c#、教程、Object'/><category term='c#、教程、对象'/><category term='ADO.NET;.NET'/><title type='text'>.NET进阶学习;.NET学习;C#</title><subtitle type='html'>.NET进阶学习,C#到ASP.NET,从桌面应用程序到WEB应用程序</subtitle><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/posts/default'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default?max-results=100'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>79</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>100</openSearch:itemsPerPage><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-1179481573741030522</id><published>2009-05-10T17:38:00.000-07:00</published><updated>2009-05-10T17:51:25.602-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='C#、教程、字典'/><title type='text'>C#教程（四十六） 字典(1)</title><content type='html'>字典表示一种非常复杂的数据结构，这种数据结构允许按照某个键来访问元素，这个键可以是任意数据类型。字典还可以称为映射或散列表。在希望把对象保存为数组，但希望使用其他数据类型(不是数字类型)来给结构建立索引时，字典是非常适合的。也可以向字典自由添加和删除元素，这有点像ArrayList。但没有改变内存中后续数据项的性能开销。&lt;br /&gt;&lt;br /&gt;下面介绍字典的使用场合，本节后面会举一个示例MortimerPhonesEmployees来说明。这个示例假定Mortimer Phones(这是第3章介绍的移动电话公司)用某个软件处理其员工的信息。为此需要一个数据结构，它有点像数组，其中包含员工的数据，假定每个Mortimer Phones员工都用一个员工ID来标识，这个员工ID是一组字符，例如B342 或 W435，存储在EmployeeID对象中，员工的信息则存储在EmployeeData对象中，本例只包含员工的ID、姓名和薪水。&lt;br /&gt;&lt;br /&gt;假定有下述EmployeeID：&lt;br /&gt;&lt;br /&gt;EmployeeID id = new EmployeeID("W435");&lt;br /&gt;&lt;br /&gt;有一个变量employees，在语法上，我们可以把这个变量当作EmployeeData对象的一个数组。但在实际上，它并不是一个数组，而是一个字典，因此，可以使用上面声明的ID获得员工的信息，如下所示。&lt;br /&gt;&lt;br /&gt;EmployeeData theEmployee = employees[id];&lt;br /&gt;&lt;br /&gt;   // Note that id is NOT a numeric type – it is an EmployeeID instance&lt;br /&gt;&lt;br /&gt;这就是字典的功能。它看起来像一个数组(实际上比数组更强大，它更像一个ArrayList，因为可以动态设置它的容量，添加或删除其元素)，但不必用整数给它建立索引，而可以使用任意数据类型来建立索引。对于字典，这称为键，而不是索引。粗略地讲，在访问字典中的元素(在前面的例子中就是ID对象)时，字典就会使用这个键，还可以对这个键的值进行某些处理，这种处理会根据键的值返回一个整数，用于确定在“数组”中，元素应存储在什么地方，或从什么地方获取。使用字典来存储对象的其他场合有：&lt;br /&gt;&lt;br /&gt;●       可以存储员工或其他人的信息，用他们的社会安全号作为索引。社会安全号码基本上是一个整数，但不能使用数组，并把社会安全号作为索引，因为US社会安全号在理论上的最大值是999999999。在32位系统上，不能在程序中给数组分配这么大的地址空间！数组的大部分元素都是空的。而使用字典，可以用社会安全号来为员工建立索引，且字典占据的空间较小。&lt;br /&gt;&lt;br /&gt;●       可以存储地址，索引是邮政编码。在USA，邮政编码都是数字，但在加拿大和英国，邮政编码是包含字母和数字的字符串。&lt;br /&gt;&lt;br /&gt;●       可以存储对象或人的任何数据，其索引是对象或人的名字。&lt;br /&gt;&lt;br /&gt;尽管字典的作用是使客户机代码看起来更像一个动态的数组，有非常灵活的索引机制，但在后台要做许多工作才能实现这个功能。大体上，任何类的对象都可以用作字典的索引键，但在此之前，需要对类执行某些功能，通常要执行方法GetHashCode()，所有的类和结构都从System.Object继承了这个方法。本节将详细讨论字典，它的工作原理和GetHashCode()的调用方式。然后介绍MortimerPhonesEmployees示例，它说明了如何使用字典，以及如何建立一个类，把它用作一个键。&lt;br /&gt;&lt;br /&gt;1. 现实生活中的字典&lt;br /&gt;使用字典这个名称，是因为其数据结构非常类似于现实生活中的字典。在一本字典中，通常可以查找某个单词的含义(在外语字典中，则可以查找一个单词的汉译)；给出含义的几行文本(或翻译)就是我们感兴趣的数据。大的字典会有成千上万个条目，在查找单词的含义时，使用这样的字典肯定可以查到，因为我们是按照字母顺序查找的。此时，要查找的单词就相当于用于获取自己感兴趣的数据的键。我们对单词本身并不像与之相关的数据那样感兴趣。这个单词只提供了查找字典中条目的方式，因此，要建立一个字典，要做3件事：&lt;br /&gt;&lt;br /&gt;●       要查找的数据&lt;br /&gt;&lt;br /&gt;●       键&lt;br /&gt;&lt;br /&gt;●       在字典中查找数据的算法&lt;br /&gt;&lt;br /&gt;算法是字典的重要部分。只知道键是不够的，还需要一种方式，利用键来确定数据结构中条目的位置。在现实生活中的字典里，这种算法就是以字母顺序来排列单词。&lt;br /&gt;&lt;br /&gt;2. .NET中的字典&lt;br /&gt;在.NET中，基本的字典是由类Hashtable来表示的，它遵循现实生活中字典的规则，但假定键和条目都是Object类型。散列表可以存储各种数据结构，而现实生活中的字典只使用字符串作为它的键。&lt;br /&gt;&lt;br /&gt;虽然Hashtable表示可以存储任何东西的一般字典，但也可以定义自己的更具体化的字典类。Microsoft提供了一个抽象基类DictionaryBase，它具有基本的字典功能，从中可以派生自己的字典类。还有一个已建立好的.NET基类System.Collections.Specialized.StringDictionary，如果键是字符串，就可以使用它来代替Hashtable。&lt;br /&gt;&lt;br /&gt;与StringBuilder和 ArrayList一样，在创建Hashtable对象时，可以指定它的原始容量：&lt;br /&gt;&lt;br /&gt;Hashtable employees = new Hashtable(53);&lt;br /&gt;&lt;br /&gt;与往常一样，Hashtable有许多其他的构造函数，但这是最常用的一个，注意在此选择了一个不太常见的最初容量53，其原因是在字典中使用内部算法时，如果容量是一个素数，它们的工作效率最高。&lt;br /&gt;&lt;br /&gt;给Hashtable添加一个对象，要使用方法Add()，但Hashtable.Add()带有两个参数，它们都是对象引用。第一个参数是对键的引用，第二个参数是对数据的引用。在后面的示例中，对EmployeeID和 EmployeeData类执行这个方法，如下所示：&lt;br /&gt;&lt;br /&gt;EmployeeID id; &lt;br /&gt;&lt;br /&gt;EmployeeData data;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;// initialize id and data to refer to some employee&lt;br /&gt;&lt;br /&gt;// assume employees is a Hashtable instance &lt;br /&gt;&lt;br /&gt;//that contains EmployeeData references &lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;employees.Add(id, data);&lt;br /&gt;&lt;br /&gt;为了获取一个数据项中的数据，需要提供键。Hashtable执行一个索引符，这样才能获取数据，这就是前面介绍的获取数组的语法：&lt;br /&gt;&lt;br /&gt;EmployeeData data = employees[id];&lt;br /&gt;&lt;br /&gt;还可以从字典中删除数据项，这也要提供被删除的对象的键：&lt;br /&gt;&lt;br /&gt;employees.Remove(id);&lt;br /&gt;&lt;br /&gt;使用Count属性，可以确定在散列表中有多少个条目：&lt;br /&gt;&lt;br /&gt;int nEmployees = employees.Count;&lt;br /&gt;&lt;br /&gt;但要注意，字典中没有Insert()方法。我们还没有介绍字典的内部工作情况，但添加数据和插入数据没有什么区别。与数组和ArrayList不同，在结构的开头没有一个大的数据块，在其尾部，也没有空的数据块。而在字典中，没有标记的的部分都是空的，如图9-1所示。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;图  9-1&lt;br /&gt;&lt;br /&gt;当添加一个数据项时，该数据项会放在字典的任何位置。在使用字典时，不需要知道如何根据键来确定这个位置。重要的是，用于确定数据项的位置的算法应非常可靠，只要记住键是什么，就可以把这个键传送给Hashtable对象，该对象就可以使用这个键快速确定数据项的位置，并获取该数据项。本节的后面会介绍这个算法的工作方式。现在只要知道它使用了键的GetHashCode()方法即可。&lt;br /&gt;&lt;br /&gt;注意上图被简化了。每个键/数据项对实际上并没有存储在字典结构的内部—— 通常对于引用类型来说，存储的是对象引用，它们可以指定对象实际定位在堆的什么地方。&lt;br /&gt;&lt;br /&gt;3. 字典的工作情况&lt;br /&gt;前面介绍了字典(散列表)使用起来非常方便，但这里有一个问题：Hashtable(和其他字典类)使用某种算法，根据键来确定每个对象的位置，实际上，该算法并不完全是由Hashtable类提供的。它有两个部分，其中一个部分的代码必须由key类来提供。如果使用一个Microsoft提供的类，并把这个类用作键(例如字符串)，就不会有任何问题(Microsoft已经编写了所有的代码)，但如果key类是自己编写的，就必须自己编写这部分算法。&lt;br /&gt;&lt;br /&gt;在计算机中，由key类执行的部分算法称为散列(因此字典也称为散列表)，Hashtable在散列算法中有非常特殊的地位：对象的GetHashCode()方法，继承于System.Object。只要字典类需要定位数据项的位置，就会调用键对象的GetHashCode()方法。因此，在讨论System.Object()时，我们强调如果重写了GetHashCode()方法，就会对如何编写该重写方法有许多苛刻的要求。其原因是该重写方法必须以某种方式确保字典类能正常工作(显然，如果在字典中，不把类当作键来使用，就不需要重写GetHashCode()方法)。&lt;br /&gt;&lt;br /&gt;其工作方式是GetHashCode()返回一个int类型的数据，它应使用这个键的值来生成该int类型的数据。Hashtable获取这个值，并对它进行其他一些操作，其中涉及到一些复杂的数学计算，最后返回一个索引，表示带有给定散列的数据项在字典中存储的位置。我们不详细介绍这部分算法，Microsoft已经为这部分算法编写好了代码，所以不需要详细了解，但该算法使用素数，而且这就是散列表容量为什么应是一个素数的原因。&lt;br /&gt;&lt;br /&gt;为了使GetHashCode()重写方法正常工作，有一些相当严格的要求。这些要求听起来相当抽象，难以理解，但不必太担心，MortimerPhonesEmployees示例会对此进行说明，编写一个满足这些要求的key类也不是那么困难：&lt;br /&gt;&lt;br /&gt;●       该方法应比较快(因为在字典中放置或获取数据项要求比较快)。&lt;br /&gt;&lt;br /&gt;●       该方法应比较一致，如果两个键表示相同的值，那么它们必须为散列提供相同的值。&lt;br /&gt;&lt;br /&gt;●       该方法给出的值最好能平均分布在int可以存储的数字的整个范围内。&lt;br /&gt;&lt;br /&gt;最后一个条件的原因是有一个潜在的问题：如果在字典中，两个数据项的散列有相同的索引，该怎么办？&lt;br /&gt;&lt;br /&gt;如果发生这种情况，字典类就必须寻找最近的可利用的自由单元来存储第二个数据项，必须进行一定的搜索，以便以后获取这个条目。这显然这会降低性能，如果许多键都有相同的索引，就有可能出现崩溃。根据Microsoft的部分算法的工作方式，在计算出来的散列值平均分布在int.MinValue和 int.MaxValue之间时，这种崩溃的危险会降低到最小。&lt;br /&gt;&lt;br /&gt;字典的元素越多，键之间的崩溃危险也越大，所以最好确保字典的容量大于其中的元素个数。因此，在字典被填满前，Hashtable会自动重新分配内存，增大其容量。表被填满的比例称为负载(load)，可以为这个负载设置一个最大值，在另一个Hashtable构造函数中给Hashtable重新分配内存前，该负载会达到这个最大值：&lt;br /&gt;&lt;br /&gt;// capacity =50, Max Load = 0.5&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;Hashtable employees = new Hashtable(50, 0.5);&lt;br /&gt;&lt;br /&gt;负载的最大值越小，散列表的工作效率就越高，但它占据的内存也越大。当散列表为了增大其容量而重新分配内存时，总会选择一个素数作为其新容量。&lt;br /&gt;&lt;br /&gt;上面列出的另一个要点是，散列算法必须一致。如果两个对象包含相同的数据，它们就必须给出相同的散列值，这就是重写System.Object的Equals()和 GetHashCode()方法时必须考虑的重要限制，Hashtable确定两个键A和B是否相等的方式是调用A.Equals(B)，即必须确保下述条件：&lt;br /&gt;&lt;br /&gt;如果A.Equals(B)是true，则A.GetHashCode()和B.GetHashCode()必须返回相同的散列。&lt;br /&gt;&lt;br /&gt;这看起来可能非常微妙，但非常重要。如果重写这些方法的方式不能保证上述语句为true，用这个类实例作为键的散列表就不能正常工作。例如，把一个对象放在散列表中，但从来没有获取过它，或者试图获取一个数据项，但会返回错误的数据项。&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;因此，如果为Equals()提供了一个重写方法，但没有为GetHashCode()提供重写方法，C#编译器就会显示一个编译警告。&lt;br /&gt;&lt;br /&gt;对于System.Object，这个条件是true，因为Equals()仅比较引用，GetHashCode()会根据对象的地址返回一个散列，则基于键的散列表没有重写这些方法，将正常工作。但是，这种方式也有问题，只有键是相同的对象时，才是相等的。当把一个对象放在字典中时，就必须挂起键的引用。以后也不能实例化另一个有相同值的键对象，因为相同的值被定义为表示相同的实例。如果没有重写Equals() 和 GetHashCode()的Object版本，类就不能在散列表中方便地使用。因此，应根据键的值来执行GetHashCode()，生成一个散列，而不是根据内存中的地址来执行该方法，这就是必须为要用作键的类重写Equals() 和 GetHashCode()的原因。&lt;br /&gt;&lt;br /&gt;System.String有3个重载方法，重载Equals()提供了值的比较，GetHashCode()也被相应地重载，根据字符串的值返回一个散列。因此，字典中把字符串用作键是非常方便的。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-1179481573741030522?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/1179481573741030522/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/05/c-1.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/1179481573741030522'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/1179481573741030522'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/05/c-1.html' title='C#教程（四十六） 字典(1)'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-5385621392247792066</id><published>2009-05-07T17:14:00.000-07:00</published><updated>2009-05-07T17:16:27.700-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='C#、教程、集合'/><title type='text'>C#教程（四十五） 集合</title><content type='html'>集合表示一组可以通过遍历每个元素来访问的一组对象，特别是可以使用foreach循环来访问它们。换言之，编写下面的代码时，假定变量MessageSet是一个集合：&lt;br /&gt;&lt;br /&gt;foreach (string nextMessage in messageSet)&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   DoSomething(nextMessage);&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;使用foreach循环是集合的主要目的，集合没有提供其他特性。&lt;br /&gt;&lt;br /&gt;下面将详细介绍集合，并把前面开发的Vector示例转换为一个集合。集合的概念很广，实际上它并不是.NET的新增内容，而是COM的一部分，在Visual Basic 6中得到使用，其语法是For…Each，非常方便。Java也有一个foreach循环，其底层的结构非常类似于.NET集合。&lt;br /&gt;&lt;br /&gt;1. 集合的概念&lt;br /&gt;对象如果可以提供相关对象的引用，就是一个集合，称为枚举，它可以遍历集合中的数据项。更专业的说法是集合必须实现接口System.Collections.IEnumerable。IEnumerable只定义了一个方法，如下所示。&lt;br /&gt;&lt;br /&gt;interface IEnumerable&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   IEnumerator GetEnumerator();&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;GetEnumerator()的目的是返回枚举对象。从上面的代码可以看出，该枚举对象实现接口System.Collections.IEnumerator。&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;还有一个集合接口ICollection，它派生于IEnumerable。更复杂的集合也实现这个接口。除了GetEnumerator()外，它还有一个属性，可以返回集合中的元素个数。它还可以把集合复制到数组中，并提供信息，说明它是否是线程安全的。但是，这里只考虑较简单的集合接口IEnumerable。&lt;br /&gt;&lt;br /&gt;IEnumerator如下所示。&lt;br /&gt;&lt;br /&gt;interface IEnumerator &lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   object Current { get; }&lt;br /&gt;&lt;br /&gt;   bool MoveNext();&lt;br /&gt;&lt;br /&gt;   void Reset();&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;IEnumerator的工作方式如下：实现该接口的对象应与一个集合相关联，这个对象在第一次初始化时，还没有指向集合中的任何元素，必须调用MoveNext()，移动枚举，才能使它指向集合中的第一个元素。接着用Current属性获取该元素，Current属性返回一个对象引用，所以必须把它的数据类型转换为要在集合中查找的对象类型。可以对该对象进行任何操作，之后再次调用MoveNext()方法，移动到集合的下一个元素上。重复这个过程，直到集合中没有元素为止，当Current属性返回null时，就表示到达了集合的末尾。如果要返回到集合的开头，可以随时调用Reset()方法。注意Reset()方法实际上返回到集合开头前面的位置，如果要调用这个方法，就需要接着调用MoveNext()，指向第一个元素。&lt;br /&gt;&lt;br /&gt;从这个例子可以看出，当不想提供下标时，集合是遍历元素的另一种方式。可以根据集合本身来选择元素返回的顺序，这通常意味着只要可以查看所有的元素，就不需要考虑获得元素的顺序。但在一些情况下，集合要按一定的顺序返回元素。此时，集合就是一种基本类型的对象组，因为它不允许向该组添加或删除元素，而只能按照集合所确定的顺序获得元素，并查看它们。甚至不能替换或修改集合中的元素，因为Current属性是只读的。这种集合主要是为foreach循环提供更方便的语法。&lt;br /&gt;&lt;br /&gt;很明显，&lt;a href="http://futureangle.blogspot.com"&gt;数组&lt;/a&gt;也是集合，因为foreach命令可以用于数组。对于数组这种特殊情况，System.Array类提供的枚举可以按照下标从0开始的升序来遍历其中的元素。&lt;br /&gt;&lt;br /&gt;在C#中，foreach循环只是一种语法上的简写方式：&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   IEnumerator enumerator = MessageSet.GetEnumerator();&lt;br /&gt;&lt;br /&gt;   string nextMessage;&lt;br /&gt;&lt;br /&gt;   enumerator.MoveNext();&lt;br /&gt;&lt;br /&gt;   while ( (nextMessage = enumerator.Current) != null)&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      DoSomething(nextMessage);     // NB. We only have read access&lt;br /&gt;&lt;br /&gt;                                    // toNextMessage&lt;br /&gt;&lt;br /&gt;      enumerator.MoveNext();&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;注意上述代码段中的闭合花括号。提供它们的原因是为了确保这段代码的作用与前面的foreach循环的作用一样。如果没有使用闭合花括号，这段代码就会有所不同，nextMessage 和 enumerator变量在循环结束时应保留在内存中。&lt;br /&gt;&lt;br /&gt;集合的一个重要方面是枚举作为一个单独的对象返回。它不应是与集合相同的对象，原因是多个枚举可以同时应用到同一个集合上。&lt;br /&gt;&lt;br /&gt;2. 给Vector 结构添加集合支持&lt;br /&gt;下面对第3章的Vector结构进行另一个集合支持扩展。&lt;br /&gt;&lt;br /&gt;在Vector结构中，Vector实例包含3个元素x、y和z，第3章已定义了一个索引符，因此可以把Vector实例当作数组，使用SomeVector[0] 访问x元素，使用SomeVector[1]访问y元素，使用SomeVector[2] 访问z元素。&lt;br /&gt;&lt;br /&gt;下面把Vector结构扩展为一个新代码示例VectorAsCollection项目，编写如下代码，迭代 Vector中的元素：&lt;br /&gt;&lt;br /&gt;foreach (double component in someVector)&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("Component is " + component);&lt;br /&gt;&lt;br /&gt;首先让Vector实现IEnumerable接口，把它标记为一个集合。第一步是修改Vector结构的声明：&lt;br /&gt;&lt;br /&gt;struct Vector : IFormattable, IEnumerable&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   public double x, y, z;&lt;br /&gt;&lt;br /&gt;注意出现了IFormattable接口，因为我们在前面为它添加了字符串格式说明符。下面需要实现IEnumerable接口。&lt;br /&gt;&lt;br /&gt;      public IEnumerator GetEnumerator()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         return new VectorEnumerator(this);&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;GetEnumerator()的实现比较复杂，但它取决于一个新类VectorEnumerator，这个类需要定义。因为VectorEnumerator不是外部代码需要直接看到的类，所以可以把它声明为Vector结构中的私有类，其定义如下所示：&lt;br /&gt;&lt;br /&gt;private class VectorEnumerator : IEnumerator&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   Vector theVector;      // Vector object that this enumerato refers to &lt;br /&gt;&lt;br /&gt;   int location;       // which element of theVector the enumerator is&lt;br /&gt;&lt;br /&gt;                   // currently referring to &lt;br /&gt;&lt;br /&gt;   public VectorEnumerator(Vector theVector)&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      this.theVector = theVector;&lt;br /&gt;&lt;br /&gt;      location =–1;&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;   public bool MoveNext()&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      ++location;&lt;br /&gt;&lt;br /&gt;      return (location &gt; 2) ? false : true;&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;   public object Current&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      get&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         if (location &lt; 0 || location &gt; 2)&lt;br /&gt;&lt;br /&gt;            throw new InvalidOperationException(&lt;br /&gt;&lt;br /&gt;               "The enumerator is either before the first element or " +&lt;br /&gt;&lt;br /&gt;               "after the last element of the Vector");&lt;br /&gt;&lt;br /&gt;         return theVector[(uint)location];&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;   public void Reset()&lt;br /&gt;&lt;br /&gt;private class VectorEnumerator : IEnumerator&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;      location = -1;&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;因为需要一个枚举，所以VectorEnumerator实现IEnumerator接口。它还包含两个成员字段theVector和location。TheVector是与这个枚举相关的Vector(集合)引用，而location是一个int类型的变量，表示枚举在集合中的什么地方引用，换言之，就是Current属性是否应获取矢量的x, y 或 z元素。&lt;br /&gt;&lt;br /&gt;在本例中，要把location当作一个下标，在内部实现enumerator，把Vector当作一个数组来访问。此时，有效的下标是0、1和2。这里可以使用–1，表示枚举在集合开头的前面，使用3表示到达了集合的末尾。因此，在VectorEnumerator构造函数中把这个字段初始化为–1：&lt;br /&gt;&lt;br /&gt;   public VectorEnumerator(Vector theVector)&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      this.theVector = theVector;&lt;br /&gt;&lt;br /&gt;      location =–1;&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;注意构造函数也带有要枚举的Vector实例的引用—— 这是在Vector.GetEnumerator()方法中提供的：&lt;br /&gt;&lt;br /&gt;   public IEnumerator GetEnumerator()&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      return new VectorEnumerator(this);&lt;br /&gt;&lt;br /&gt;   }&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-5385621392247792066?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/5385621392247792066/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/05/c_07.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/5385621392247792066'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/5385621392247792066'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/05/c_07.html' title='C#教程（四十五） 集合'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-846427300857091788</id><published>2009-05-06T17:20:00.000-07:00</published><updated>2009-05-06T17:21:20.774-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='C#、教程、数组列表'/><title type='text'>C#教程（四十四） 数组列表</title><content type='html'>数组列表非常类似于数组，但数组列表是可以增大的，它由System.Collections.ArrayList类来表示。&lt;br /&gt;&lt;br /&gt;ArrayList类与前面的StringBuilder类也有一些相似之处。StringBuilder类可以分配足够的存储单元来存储一定量的字符，允许在该空间中处理字符，ArrayList类也是这样，有足够的存储单元来存储一定量的对象引用，这样，就可以高效地处理这些对象引用。如果试图给ArrayList类添加的对象数超出了容量允许的范围，ArrayList类的容量就会自动增大，新增的内存区域可以存储当前容量两倍的元素，并重新定位这些元素。&lt;br /&gt;&lt;br /&gt;通过指定最初的容量就可以实例化数组列表。例如，假定创建一个Vector列表：&lt;br /&gt;&lt;br /&gt;ArrayList vectors = new ArrayList(20);&lt;br /&gt;&lt;br /&gt;如果没有指定初始大小，则把该大小默认设置为16。&lt;br /&gt;&lt;br /&gt;ArrayList vectors = new ArrayList();   // capacity of 16&lt;br /&gt;&lt;br /&gt;使用Add()方法可以添加元素：&lt;br /&gt;&lt;br /&gt;vectors.Add(new Vector(2,2,2));&lt;br /&gt;&lt;br /&gt;vectors.Add(new Vector(3,5,6));&lt;br /&gt;&lt;br /&gt;ArrayList把所有的元素都当作对象引用。因此可以在ArrayList中存储任何想要存储的对象，但在访问对象时，需要把它们的数据类型转换回合适的数据类型：&lt;br /&gt;&lt;br /&gt;Vector element1 = (Vector)vectors[1];&lt;br /&gt;&lt;br /&gt;这个示例还显示了ArrayList定义的一个索引符，这样，就可以使用类似于数组的语法来访问其元素了。也可以把元素插入到ArrayList中：&lt;br /&gt;&lt;br /&gt;vectors.Insert(1, new Vector(3,2,2));   // inserts at position 1&lt;br /&gt;&lt;br /&gt;还可以重写Insert()方法，如果给定一个ICollection接口引用，就可以把一个集合中的所有元素插入到ArrayList中。&lt;br /&gt;&lt;br /&gt;可以删除元素：&lt;br /&gt;&lt;br /&gt;vvectors.RemoveAt(1);   // removes object at position 1&lt;br /&gt;&lt;br /&gt;也可以为另一个方法Remove()提供对象引用，但这么做，花费的时间会比较长，因为ArrayList要在数组中进行线性搜索，以查找对象。&lt;br /&gt;&lt;br /&gt;注意添加和删除元素都会使后续的元素在内存中作相应的改变，即使不需要对整个ArrayList重新分配内存，也是这样。&lt;br /&gt;&lt;br /&gt;可以通过Capacity属性修改或读取容量：&lt;br /&gt;&lt;br /&gt;vectors.Capacity = 30;&lt;br /&gt;&lt;br /&gt;但要注意，修改容量会给整个ArrayList重新分配新的内存块，其容量大小是所要求的容量大小。&lt;br /&gt;&lt;br /&gt;ArrayList中的元素个数可以通过Count属性获得：&lt;br /&gt;&lt;br /&gt;int nVectors = Vectors.Count;&lt;br /&gt;&lt;br /&gt;如果需要建立一个对象数组，但预先不指定该数组有多大，就可以使用数组列表。此时，可以在ArrayList中构造一个数组，在完成后，如果确实需要把数据当作数组来处理，就再把ArrayList复制到一个数组(例如，把数组传递给一个带有数组参数的方法)。ArrayList和 Array之间的关系在许多方面都类似于StringBuilder和String的关系。&lt;br /&gt;&lt;br /&gt;但与StringBuilder类不同的是，没有把数组列表转换为数组的方法。必须使用一个循环，手工复制引用。但要注意，只能复制引用，不能复制对象，所以不会对性能产生大的影响：&lt;br /&gt;&lt;br /&gt;// vectors is an ArrayList instance being used to store Vector instances&lt;br /&gt;&lt;br /&gt;Vector [] vectorsArray = new Vector[vectors.Count];&lt;br /&gt;&lt;br /&gt;for (int i=0 ; i&lt; vectors.Count ; i++)&lt;br /&gt;&lt;br /&gt;   vectorsArray[i] = (Vector)vectors [i];&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-846427300857091788?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/846427300857091788/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/05/c_06.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/846427300857091788'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/846427300857091788'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/05/c_06.html' title='C#教程（四十四） 数组列表'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-9146981315790736161</id><published>2009-05-04T00:04:00.000-07:00</published><updated>2009-05-04T00:05:20.640-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='C#、教程、集合'/><title type='text'>C#教程（四十三）集合</title><content type='html'>有时需要在数据集合中存储多个数据项，也就是说需要在较大的结构中存储以某种方式相关的数据组或数据集合。C#语言和.NET Framework为在代码中对这类数据值进行排序提供了许多方式。&lt;br /&gt;&lt;br /&gt;本章介绍如何使用对象组，详细讨论数组列表、字典和集合，以及如何在C#代码中正确使用它们，以获得最佳性能。&lt;br /&gt;&lt;br /&gt;9.1  对象组&lt;br /&gt;首先介绍.NET基类中对数据结构的支持，即把许多类似的对象组合起来。最简单的数据结构是数组，第3章介绍了数组的用法。数组是System.Array类的一个实例，但C#包装了这个类的语法。System.Array的优点是可以高效地访问给定下标的元素，这个类有自己的C#语法，使用它编程非常直观。但是，数组有一个缺点，即在实例化时需要指定数组的大小。以后也不能添加、插入或删除元素。数组还必须有一个数字下标，这样才能访问其中的元素。在处理一组Employee记录时，如果需要按照Employee的姓名查找记录，这个下标并不是很有用。&lt;br /&gt;&lt;br /&gt;.NET扩展了对许多在不同环境下使用的其他数据结构的支持，不仅如此，.NET还有许多接口，类实现这些接口后，就可以声明，它们支持某种数据结构类型的全部功能。下面介绍3个这样的结构：&lt;br /&gt;&lt;br /&gt;●       数组列表&lt;br /&gt;&lt;br /&gt;●       集合&lt;br /&gt;&lt;br /&gt;●       字典(有时也称为映射)。&lt;br /&gt;&lt;br /&gt;除了基类System.Array以外，其他的数据结构类都在System.Collections命名空间中。&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;名称System.Collections反映了另一个术语方面的问题。集合常常用于表示任意数据结构，但它还有一个更特殊的含义：实现IEnumerable或Icollection接口的类，本章后面会探讨这种数据结构。本章使用集合来表示这种特殊意义的结构，但其他章节则不是。.NET基类的名称规则迫使我们使用它的一般含义。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-9146981315790736161?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/9146981315790736161/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/05/c.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/9146981315790736161'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/9146981315790736161'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/05/c.html' title='C#教程（四十三）集合'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-3286149419360578469</id><published>2009-04-28T17:27:00.000-07:00</published><updated>2009-04-28T17:28:41.781-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='C#、教程、正则表达式'/><title type='text'>C#教程（四十二）正则表达式(2)</title><content type='html'>该示例的核心是一个方法WriteMatches()，它把MatchCollection中的所有匹配以比较详细的方式显示出来。对于每个匹配，它都会显示该匹配在输入字符串中所在的索引、匹配的字符串和一个略长的字符串，其中包含匹配和输入文本中至多10个外围字符，其中至多有5个字符放在匹配的前面，至多5个字符放在匹配的后面(如果匹配的位置在输入文本的开头或结尾5个字符内，则结果中匹配前后的字符就会少于5个)。换言之，如果要匹配的单词是messaging，靠近输入文本末尾的匹配应是“and messaging of d”，匹配的前后各有5个字符，但位于输入文本的最后一个字上的匹配就应是"g of data" —— 匹配的后面只有一个字符。因为在该字符的后面是字符串的结尾。这个长字符串可以更清楚地表明正则表达式是在什么地方查找到匹配的：&lt;br /&gt;&lt;br /&gt;static void WriteMatches(string text, MatchCollection matches)&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("Original text was: \n\n" + text + "\n");&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("No. of matches: " + matches.Count);&lt;br /&gt;&lt;br /&gt;   foreach (Match nextMatch in matches)&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      int Index = nextMatch.Index;&lt;br /&gt;&lt;br /&gt;      string result = nextMatch.ToString();&lt;br /&gt;&lt;br /&gt;      int charsBefore = (Index &lt; 5) ? Index : 5;&lt;br /&gt;&lt;br /&gt;      int fromEnd = text.Length - Index - result.Length;&lt;br /&gt;&lt;br /&gt;      int charsAfter = (fromEnd &lt; 5) ? fromEnd : 5;&lt;br /&gt;&lt;br /&gt;      int charsToDisplay = charsBefore + charsAfter + result.Length;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      Console.WriteLine("Index: {0}, \tString: {1}, \t{2}",&lt;br /&gt;&lt;br /&gt;         Index, result,&lt;br /&gt;&lt;br /&gt;         text.Substring(Index - charsBefore, charsToDisplay));&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;在这个方法中，处理过程是确定在较长的子字符串中有多少个字符可以显示，而无需超限输入文本的开头或结尾。注意在Match对象上使用了另一个属性Value，它包含标识该匹配的字符串。而且，RegularExpressionsPlayaround只包含名为Find1、Find2等的方法，这些方法根据本节中的示例执行某些搜索操作。例如，Find2在字开头处查找以a开头的字符串：&lt;br /&gt;&lt;br /&gt;static void Find2()&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;string Text = &lt;br /&gt;&lt;br /&gt;@"This comprehensive compendium provides a broad and thorough investigation of all&lt;br /&gt;&lt;br /&gt;aspects of programming with ASP.NET. Entity revised and updated for the 1.1 &lt;br /&gt;&lt;br /&gt;Release of .NET, this book will give you the information you need to master ASP.NET&lt;br /&gt;&lt;br /&gt;And build a dynamic, successful, enterprise Web application.”;&lt;br /&gt;&lt;br /&gt;   string pattern = @"\ba";&lt;br /&gt;&lt;br /&gt;   MatchCollection matches = Regex.Matches(text, pattern, &lt;br /&gt;&lt;br /&gt;     RegexOptions.IgnoreCase);&lt;br /&gt;&lt;br /&gt;   WriteMatches(text, matches);&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;这是一个简单的Main()方法，可以从中编辑并选择一个Find&lt;n&gt;()方法：&lt;br /&gt;&lt;br /&gt;static void Main()&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   Find1();&lt;br /&gt;&lt;br /&gt;   Console.ReadLine();&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;这段代码还使用了命名空间RegularExpressions：&lt;br /&gt;&lt;br /&gt;using System;&lt;br /&gt;&lt;br /&gt;using System.Text.RegularExpressions;&lt;br /&gt;&lt;br /&gt;运行带有Find1()方法的示例，得到如下所示的结果：&lt;br /&gt;&lt;br /&gt;RegularExpressionsPlayaround&lt;br /&gt;&lt;br /&gt;Original text was:&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;This comprehensive compendium provides a broad and thorough investigation of all&lt;br /&gt;&lt;br /&gt;aspects of programming with ASP.NET. Entity revised and updated for the 1.1 &lt;br /&gt;&lt;br /&gt;Release of .NET, this book will give you the information you need to master ASP.NET&lt;br /&gt;&lt;br /&gt;And build a dynamic, successful, enterprise Web application.&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;No. of matches: 1&lt;br /&gt;&lt;br /&gt;Index: 291,     String: application,    Web application. &lt;br /&gt;&lt;br /&gt;8.2.4  匹配、组合和捕获&lt;br /&gt;正则表达式的一个很好的特性是可以把字符组合起来，其方式与C#中的复合语句一样。在C#中，可以把任意数量的语句放在花括号中，把它们组合在一起。其结果就像一个复合语句那样。在正则表达式模式中，也可以把任何字符组合起来(包括元字符和转义序列)，像处理一个字符那样处理它们。惟一的区别是要使用圆括号，而不是花括号，得到的序列称为一个组。&lt;br /&gt;&lt;br /&gt;例如，模式(an)+定位序列an的任意重复。量词+只应用于它前面的一个字符，但因为我们把字符组合起来了，所以它现在把重复的an作为一个单元来对待。(an)+应用到输入文本"bananas came to Europe late in the annals of history"上，会从bananas中选择出anan。另一方面，如果使用an+，则将从annals中选择ann，从bananas中选择出两个an。表达式an+可以提取出an、anan、ananan等，而表达式an+可以提取出an、ann、annn等。&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;在上面的示例中，为什么(an)+从banana中选择的是anan，而没有把单个的an作为一个匹配。匹配规则是不能重叠的。如果有可能重叠，在默认情况下就选择较长的匹配。&lt;br /&gt;&lt;br /&gt;但是，组的功能要比这强大得多。在默认情况下，把模式的一部分组合为一个组时，就要求正则表达式引擎记住可以按照这个组来匹配，也可以按照整个模式来匹配。换言之，可以把组当作一个要匹配的模式来返回，如果要把字符串分解为各个部分，这种模式就是非常有效的。&lt;br /&gt;&lt;br /&gt;例如，URI的格式是&lt;protocol&gt;://&lt;address&gt;:&lt;port&gt;，其中端口是可选的。它的一个示例是http://www.wrox.com:4355。假定要从一个URI中提取协议、地址和端口，而且紧邻URI的后面可能有空白(但没有标点符号)，就可以使用下面的表达式：&lt;br /&gt;&lt;br /&gt;\b(\S+)://(\S+)(?::(\S+))?\b&lt;br /&gt;&lt;br /&gt;该表达式的工作方式如下：首先，前导和尾部的\b序列确保只需要考虑完全是字的文本部分，在这个文本部分中，第一组(\S+)://会选择一个或多个不是空白的字符，其后是://。在HTTP URI的开头会选择出http://。花括号表示把http存储为一个组。后面的序列(\S+)则在上述URI中选择www.wrox.com，这个组在遇到词的结尾(结束\b)时或标记另一个组的冒号(：)时结束。&lt;br /&gt;&lt;br /&gt;下一个组选择端口(本例是:4355)。后面的？表示这个组在匹配中是可选的，如果没有:xxxx，也不会妨碍匹配的标记。这是非常重要的，因为端口号在URI中一般不指定，实际上，在大多数情况下，URI是没有端口号的。但是，事情会比较复杂。我们希望指定冒号可以出现，也可以不出现，但不希望把这个冒号也存储在组中。为此，可以嵌套两个组：内部的(\S+)组选择冒号后面的内容(本例中是4355)，外面的组包含内部的组，后面是一个冒号，该冒号又在序列?:的后面。这个序列表示该组不应保存(只需要保存4355，不需要保存:4355)。不要把这两个冒号混淆了，第一个冒号是序列?:的一部分，表示不保存这个组，第二个冒号是要搜索的文本。&lt;br /&gt;&lt;br /&gt;在下面的字符串上运行该模式，得到的匹配是http://www.wrox.com。&lt;br /&gt;&lt;br /&gt;Hey I've just found this amazing URI at http:// what was it – oh yes http://www.wrox.com&lt;br /&gt;&lt;br /&gt;在这个匹配中，找到了3个组，还有第四个组表示匹配本身。理论上，每个组都可以选择0次、1次或多次匹配。单个的匹配就称为捕获。在第一个组(\S+)中，有一个捕获http，第二个组也有一个捕获www.wrox.com，但第三个组没有捕获，因为在这个URI中没有端口号。&lt;br /&gt;&lt;br /&gt;注意，该字符串包含第二个http://。虽然它匹配于第一个组，但不会被搜索出来，因为整个搜索表达式不匹配于这部分文本。&lt;br /&gt;&lt;br /&gt;前面没有介绍使用组和捕获的任何C#示例，下面提到的.NET类RegularExpressions就通过Group和Capture类支持组和捕获。GroupCollection 和 CaptureCollection分别表示组和捕获的集合，Match类有一个方法Groups()，它返回相应的GroupCollection对象，Group类也相应地执行一个方法Captures()，它返回CaptureCollection对象。对象之间的关系如图8-3所示。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;图  8-3&lt;br /&gt;&lt;br /&gt;把一些字符组合起来后，每次都会返回一个组对象。如果只是希望把一些字符组合起来，作为搜索模式的一部分，实例化对象就会浪费相当大的系统开销。对于单个的组，可以用以字符序列?:开头的组禁止这么做，就像URI示例那样。而对于所有的组，可以在RegEx.Matches()方法上指定RegExOptions.ExplicitCaptures标志，如同前面的示例那样。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-3286149419360578469?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/3286149419360578469/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/04/c2_28.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/3286149419360578469'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/3286149419360578469'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/04/c2_28.html' title='C#教程（四十二）正则表达式(2)'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-3767784570515158779</id><published>2009-04-26T19:14:00.000-07:00</published><updated>2009-04-26T19:15:35.581-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='C#、教程、正则表达式'/><title type='text'>C#教程（四十一）正则表达式(1)</title><content type='html'>正则表达式在各种程序中都有着难以置信的作用，但并不是所有的开发人员都知道这一点。正则表达式被当作一种有特定功能的小型编程语言：在大的字符串表达式中定位一个子字符串。它不是一种新技术，最初它是在UNIX环境中开发的，与Perl一起使用得比较多。Microsoft把它移植到Windows中，到目前为止在脚本语言中用得比较多。但System.Text.Regular- Expressions命名空间中的许多.NET类都支持正则表达式。&lt;br /&gt;&lt;br /&gt;许多您都不太熟悉正则表达式语言，所以本节将主要解释正则表达式和相关的.NET类。如果您很熟悉正则表达式，就可以跳过本节，学习.NET基类的引用。注意，.NET正则表达式引擎是为兼容Perl 5的正则表达式设计的，但有一些新特性。&lt;br /&gt;&lt;br /&gt;8.2.1  正则表达式概述&lt;br /&gt;正则表达式语言是一种专门用于字符串处理的语言。它包含两个功能：&lt;br /&gt;&lt;br /&gt;●       一组用于标识字符类型的转义代码。您可能很熟悉DOS表达式中的*字符表示任意子字符串(例如，DOS命令Dir Re*会列出所有名称以Re开头的文件)。正则表达式使用与*类似的许多序列来表示“任意一个字符”、“一个单词”、“一个可选的字符”等。&lt;br /&gt;&lt;br /&gt;●       一个系统。在搜索操作中，它把子字符串和中间结果的各个部分组合起来。&lt;br /&gt;&lt;br /&gt;●       使用正则表达式，可以对字符串执行许多复杂而高级的操作，例如：&lt;br /&gt;&lt;br /&gt;●       区分(可以是标记或删除)字符串中所有重复的单词，例如，把The computer books books转换为The computer books。&lt;br /&gt;&lt;br /&gt;●       把所有单词都转换为标题格式，例如把this is a Title转换为This Is A Title。&lt;br /&gt;&lt;br /&gt;●       把长于3个字符的所有单词都转换为标题格式，例如把this is a Title转换为This is a Title。&lt;br /&gt;&lt;br /&gt;●       确保句子有正确的大写形式。&lt;br /&gt;&lt;br /&gt;●       区分URI的各个元素(例如http://www.wrox.com，提取出协议、计算机名、文件名等)。&lt;br /&gt;&lt;br /&gt;当然，这些都是可以在C#中用System.String 和 System.Text.StringBuilder执行的任务。但是，在一些情况下，还需要编写相当多的C#代码。如果使用正则表达式，这些代码一般可以压缩为几行代码。实际上，是实例化了一个对象System.Text.RegularExpressions.RegEx(甚至更简单：调用静态的RegEx()方法)，给它传送要处理的字符串和一个正则表达式(这是一个字符串，包含用正则表达式语言编写的指令)，就可以了。&lt;br /&gt;&lt;br /&gt;正则表达式字符串初看起来像是一般的字符串，但其中包含了转义序列和有特定含义的其他字符。例如，序列\b表示一个字的开头和结尾(字的边界)，如果要表示正在查找以字符th开头的字，就可以编写正则表达式\bth(即序列字边界是– t – h)。如果要搜索所有以th结尾的字，就可以编写th\b(序列t – h–字边界)。但是，正则表达式要比这复杂得多，例如，可以在搜索操作中找到存储部分文本的工具性程序。本节仅介绍正则表达式的功能。&lt;br /&gt;&lt;br /&gt;假定应用程序需要把US电话号码转换为国际格式。在美国，电话号码的格式为314-123-1234，常常写作(314) 123-1234。在把这个国家格式转换为国际格式时，必须在电话号码的前面加上+1(美国的国家代码)，并给区域代码加上括号：+1(314) 123-1234。在查找和替换时，这并不复杂，但如果要使用字符串类完成这个转换，就需要编写一些代码(这表示，必须使用System.String上的方法来编写代码)，而正则表达式语言可以构造一个短的字符串来表达上述含义。&lt;br /&gt;&lt;br /&gt;所以，本节只有一个非常简单的示例，我们只考虑如何查找字符串中的某些子字符串，无须考虑如何修改它们。&lt;br /&gt;&lt;br /&gt;8.2.2  RegularExpressionsPlayaround示例&lt;br /&gt;下面将开发一个小示例，执行并显示一些搜索的结果，说明正则表达式的一些特性，以及如何在C#中使用.NET正则表达式引擎。这个示例文档中使用的文本是引自另一本有关XML的Wrox Press书(ASP.NET 1.1高级编程，清华大学出版社翻译出版)：&lt;br /&gt;&lt;br /&gt;string Text = &lt;br /&gt;&lt;br /&gt;@"This comprehensive compendium provides a broad and thorough investigation of all&lt;br /&gt;&lt;br /&gt;aspects of programming with ASP.NET. Entirely revised and updated for the 1.1 &lt;br /&gt;&lt;br /&gt;Release of .NET, this book will give you the information you need to master ASP.NET&lt;br /&gt;&lt;br /&gt;and build a dynamic, successful, enterprise Web application.";&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;不考虑换行，则上面的表达式是合法的C#代码—— 说明了使用字符串时应在前面加上符号@。&lt;br /&gt;&lt;br /&gt;我们把这个文本称为输入字符串。为了说明正则表达式.NET类，我们先进行一次纯文本的搜索，这次搜索不带任何转义序列或正则表达式命令。假定要查找所有的字符串ion，把这个搜索字符串称为模式。使用正则表达式和上面声明的变量Text，编写出下面的代码：&lt;br /&gt;&lt;br /&gt;string Pattern = "ion";&lt;br /&gt;&lt;br /&gt;MatchCollection Matches = Regex.Matches(Text, Pattern,&lt;br /&gt;&lt;br /&gt;                                    RegexOptions.IgnoreCase |&lt;br /&gt;&lt;br /&gt;                                    RegexOptions.ExplicitCapture);&lt;br /&gt;&lt;br /&gt;foreach (Match NextMatch in Matches)&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   Console.WriteLine(NextMatch.Index);&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;在这段代码中，使用了System.Text.RegularExpressions命名空间中Regex类的静态方法Matches()。这个方法的参数是一些输入文本、一个模式和RegexOptions枚举中的一组可选标志。在本例中，指定所有的搜索都不应区分大小写。另一个标记ExplicitCapture 改变了收集匹配的方式，对于本例，这样可以使搜索的效率更高，其原因详见后面的内容(尽管它还有这里没有介绍的其他用法)。Matches()返回MatchCollections对象的引用。匹配是一个技术术语，表示表达式中模式实例的查找结果，用System.Text.RegularExpressions.Match来代表。因此，我们返回一个包含所有匹配的MatchCollection，每个匹配都用一个Match对象来表示。在上面的代码中，只是在集合中迭代，使用Match类的Index属性，返回输入文本中匹配所在的索引。运行这段代码，将得到3个匹配。&lt;br /&gt;&lt;br /&gt;除了一些新的.NET基类外，这些内容都不是新的。但正则表达式的功能主要取决于模式字符串。原因是模式字符串不仅仅包含纯文本。如前所述，它还可以包含元字符和转义序列，其中元字符是给出命令的特定字符，而转义序列的工作方式与C#的转义序列相同，它们都是以反斜杠\开头的字符，具有特殊的含义。&lt;br /&gt;&lt;br /&gt;例如，假定要查找以n开头的字，就可以使用转义序列\b，它表示一个字的边界(字的边界是以某个字母数字表的字符开头，或者后面是一个空白字符或标点符号)。可以编写如下代码：&lt;br /&gt;&lt;br /&gt;string Pattern = @"\bn";&lt;br /&gt;&lt;br /&gt;MatchCollection Matches = Regex.Matches(Text, Pattern,&lt;br /&gt;&lt;br /&gt;                                    RegexOptions.IgnoreCase | &lt;br /&gt;&lt;br /&gt;                                    RegexOptions.ExplicitCapture);&lt;br /&gt;&lt;br /&gt;注意字符串前面的符号@。要在运行时把\b传递给.NET正则表达式引擎，反斜杠\不应被C#编译器解释为转义序列。如果要查找以序列ion结尾的字，可以使用下面的代码：&lt;br /&gt;&lt;br /&gt;string Pattern = @"ion\b";&lt;br /&gt;&lt;br /&gt;如果要查找以字母a开头，以序列ion结尾的所有字 (在本例中仅有一个匹配application)就必须在上面的代码中添加一些内容。显然，我们需要一个以\ba开头，以ion\b结尾的模式，但中间的内容怎么办？需要告诉应用程序在a和ion中间的内容可以是任意长度的任意字符，只要这些字符不是空白即可。实际上，正确的模式如下所示。&lt;br /&gt;&lt;br /&gt;string Pattern = @"\ba\S*ion\b";&lt;br /&gt;&lt;br /&gt;使用正则表达式要习惯的一点是，对像这样怪异的字符序列见怪不怪。但这个序列的工作是非常逻辑化的。转义序列\S表示任何不是空白的字符。*称为数量词，其含义是前面的字符可以重复任意次，包括0次。序列\S*表示任意个不是空白的字符。因此，上面的模式匹配于以a开头，以ion结尾的任何单词。&lt;br /&gt;&lt;br /&gt;表8-4是可以使用的一些主要的特定字符或转义序列，但这个表并不完整，要查看完整的列表请参考MSDN文档。&lt;br /&gt;&lt;br /&gt;表  8-4&lt;br /&gt;&lt;br /&gt;符  号&lt;br /&gt; 含  义&lt;br /&gt; 示  例&lt;br /&gt; 匹配的示例&lt;br /&gt; &lt;br /&gt;^&lt;br /&gt; 输入文本的开头&lt;br /&gt; ^B&lt;br /&gt; B，但只能是文本中的第一个字符 &lt;br /&gt; &lt;br /&gt;$&lt;br /&gt; 输入文本的结尾&lt;br /&gt; X$&lt;br /&gt; X， 但只能是文本中的最后一个字符&lt;br /&gt; &lt;br /&gt;.&lt;br /&gt; 除了换行字符(\n)以外的所有单个字符&lt;br /&gt; i.ation&lt;br /&gt; isation、ization &lt;br /&gt; &lt;br /&gt;*&lt;br /&gt; 可以重复0次或多次的前导字符&lt;br /&gt; ra*t&lt;br /&gt; rt、rat、raat和raaat等&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;                                                                 (续表)    &lt;br /&gt;&lt;br /&gt;+&lt;br /&gt; 可以重复1次或多次的前导字符&lt;br /&gt; ra+t&lt;br /&gt; rat、raat和 raaat等(但不能是rt)&lt;br /&gt; &lt;br /&gt;?&lt;br /&gt; 可以重复0次或1次的前导字符&lt;br /&gt; ra?t&lt;br /&gt; 只有rt 和 rat匹配&lt;br /&gt; &lt;br /&gt;\s&lt;br /&gt; 任何空白字符&lt;br /&gt; \sa&lt;br /&gt; [space]a、\ta、\na (\t 和 \n 与C#的\t 和 \n含义相同)&lt;br /&gt; &lt;br /&gt;\S&lt;br /&gt; 任何不是空白的字符&lt;br /&gt; \SF&lt;br /&gt; aF、rF、cF、但不能是\tf&lt;br /&gt; &lt;br /&gt;\b&lt;br /&gt; 字边界&lt;br /&gt; ion\b&lt;br /&gt; 以ion结尾的任何字&lt;br /&gt; &lt;br /&gt;\B&lt;br /&gt; 不是字边界的位置&lt;br /&gt; \BX\B&lt;br /&gt; 字中间的任何X&lt;br /&gt; &lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;如果要搜索一个元字符，也可以通过带有反斜杠的转义字符来表示。例如，.(一个句点)表示除了换行字符以外的任何字符，而\.表示一个点。&lt;br /&gt;&lt;br /&gt;可以把可替换的字符放在方括号中，请求匹配包含这些字符。例如，[1|c]表示字符可以是l或c。如果要搜索map或man，可以使用序列ma[n|p]。在方括号中，也可以指定一个范围，例如[a-z]表示所有的小写字母，[A-E]表示A到E之间的所有大写字母，[0-9]表示一个数字。如果要搜索一个整数(该序列只包含0到9的字符)，就可以编写[0-9]+(注意，使用+字符表示至少要有这样一个数字，但可以有多个数字，所以9、 83和 854等都是匹配的)。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-3767784570515158779?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/3767784570515158779/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/04/c1_26.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/3767784570515158779'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/3767784570515158779'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/04/c1_26.html' title='C#教程（四十一）正则表达式(1)'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-5686975150267205485</id><published>2009-04-23T17:16:00.000-07:00</published><updated>2009-04-23T17:17:19.169-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='C#、教程、格式化字符串'/><title type='text'>C#教程（四十）格式化字符串</title><content type='html'>前面的代码示例中编写了许多类和结构，对这些类和结构执行ToString()方法，都是为了能显示给定变量的内容。但是，用户常常希望以各种可能的方式显示变量的内容，在不同的文化或地区背景中有不同的格式。.NET基类System.DateTime就是最明显的一个示例：可以把日期显示为14 February 2002、14 Feb 2002、2/14/02(美国)、14/2/02(英国)或14. February 2002(德国)。&lt;br /&gt;&lt;br /&gt;同样，对于第3章中编写的Vector结构，执行Vector.ToString()方法，是为了以(4, 56, 8)格式显示矢量。编写矢量的另一个非常常用的方式是4i + 56j + 8k。如果要使类的用户友好性比较高，就需要使用某些工具以用户希望使用的方式显示它们的字符串表示。.NET运行库定义了一种标准方式：使用一个接口IFormattable，本节的主题就是说明如何把这个重要特性添加到类和结构上。&lt;br /&gt;&lt;br /&gt;在显示一个变量时，常常需要指定它的格式，此时我们经常调用Console.WriteLine()方法。因此，我们把这个方法作为示例，但这里的讨论适用于格式化字符串的大多数情况。例如，如果要在列表框或文本框中显示一个变量的值，一般要使用String.Format()方法来获得该变量的合适字符串表示，但实际上所用的格式说明符所需的格式与传递给Console.WriteLine()的格式相同，因此本节把Console.WriteLine()作为一个示例来说明。首先看看在为基本类型提供格式字符串时会发生什么，再看看如何把自己的类和结构的格式说明符拖动到过程中。&lt;br /&gt;&lt;br /&gt;第2章在Console.Write()和 Console.WriteLine()中使用了格式字符串：&lt;br /&gt;&lt;br /&gt;double d = 13.45;&lt;br /&gt;&lt;br /&gt;int i = 45;&lt;br /&gt;&lt;br /&gt;Console.WriteLine("The double is {0,10:E} and the int contains {1}", d, i);&lt;br /&gt;&lt;br /&gt;格式字符串本身大都由要显示的文本组成，但只要有要格式化的变量，它在参数列表中的下标就必须放在括号中。在括号中还可以有与该项目的格式相关的其他信息，例如可以包含：&lt;br /&gt;&lt;br /&gt;●       该项目要占用的字符数，这个信息的前面应有一个逗号，负值表示该项目应左对齐，正值表示该项目应右对齐。如果该项目占用的字符数比给定的多，其内容也会完整地显示出来。&lt;br /&gt;&lt;br /&gt;●       格式说明符也可以显示出来。它的前面应有一个冒号，表示应如何格式化该项目。例如，把一个数字格式化为货币，或者以科学计数法显示。&lt;br /&gt;&lt;br /&gt;第2章简要介绍了数字类型的常见格式说明符，表8-3再次引用该表。&lt;br /&gt;&lt;br /&gt;表  8-3&lt;br /&gt;&lt;br /&gt;格式符&lt;br /&gt; 应  用&lt;br /&gt; 含  义&lt;br /&gt; 示  例&lt;br /&gt; &lt;br /&gt;C&lt;br /&gt; 数字类型 &lt;br /&gt; 专用场合的货币值&lt;br /&gt; $4834.50 (USA)&lt;br /&gt;&lt;br /&gt;£4834.50 (UK)&lt;br /&gt; &lt;br /&gt;D&lt;br /&gt; 只用于整数类型&lt;br /&gt; 一般的整数&lt;br /&gt; 4834&lt;br /&gt; &lt;br /&gt;E&lt;br /&gt; 数字类型&lt;br /&gt; 科学计数法&lt;br /&gt; 4.834E+003&lt;br /&gt; &lt;br /&gt;F&lt;br /&gt; 数字类型&lt;br /&gt; 小数点后的位数固定&lt;br /&gt; 4384.50&lt;br /&gt; &lt;br /&gt;G&lt;br /&gt; 数字类型&lt;br /&gt; 一般的数字&lt;br /&gt; 4384.5&lt;br /&gt; &lt;br /&gt;N&lt;br /&gt; 数字类型&lt;br /&gt; 通常是专用场合的数字 格式&lt;br /&gt; 4,384.50 (UK/USA)&lt;br /&gt;&lt;br /&gt;4 384,50 (欧洲大陆)&lt;br /&gt; &lt;br /&gt;P&lt;br /&gt; 数字类型&lt;br /&gt; 百分比计数法&lt;br /&gt; 432,000.00%&lt;br /&gt; &lt;br /&gt;X&lt;br /&gt; 只用于整数类型&lt;br /&gt; 16进制格式&lt;br /&gt; 1120 (如果要显示0x1120，需要写上0x)&lt;br /&gt; &lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;如果要在整数上添加前导0，可以使用格式说明符0重复指定所需的次数。例如，格式说明符0000会把3显示为0003，99显示为0099。&lt;br /&gt;&lt;br /&gt;这里不能给出完整的列表，因为其他数据类型有自己的格式说明符。本节的主要目的是说明如何为自己的类定义格式说明符。&lt;br /&gt;&lt;br /&gt;1. 字符串的格式化&lt;br /&gt;作为如何格式化字符串的一个示例，看看执行下面的语句会得到什么结果：&lt;br /&gt;&lt;br /&gt;Console.WriteLine("The double is {0,10:E} and the int contains {1}", d, i);&lt;br /&gt;&lt;br /&gt;Console.WriteLine()只是把参数的完整列表传送给静态方法String.Format()，如果要在字符串中以其他方式格式化这些值，例如显示在一个文本框中，也可以调用这个方法。带有3个参数的WriteLine()重载方法的执行方式如下：&lt;br /&gt;&lt;br /&gt;// Likely implementation of Console.WriteLine()&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;public void WriteLine(string format, object arg0, object arg1)&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   Console.WriteLine(string.Format(format, arg0, arg1));&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;上面的代码依次调用了带有1个参数的重载方法WriteLine()，仅显示了传递过来的字符串的内容，没有对它进行进一步的格式化。&lt;br /&gt;&lt;br /&gt;String.Format()现在需要用对应对象的合适字符串表示来替换每个格式说明符，构造最终的字符串。但是，如前所述，对于这个建立字符串的过程，需要StringBuilder实例，而不是字符串实例。在这个示例中，StringBuilder实例是用字符串的第一部分(即文本“The double is”)创建和初始化的。然后调用StringBuilder.AppendFormat()方法，传递第一个格式说明符“{0,10:E}”和相应的对象double，把这个对象的字符串表示添加到构造好的字符串中，这个过程会继续重复调用StringBuilder.Append() 和 StringBuilder.AppendFormat()方法，直到得到了全部格式化好的字符串为止。&lt;br /&gt;&lt;br /&gt;下面的内容比较有趣。因为StringBuilder.AppendFormat()需要指出如何格式化对象，所以它做的第一件事是检查对象，确定它是否执行System命名空间中的接口IFormattable。试着把这个对象转换为接口，会发现转换很容易成功，或者使用C#关键字is，也能实现此转换。如果转换失败，AppendFormat()只会调用对象的ToString()方法，其中所有的对象都继承了System.Object或被重写。在前面给出的编写各种类和结构的示例中，执行过程都是这样，因为我们编写的类都没有执行这个接口。这就是在前面的章节中，Object.ToString()的重写方法允许在Console.WriteLine()语句中显示类和结构如Vector的原因。&lt;br /&gt;&lt;br /&gt;但是，所有预定义的基本数字类型都执行这个接口，对于这些类型，特别是这个示例中的double和int，就不会调用继承了System.Object的基本ToString()方法。为了理解这个过程，需要了解IFormattable接口。&lt;br /&gt;&lt;br /&gt;IFormattable只定义了一个方法，该方法也叫作ToString()，它带有两个参数，这与System. Object版本的ToString()不同，它不带参数。下面是IFormattable的定义：&lt;br /&gt;&lt;br /&gt;interface IFormattable&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   string ToString(string format, IFormatProvider formatProvider);&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;这个ToString()重载方法的第一个参数是一个字符串，它指定要求的格式。换言之，它是字符串的说明符部分，放在字符串的｛｝中，该参数最初传递给Console.WriteLine()或 String. Format()。例如，在本例中，最初的语句如下：&lt;br /&gt;&lt;br /&gt;Console.WriteLine("The double is {0,10:E} and the int contains {1}", d, i);&lt;br /&gt;&lt;br /&gt;在计算第一个说明符{0,10:E}时，在double变量d上调用这个重载方法，传递给它的第一个参数是"E"。StringBuilder.AppendFormat()传递的总是显示在原始字符串的合适格式说明符内冒号后面的文本。&lt;br /&gt;&lt;br /&gt;本书不讨论ToString()的第2个参数，它是执行接口IFormatProvider的对象引用。这个接口提供了ToString()在格式化对象时需要考虑的更多信息—— 一般包括文化背景信息(.NET文化背景类似于Windows的背景，如果格式化货币或日期，就需要这些信息)。如果直接从源代码中调用这个ToString()重载方法，就需要提供这样一个对象。但StringBuilder.AppendFormat()为这个参数传递一个空值。如果formatProvider为空，ToString()就要使用系统设置中指定的文化背景信息。&lt;br /&gt;&lt;br /&gt;现在回过头来看看本例。第一个要格式化的项目是double，对此要求使用指数计数法，格式说明符为E。如前所述，StringBuilder.AppendFormat()方法会建立执行IFormattable接口的对象double，因此要调用带有两个参数的ToString()重载方法，其第一个参数是字符串“E”。第二个参数为空。现在double的这个方法在执行时，会考虑要求的格式和当前的文化背景，以合适的格式返回double的字符串表示。StringBuilder.AppendFormat()则按照需要在返回的字符串中添加前导空格，使之共有10个字符。&lt;br /&gt;&lt;br /&gt;下一个要格式化的对象是int，它不需要任何特殊的格式 (格式说明符是{1})。由于没有格式要求，StringBuilder.AppendFormat()会给该格式字符串传递一个空引用，并适当地响应带有两个参数的int.ToString()重载方法，由于没有特殊的格式要求，所以也可以调用不带参数的ToString()方法。&lt;br /&gt;&lt;br /&gt;整个过程如图8-2所示。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;图  8-2&lt;br /&gt;&lt;br /&gt;2. FormattableVector示例&lt;br /&gt;前面介绍了如何构造格式字符串，下面扩展本书前面的Vector示例，以多种方式格式化矢量。这个示例的代码可以从www.wrox.com上下载。只要理解了所涉及到的规则，实际上编写代码就相当简单了。我们只需要实现IFormattable，提供由该接口定义的ToString()重载方法   即可。&lt;br /&gt;&lt;br /&gt;要提供的格式说明符如下：&lt;br /&gt;&lt;br /&gt;●       N 应解释为一个请求，以提供一个数字，即矢量的模，它是其成员的平方和，在数学上等于Vector的长度的平方，通常放在两个竖杠的中间：||34.5||。&lt;br /&gt;&lt;br /&gt;●       VE 应解释为以科学计数法显示每个成员的一个请求，就像说明符E应用于double，就可以表示为 (2.3E+01, 4.5E+02, 1.0E+00)。&lt;br /&gt;&lt;br /&gt;●       IJK应解释为以格式23i + 450j + 1k显示矢量的一个请求。 &lt;br /&gt;&lt;br /&gt;●       其他内容应仅返回Vector的默认表示方法 (23, 450, 1.0)。&lt;br /&gt;&lt;br /&gt;为了简单起见，我们不以IJK和科学计数法的格式执行任何选项以显示矢量，而是以不区分大小写的方式来测试说明符，允许使用ijk和IJK。注意，使用什么字符串表示格式说明符完全取决于用户。&lt;br /&gt;&lt;br /&gt;为此，首先修改Vector的声明，使之执行IFormattable：&lt;br /&gt;&lt;br /&gt;struct Vector : IFormattable&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   public double x, y, z;&lt;br /&gt;&lt;br /&gt;下面添加带有2个参数的ToString()重载的实现：&lt;br /&gt;&lt;br /&gt;public string ToString(string format, IFormatProvider formatProvider)&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   if (format == null)&lt;br /&gt;&lt;br /&gt;      return ToString();&lt;br /&gt;&lt;br /&gt;   string formatUpper = format.ToUpper();&lt;br /&gt;&lt;br /&gt;   switch (formatUpper)&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      case "N":&lt;br /&gt;&lt;br /&gt;         return "|| " + Norm().ToString() + " ||";&lt;br /&gt;&lt;br /&gt;      case "VE":&lt;br /&gt;&lt;br /&gt;         return String.Format("( {0:E}, {1:E}, {2:E} )", x, y, z);&lt;br /&gt;&lt;br /&gt;      case "IJK":&lt;br /&gt;&lt;br /&gt;         StringBuilder sb = new StringBuilder(x.ToString(), 30);&lt;br /&gt;&lt;br /&gt;         sb.Append(" i + ");&lt;br /&gt;&lt;br /&gt;         sb.Append(y.ToString());&lt;br /&gt;&lt;br /&gt;         sb.Append(" j + ");&lt;br /&gt;&lt;br /&gt;         sb.Append(z.ToString());&lt;br /&gt;&lt;br /&gt;         sb.Append(" k");&lt;br /&gt;&lt;br /&gt;         return sb.ToString();&lt;br /&gt;&lt;br /&gt;      default:&lt;br /&gt;&lt;br /&gt;         return ToString();&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;这就是我们需要编写的代码。注意在调用任何方法前，应防止使用格式字符串为空的参数。我们希望这个方法尽可能健壮，所有基本类型的格式说明符都是不区分大小写的，其他开发人员也希望能使用我们的类。对于格式说明符VE，需要把每个成员格式化为科学计数法，所以再次使用String.Format ()方法。字段x、y和z都是double类型。对于IJK格式限定符，把几个子字符串添加到字符串中，使用StringBuilder对象来提高性能。&lt;br /&gt;&lt;br /&gt;为了保证完整，也可以再次使用前面开发的无参数的ToString()重载方法：&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;public override string ToString()&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   return "( " + x + " , " + y + " , " + z + " )"; &lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;最后，需要添加一个Norm()方法，计算矢量的平方(模)，因为在开发Vector结构时，没有提供这个方法：&lt;br /&gt;&lt;br /&gt;public double Norm()&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   return x*x + y*y + z*z;&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;下面用一些合适的测试代码测试可格式化的矢量：&lt;br /&gt;&lt;br /&gt;static void Main()&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   Vector v1 = new Vector(1,32,5);&lt;br /&gt;&lt;br /&gt;   Vector v2 = new Vector(845.4, 54.3, –7.8);&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("\nIn IJK format,\nv1 is {0,30:IJK}\nv2 is {1,30:IJK}",&lt;br /&gt;&lt;br /&gt;                      v1, v2);&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("\nIn default format,\nv1 is {0,30}\nv2 is {1,30}", v1,&lt;br /&gt;&lt;br /&gt;                      v2);&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("\nIn VE format\nv1 is {0,30:VE}\nv2 is {1,30:VE}", v1,&lt;br /&gt;&lt;br /&gt;                      v2);&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("\nNorms are:\nv1 is {0,20:N}\nv2 is {1,20:N}", v1,&lt;br /&gt;&lt;br /&gt;                      v2);&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;运行这个示例的结果如下所示：&lt;br /&gt;&lt;br /&gt;FormattableVector&lt;br /&gt;&lt;br /&gt;In IJK format,&lt;br /&gt;&lt;br /&gt;v1 is               1 i + 32 j + 5 k&lt;br /&gt;&lt;br /&gt;v2 is        845.4 i + 54.3 j + -7.8 k&lt;br /&gt;&lt;br /&gt;In default format,&lt;br /&gt;&lt;br /&gt;v1 is                 ( 1 , 32 , 5 )&lt;br /&gt;&lt;br /&gt;v2 is          ( 845.4 , 54.3 , -7.8 )&lt;br /&gt;&lt;br /&gt;In VE format&lt;br /&gt;&lt;br /&gt;v1 is ( 1.000000E+000, 3.200000E+001, 5.000000E+000 )&lt;br /&gt;&lt;br /&gt;v2 is ( 8.454000E+002, 5.430000E+001,–7.800000E+000 )&lt;br /&gt;&lt;br /&gt;Norms are:&lt;br /&gt;&lt;br /&gt;v1 is           || 1050 ||&lt;br /&gt;&lt;br /&gt;v2 is      || 717710.49 ||&lt;br /&gt;&lt;br /&gt;这说明了选用的定制格式说明符是正确的。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-5686975150267205485?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/5686975150267205485/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/04/c_23.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/5686975150267205485'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/5686975150267205485'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/04/c_23.html' title='C#教程（四十）格式化字符串'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-4110219383913660152</id><published>2009-04-21T17:17:00.000-07:00</published><updated>2009-04-21T17:18:48.524-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='C#、教程、创建字符串'/><title type='text'>C#教程（四十） 创建字符串</title><content type='html'>string类是一个功能非常强大的类，它执行许多很有用的方法。但是，string类存在一个问题：重复修改给定的字符串，效率会很低，它实际上是一个不可变的数据类型，一旦对字符串对象进行了初始化，该字符串对象就不能改变了。修改字符串内容的方法和运算符实际上是创建一个新的字符串，如果必要，可以把旧字符串的内容复制到新字符串中。例如，下面的代码：&lt;br /&gt;&lt;br /&gt;string greetingText = "Hello from all the guys at Wrox Press. ";&lt;br /&gt;&lt;br /&gt;greetingText += "We do hope you enjoy this book as much as we enjoyed writing it.";&lt;br /&gt;&lt;br /&gt;在执行这段代码时，首先，创建一个System.String类型的对象，并初始化为文本“Hello from all the people at Wrox Press. ”。注意该行最后的空格。此时.NET 运行库会为该字符串分配足够的内存来保存这个文本(39个字符)，再设置变量greetingText，表示这个字符串实例。&lt;br /&gt;&lt;br /&gt;从语法上看，下一行代码是把更多的文本添加到字符串中。实际上并非如此，而是创建一个新字符串实例，给它分配足够的内存，以保存合并起来的文本(共103个字符)。最初的文本“Hello from all the people at Wrox Press. ”复制到这个新字符串中，再加上额外的文本“We do hope you enjoy this book as much as we enjoyed writing it”。然后更新存储在变量greetingText中的地址，使变量正确地指向新的字符串对象。旧的字符串对象被撤销了引用——不再有变量引用它，下一次垃圾收集器清理应用程序中所有未使用的对象时，就会删除它。&lt;br /&gt;&lt;br /&gt;这本身还不坏，但假定要对这个字符串加密，在字母表中，用ASCII码中的字符替代其中的每个字母(标点符号除外)，以便以后作为非常简单的加密模式的一部分，就会把该字符串变成“Ifmmp gspn bmm uif hvst bu Xspy Qsftt. Xf ep ipqf zpv fokpz uijt cppl bt nvdi bt xf fokpzfe xsjujoh ju.”。完成这个任务有好几种方式，但最简单、最高效的一种(假定只使用String类)是使用String.Replace()方法，把字符串中指定的子字符串用另一个子字符串代替。使用Replace()，加密文本的代码如下所示：&lt;br /&gt;&lt;br /&gt;string greetingText = "Hello from all the guys at Wrox Press. ";&lt;br /&gt;&lt;br /&gt;greetingText += "We do hope you enjoy this book as much as we enjoyed writing it.";&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;for(int i = (int)'z'; i&gt;=(int)'a' ; i––)&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   char old1 = (char)i;&lt;br /&gt;&lt;br /&gt;   char new1 = (char)(i+1);&lt;br /&gt;&lt;br /&gt;   greetingText = greetingText.Replace(old1, new1);&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;for(int i = (int)'Z'; i&gt;=(int)'A' ; i––)&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   char old1 = (char)i;&lt;br /&gt;&lt;br /&gt;   char new1 = (char)(i+1);&lt;br /&gt;&lt;br /&gt;   greetingText = greetingText.Replace(old1, new1);&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;Console.WriteLine("Encoded:\n" + greetingText);&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;为了简单起见，这段代码没有把Z换成A，或把z换成a。这些字符分别编码为[和{。&lt;br /&gt;&lt;br /&gt;Replace()以一种智能化的方式工作，在某种程度上，它并没有实际创建一个新字符串，除非要对旧字符串进行某些改变。原来的字符串包含23个不同的小写字母，和3个不同的大写字母。所以Replace()就分配一个新字符串，共26次，每个新字符串都包含103个字符。因此加密过程需要在堆上有一个能存储总共2678个字符的字符串对象，最终将等待被垃圾收集！显然，如果使用字符串进行文字处理，应用程序就会有严重的性能问题。&lt;br /&gt;&lt;br /&gt;为了解决这个问题，Microsoft提供了System.Text.StringBuilder类。StringBuilder不像String功能那么强大，后者已有那么多所支持的方法。在StringBuilder上可以进行的处理仅限于替换和添加或删除字符串中的文本。但是，它的工作方式非常高效。&lt;br /&gt;&lt;br /&gt;在构造一个字符串时，要给它分配足够的内存来保存字符串，但StringBuilder通常分配的内存会比需要的更多。可以选择显式指定要分配多少内存，但如果没有显式指定，存储单元量在默认情况下就根据StringBuilder初始化时的字符串长度来确定。它有两个主要的属性：&lt;br /&gt;&lt;br /&gt;●       Length指定字符串的实际长度；&lt;br /&gt;&lt;br /&gt;●       Capacity是字符串占据存储单元的长度。&lt;br /&gt;&lt;br /&gt;对字符串的修改就在赋予StringBuilder实例的存储单元中进行，这就大大提高了添加子字符串和替换单个字符的效率。删除或插入子字符串仍然效率低下，因为这需要移动随后的字符串。只有执行扩展字符串容量的操作，才会给字符串分配需要的新内存，才可能移动整个包含的字符串。在编写本书的过程中，Microsoft并没有说明会添加多少额外的容量，但从经验来看，StringBuilder如果检测到容量超出，且该容量中没有显示设置新值，就会使自己的容量翻倍。&lt;br /&gt;&lt;br /&gt;例如，如果使用StringBuilder对象构造最初的欢迎字符串，可以编写下面的代码：&lt;br /&gt;&lt;br /&gt;StringBuilder greetingBuilder =&lt;br /&gt;&lt;br /&gt;   new StringBuilder("Hello from all the guys at Wrox Press. ", 150);&lt;br /&gt;&lt;br /&gt;greetingBuilder.Append("We do hope you enjoy this book as much as we enjoyed writing it");&lt;br /&gt;&lt;br /&gt;                        &lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;为了使用StringBuilder类，需要在代码中引用System.Text。&lt;br /&gt;&lt;br /&gt;在这段代码中，为StringBuilder设置的初始容量是150。最好把容量设置为字符串可能的最大长度，确保StringBuilder不需要重新分配内存，因为其容量足够用了。理论上，可以设置尽可能大的数字，足够给该容量传送一个int，但如果实际上给字符串分配20亿个字符的空间(这是StringBuilder实例允许拥有的最大理论空间)，系统就可能会没有足够的内存。&lt;br /&gt;&lt;br /&gt;执行上面的代码，首先创建一个StringBuilder对象，如图8-1所示。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;图  8-1&lt;br /&gt;&lt;br /&gt;在调用Append()方法时，其他文本就放在空的空间中，不需要分配更多的内存。但是，多次替换文本才能获得使用StringBuilder所带来的性能提高。例如，如果要以前面的方式加密文本，就可以执行整个加密过程，无须分配更多的内存：&lt;br /&gt;&lt;br /&gt;StringBuilder greetingBuilder =&lt;br /&gt;&lt;br /&gt;   new StringBuilder("Hello from all the guys at Wrox Press. ", 150);&lt;br /&gt;&lt;br /&gt;greetingBuilder.Append("We do hope you enjoy this book as much as we enjoyed writing it");&lt;br /&gt;&lt;br /&gt;                        &lt;br /&gt;&lt;br /&gt;for(int i = (int)'z'; i&gt;=(int)'a' ; i--)&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   char old1 = (char)i;&lt;br /&gt;&lt;br /&gt;   char new1 = (char)(i+1);&lt;br /&gt;&lt;br /&gt;   greetingBuilder = greetingBuilder.Replace(old1, new1);&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;for(int i = (int)'Z'; i&gt;=(int)'A' ; i––   )&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   char old1 = (char)i;&lt;br /&gt;&lt;br /&gt;   char new1 = (char)(i+1);&lt;br /&gt;&lt;br /&gt;   greetingBuilder = greetingBuilder.Replace(old1, new1);&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;Console.WriteLine("Encoded:\n" + greetingBuilder.ToString());&lt;br /&gt;&lt;br /&gt;这段代码使用了StringBuilder.Replace()方法，它的功能与String.Replace()一样，但不需要在过程中复制字符串。在上述代码中，为存储字符串而分配的总存储单元是150个字符，用于StringBuilder实例以及在最后一个Console.WriteLine()语句中内部执行字符串操作期间分配的内存。&lt;br /&gt;&lt;br /&gt;一般，使用StringBuilder就可以执行字符串的任何操作，String可以用于存储字符串或显示最终结果等。&lt;br /&gt;&lt;br /&gt;StringBuilder成员&lt;br /&gt;前面介绍了StringBuilder的一个构造函数，它的参数是一个初始字符串及该字符串的容量。还有几个其他的StringBuilder构造函数，在这些StringBuilder构造函数中，只能提供一个字    符串：&lt;br /&gt;&lt;br /&gt;StringBuilder sb = new StringBuilder("Hello");&lt;br /&gt;&lt;br /&gt;或者用给定的容量创建一个空的StringBuilder：&lt;br /&gt;&lt;br /&gt;StringBuilder sb = new StringBuilder(20);&lt;br /&gt;&lt;br /&gt;除了前面介绍的Length 和 Capacity属性外，还有一个只读属性MaxCapacity，它表示对给定的StringBuilder实例的限制，最多可以有多少容量。在默认情况下，这由int.MaxValue给定(大约20亿，如前所述)，在构造StringBuilder对象时，也可以把这个值设置为较低的值：&lt;br /&gt;&lt;br /&gt;// This will both set initial capacity to 100, but the max will be 500.&lt;br /&gt;&lt;br /&gt;// Hence, this StringBuilder can never grow to more than 500 characters,&lt;br /&gt;&lt;br /&gt;// otherwise it will raise exception if you try to do that.&lt;br /&gt;&lt;br /&gt;StringBuilder sb = new StringBuilder(100, 500);&lt;br /&gt;&lt;br /&gt;还可以随时显式地设置容量，但如果把这个值设置为低于字符串的当前长度，或者超出了最大容量，就会抛出一个异常：&lt;br /&gt;&lt;br /&gt;StringBuilder sb = new StringBuilder("Hello");&lt;br /&gt;&lt;br /&gt;sb.Capacity = 100;&lt;br /&gt;&lt;br /&gt;主要的StringBuilder方法如表8-2所示。 &lt;br /&gt;&lt;br /&gt;表  8-2&lt;br /&gt;&lt;br /&gt;名    称&lt;br /&gt; 作    用&lt;br /&gt; &lt;br /&gt;Append()&lt;br /&gt; 给当前字符串添加一个字符串&lt;br /&gt; &lt;br /&gt;AppendFormat()&lt;br /&gt; 添加特定格式的字符串&lt;br /&gt; &lt;br /&gt;Insert()&lt;br /&gt; 在当前字符串中插入一个子字符串&lt;br /&gt; &lt;br /&gt;Remove()&lt;br /&gt; 从当前字符串中删除字符&lt;br /&gt; &lt;br /&gt;Replace()&lt;br /&gt; 在当前字符串中，用另一个字符替换某个字符，或者用当前字符串中的另一个子字符串替换某个字符串&lt;br /&gt; &lt;br /&gt;ToString()&lt;br /&gt; 把当前字符串转换为System.String对象(在System.Object中被重写)&lt;br /&gt; &lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;其中一些方法还有几种格式的重载方法。&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;AppendFormat()实际上会在调用Console.WriteLine()时调用，它负责确定所有像{0:D}的格式化表达式应使用什么表达式替代。下一节讨论这个问题。&lt;br /&gt;&lt;br /&gt;不能把StringBuilder转换为String(隐式转换和显式转换都不行)。如果要把StringBuilder的内容输出为String，惟一的方式是使用ToString()方法。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-4110219383913660152?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/4110219383913660152/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/04/c_21.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/4110219383913660152'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/4110219383913660152'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/04/c_21.html' title='C#教程（四十） 创建字符串'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-7158281898520058493</id><published>2009-04-19T17:24:00.000-07:00</published><updated>2009-04-19T17:25:43.915-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、String'/><title type='text'>c#教程（三十九）System.String类</title><content type='html'>C#中string关键字的映射实际上指向.NET 基类System.String。System.String是一个功能非常强大且用途非常广泛的基类，但它不是.NET中惟一与字符串相关的类。本章首先复习一下System.String的特性，再介绍如何使用其他的.NET类来处理字符串，特别是System.Text 和 System.Text.RegularExpressions命名空间中的类。本章主要介绍下述内容：&lt;br /&gt;&lt;br /&gt;●       创建字符串：如果多次修改一个字符串，例如，在显示字符串或将其传递给其他方法或应用程序前，创建一个较长的字符串，String类就会变得效率低下。对于这种情况，应使用另一个类System.Text.StringBuilder，因为它是专门为这种情况设计的。&lt;br /&gt;&lt;br /&gt;●       格式化表达式：这些表达式将用于后面几章中的Console.WriteLine() 方法。格式化表达式使用两个有效的接口IFormatProvider和IFormattable来处理。在自己的类上执行这两个接口，就可以定义自己的格式化序列，这样，Console.WriteLine()和类似的类就可以以指定的方式显示类的值。&lt;br /&gt;&lt;br /&gt;●       正则表达式：.NET还提供了一些非常复杂的类来识别字符串，或从长字符串中提取满足某些比较复杂条件的子字符串。例如，找出字符串中重复出现的某个字符或一组字符，或者找出以s开头、且至少包含一个n的所有单词，或者找出遵循雇员ID或社会安全号码约定的字符串。虽然可以使用字符串类，编写方法来执行这类处理，但这类方法编写起来比较繁琐，而使用System.Text.RegularExpressions命名空间中的类就比较简单，System.Text.RegularExpressions专门用于执行这类处理。&lt;br /&gt;&lt;br /&gt;8.1  System.String类&lt;br /&gt;在介绍其他字符串类之前，先快速复习一下String类上一些可利用的方法。&lt;br /&gt;&lt;br /&gt;System.String是一个类，该类专门用于存储字符串，允许对字符串进行许多操作。由于这种数据类型非常重要，&lt;a href="http://futureangle.blogspot.com"&gt;C#&lt;/a&gt;提供了它自己的关键字和相关的语法，以便使用这个类来很容易地处理字符串。&lt;br /&gt;&lt;br /&gt;使用运算符重载可以连接字符串：&lt;br /&gt;&lt;br /&gt;string message1 = "Hello";  //return "Hello"&lt;br /&gt;&lt;br /&gt;message1 += ", There";    // return "Hello, There "&lt;br /&gt;&lt;br /&gt;string message2 = message1 + "!";     // return "Hello, There!"&lt;br /&gt;&lt;br /&gt;C#还允许使用类似于索引符的语法来提取指定的字符：&lt;br /&gt;&lt;br /&gt;char char4 = "message" [4];   // returns 'a'. Note the char is zero-indexed&lt;br /&gt;&lt;br /&gt;这个类可以完成许多常见的任务，例如替换字符、删除空白和把字母变成大写形式等。可用的方法如表8-1所示。&lt;br /&gt;&lt;br /&gt;表  8-1&lt;br /&gt;&lt;br /&gt;方    法&lt;br /&gt; 作    用&lt;br /&gt; &lt;br /&gt;Compare&lt;br /&gt; 比较字符串的内容，考虑文化背景(场所)，确定某些字符是否相等&lt;br /&gt; &lt;br /&gt;CompareOrdinal&lt;br /&gt; 与Compare一样，但不考虑文化背景&lt;br /&gt; &lt;br /&gt;Format&lt;br /&gt; 格式化包含各种值的字符串和如何格式化每个值的说明符&lt;br /&gt; &lt;br /&gt;IndexOf&lt;br /&gt; 定位字符串中第一次出现某个给定子字符串或字符的位置&lt;br /&gt; &lt;br /&gt;IndexOfAny&lt;br /&gt; 定位字符串中第一次出现某个字符或一组字符的位置&lt;br /&gt; &lt;br /&gt;LastIndexOf&lt;br /&gt; 与IndexOf一样，但定位最后一次出现的位置 &lt;br /&gt; &lt;br /&gt;LastIndexOfAny&lt;br /&gt; 与IndexOfAny，但定位最后一次出现的位置&lt;br /&gt; &lt;br /&gt;PadLeft&lt;br /&gt; 在字符串的开头，通过添加指定的重复字符填充字符串&lt;br /&gt; &lt;br /&gt;PadRight&lt;br /&gt; 在字符串的结尾，通过添加指定的重复字符填充字符串&lt;br /&gt; &lt;br /&gt;Replace&lt;br /&gt; 用另一个字符或子字符串替换字符串中给定的字符或子字符串&lt;br /&gt; &lt;br /&gt;Split&lt;br /&gt; 在出现给定字符的地方，把字符串拆分为一个子字符串数组&lt;br /&gt; &lt;br /&gt;Substring&lt;br /&gt; 在字符串中获取给定位置的子字符串&lt;br /&gt; &lt;br /&gt;ToLower&lt;br /&gt; 把字符串转换为小写形式&lt;br /&gt; &lt;br /&gt;ToUpper&lt;br /&gt; 把字符串转换为大写形式&lt;br /&gt; &lt;br /&gt;Trim&lt;br /&gt; 删除首尾的空白&lt;br /&gt; &lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;这个表并不完整，但可以让您明白字符串所提供的功能。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-7158281898520058493?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/7158281898520058493/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/04/csystemstring.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/7158281898520058493'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/7158281898520058493'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/04/csystemstring.html' title='c#教程（三十九）System.String类'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-2061726050398716549</id><published>2009-04-16T17:17:00.000-07:00</published><updated>2009-04-16T17:22:49.635-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、指针'/><title type='text'>c#教程（三十八）使用指针优化性能</title><content type='html'>本节将介绍指针的一个主要应用领域：在堆栈中创建高性能、低系统开销的数组。第2章介绍了C#如何支持数组的处理。C#很容易使用一维数组和矩形或锯齿形多维数组，但有一个缺点：这些数组实际上都是对象，是System.Array的实例。因此数组只能存储在堆上，会增加系统开销。有时，我们希望创建一个使用时间比较短的高性能数组，不希望有引用对象的系统开销。而使用指针就可以做到，但只能用于一维数组。&lt;br /&gt;&lt;br /&gt;为了创建一个高性能的数组，需要使用另一个关键字：stackalloc。stackalloc命令指示.NET运行库分配堆栈上一定量的内存。在调用它时，需要为它提供两条信息：&lt;br /&gt;&lt;br /&gt;●       要存储的数据类型&lt;br /&gt;&lt;br /&gt;●       需要存储的数据个数。&lt;br /&gt;&lt;br /&gt;例如，分配足够的内存，以存储10个decimal数据，可以编写下面的代码：&lt;br /&gt;&lt;br /&gt;decimal* pDecimals = stackalloc decimal [10];&lt;br /&gt;&lt;br /&gt;注意，这个命令只是分配堆栈内存而已。它不会试图把内存初始化为任何默认值，这正好符合我们的目的。因为这是一个高性能的数组，给它不必要地初始化值会降低性能。&lt;br /&gt;&lt;br /&gt;同样，要存储20个double数据，可以编写下面的代码：&lt;br /&gt;&lt;br /&gt;double* pDoubles = stackalloc double [20];&lt;br /&gt;&lt;br /&gt;虽然这行代码指定把变量的个数存储为一个常数，但它是在运行时计算的一个数字。所以可以把上面的示例写为：&lt;br /&gt;&lt;br /&gt;int size;&lt;br /&gt;&lt;br /&gt;size = 20;   // or some other value calculated at run-time&lt;br /&gt;&lt;br /&gt;double* pDoubles = stackalloc double [size];&lt;br /&gt;&lt;br /&gt;从这些代码段中可以看出，stackalloc的语法有点不寻常。它的后面紧跟的是要存储的数据类型名(该数据类型必须是一个值类型)，其后是把需要的变量个数放在方括号中。分配的字节数是变量个数乘以sizeof(数据类型)。在这里，使用方括号表示这是一个数组。如果给20个double数据分配存储单元，就得到了一个有20个元素的double数组，最简单的数组类型可以是：逐个存储元素的内存块，如图7-6所示。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;图  7-6&lt;br /&gt;&lt;br /&gt;在图7-6中，显示了一个由stackalloc返回的指针，stackalloc总是返回分配数据类型的指针，它指向新分配内存块的顶部。要使用这个内存块，可以取消对返回指针的引用。例如，给20个double数据分配内存后，把第一个元素(数组中的元素0)设置为3.0，可以编写下面的代码：&lt;br /&gt;&lt;br /&gt;double* pDoubles = stackalloc double [20];&lt;br /&gt;&lt;br /&gt;*pDoubles = 3.0;&lt;br /&gt;&lt;br /&gt;要访问数组的下一个元素，可以使用指针算法。如前所述，如果给一个指针加1，它的值就会增加其数据类型的字节数。在本例中，就会把指针指向下一个空闲存储单元。因此可以把数组的第二个元素(数组中元素号为1)设置为8.4：&lt;br /&gt;&lt;br /&gt;double* pDoubles = stackalloc double [20];&lt;br /&gt;&lt;br /&gt;*pDoubles = 3.0; &lt;br /&gt;&lt;br /&gt;*(pDoubles+1) = 8.4;&lt;br /&gt;&lt;br /&gt;同样，可以用表达式*(pDoubles+X)获得数组中下标为X的元素。&lt;br /&gt;&lt;br /&gt;这样，就得到一种访问数组中元素的方式，但对于一般目的，使用这种语法过于复杂。C#为此定义了另一种语法。对指针应用方括号时，&lt;a href="http://futureangle.blogspot.com"&gt;C#&lt;/a&gt;为方括号提供了一种非常明确的含义。如果变量p是任意指针类型，X是一个整数，表达式p[X]就被编译器解释为*(p+X)，这适用于所有的指针，不仅仅是用stackalloc初始化的指针。利用这个简捷的记号，就可以用一种非常方便的方式访问数组。实际上，访问基于堆栈的一维数组所使用的语法与访问基于堆的、由System.Array类表示的数组是一样的：&lt;br /&gt;&lt;br /&gt;double *pDoubles = stackalloc double [20];&lt;br /&gt;&lt;br /&gt;pDoubles[0] = 3.0;   // pDoubles[0] is the same as *pDoubles&lt;br /&gt;&lt;br /&gt;pDoubles[1] = 8.4;   // pDoubles[1] is the same as *(pDoubles+1)&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;把数组的语法应用于指针并不是新东西。自从开发出C和C++语言以来，它们就是这两种语言的基础部分。实际上，C++开发人员会把这里用stackalloc获得的、基于堆栈的数组完全等同于传统的基于堆栈的C和C++数组。这个语法和指针与数组的链接方式是C语言在70年代后期流行起来的原因之一，也是指针的使用成为C和C++中一种大众化编程技巧的主要原因。&lt;br /&gt;&lt;br /&gt;高性能的数组可以用与一般&lt;a href="http://hexun.com/xiaohei588/ "&gt;C#&lt;/a&gt;数组相同的方式访问，但需要强调其中的一个警告。在C#中，下面的代码会抛出一个异常：&lt;br /&gt;&lt;br /&gt;double [] myDoubleArray = new double [20];&lt;br /&gt;&lt;br /&gt;myDoubleArray[50] = 3.0;&lt;br /&gt;&lt;br /&gt;抛出异常的原因很明显。使用越界的下标来访问数组：下标是50，但允许的最大值是19。但是，如果使用stackalloc声明了一个相同数组，对数组进行边界检查时，这个数组中没有包装任何对象，因此下面的代码不会抛出异常：&lt;br /&gt;&lt;br /&gt;double* pDoubles = stackalloc double [20];&lt;br /&gt;&lt;br /&gt;pDoubles[50] = 3.0;&lt;br /&gt;&lt;br /&gt;在这段代码中，我们分配了足够的内存来存储20个double类型数据。接着把sizeof(double)存储单元的起始位置设置为该存储单元的起始位置加上50*sizeof(double)存储单元，来保存双精度值3.0。但这个存储单元超出了刚才为double分配的内存区域。谁也不知道这个地址上存储了什么数据。最好是只使用某个当前未使用的内存，但所重写的空间也有可能是堆栈上用于存储其他变量或某个正在执行的方法的返回地址。因此，使用指针获得高性能的同时，也会付出一些代价：需要确保自己知道在做什么，否则就会抛出非常古怪的运行时错误。&lt;br /&gt;&lt;br /&gt;2. 示例QuickArray&lt;br /&gt;下面用一个stackalloc示例QuickArray来结束关于指针的讨论。在这个示例中，程序仅要求用户提供为数组分配的元素数。然后代码使用stackalloc给long型数组分配一定的存储单元。这个数组的元素是从0开始的整数的平方，结果显示在控制台上：&lt;br /&gt;&lt;br /&gt;using System;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;namespace Wrox.ProCSharp.Chapter07&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   class MainEntryPoint&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      static unsafe void Main()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         Console.Write("How big an array do you want? \n&gt; ");&lt;br /&gt;&lt;br /&gt;         string userInput = Console.ReadLine();&lt;br /&gt;&lt;br /&gt;         uint size = uint.Parse(userInput);&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;         long* pArray = stackalloc long [(int)size];&lt;br /&gt;&lt;br /&gt;               }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;运行这个示例，得到如下所示的结果：&lt;br /&gt;&lt;br /&gt;QuickArray&lt;br /&gt;&lt;br /&gt;How big an array do you want?&lt;br /&gt;&lt;br /&gt;&gt; 15&lt;br /&gt;&lt;br /&gt;Element 0 = 0&lt;br /&gt;&lt;br /&gt;Element 1 = 1&lt;br /&gt;&lt;br /&gt;Element 2 = 4&lt;br /&gt;&lt;br /&gt;Element 3 = 9&lt;br /&gt;&lt;br /&gt;Element 4 = 16&lt;br /&gt;&lt;br /&gt;Element 5 = 25&lt;br /&gt;&lt;br /&gt;Element 6 = 36&lt;br /&gt;&lt;br /&gt;Element 7 = 49&lt;br /&gt;&lt;br /&gt;Element 8 = 64&lt;br /&gt;&lt;br /&gt;Element 9 = 81&lt;br /&gt;&lt;br /&gt;Element 10 = 100&lt;br /&gt;&lt;br /&gt;Element 11 = 121&lt;br /&gt;&lt;br /&gt;Element 12 = 144&lt;br /&gt;&lt;br /&gt;Element 13 = 169&lt;br /&gt;&lt;br /&gt;Element 14 = 196&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-2061726050398716549?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/2061726050398716549/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/04/c_16.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/2061726050398716549'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/2061726050398716549'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/04/c_16.html' title='c#教程（三十八）使用指针优化性能'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-4206760143638692665</id><published>2009-04-15T17:22:00.000-07:00</published><updated>2009-04-15T17:23:59.364-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、指针'/><title type='text'>c#教程（三十七）指针(3)</title><content type='html'>下面给出一个使用指针的示例：PointerPlayaround。它执行一些简单的指针操作，显示结果，还允许查看内存中发生的情况，并确定变量存储在什么地方：&lt;br /&gt;&lt;br /&gt;using System;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;namespace Wrox.ProCSharp.Chapter07&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   class MainEntryPoint&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      static unsafe void Main()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         int x=10;&lt;br /&gt;&lt;br /&gt;         short y =–1;&lt;br /&gt;&lt;br /&gt;         byte y2 = 4;&lt;br /&gt;&lt;br /&gt;         double z = 1.5;&lt;br /&gt;&lt;br /&gt;         int* pX = &amp;x;&lt;br /&gt;&lt;br /&gt;         short* pY = &amp;y;&lt;br /&gt;&lt;br /&gt;         double* pZ = &amp;z;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;         Console.WriteLine(&lt;br /&gt;&lt;br /&gt;            "Address of x is 0x{0:X}, size is {1}, value is {2}", &lt;br /&gt;&lt;br /&gt;            (uint)&amp;x, sizeof(int), x);&lt;br /&gt;&lt;br /&gt;         Console.WriteLine(&lt;br /&gt;&lt;br /&gt;            "Address of y is 0x{0:X}, size is {1}, value is {2}", &lt;br /&gt;&lt;br /&gt;            (uint)&amp;y, sizeof(short), y);&lt;br /&gt;&lt;br /&gt;         Console.WriteLine(&lt;br /&gt;&lt;br /&gt;            "Address of y2 is 0x{0:X}, size is {1}, value is {2}", &lt;br /&gt;&lt;br /&gt;            (uint)&amp;y2, sizeof(byte), y2);&lt;br /&gt;&lt;br /&gt;         Console.WriteLine(&lt;br /&gt;&lt;br /&gt;            "Address of z is 0x{0:X}, size is {1}, value is {2}", &lt;br /&gt;&lt;br /&gt;            (uint)&amp;z, sizeof(double), z);&lt;br /&gt;&lt;br /&gt;         Console.WriteLine(&lt;br /&gt;&lt;br /&gt;            "Address of pX=&amp;x is 0x{0:X}, size is {1}, value is 0x{2:X}", &lt;br /&gt;&lt;br /&gt;            (uint)&amp;pX, sizeof(int*), (uint)pX);&lt;br /&gt;&lt;br /&gt;         Console.WriteLine(&lt;br /&gt;&lt;br /&gt;            "Address of pY=&amp;y is 0x{0:X}, size is {1}, value is 0x{2:X}", &lt;br /&gt;&lt;br /&gt;            (uint)&amp;pY, sizeof(short*), (uint)pY);&lt;br /&gt;&lt;br /&gt;         Console.WriteLine(&lt;br /&gt;&lt;br /&gt;            "Address of pZ=&amp;z is 0x{0:X}, size is {1}, value is 0x{2:X}", &lt;br /&gt;&lt;br /&gt;            (uint)&amp;pZ, sizeof(double*), (uint)pZ);&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;         *pX = 20;&lt;br /&gt;&lt;br /&gt;         Console.WriteLine("After setting *pX, x = {0}", x);&lt;br /&gt;&lt;br /&gt;         Console.WriteLine("*pX = {0}", *pX);&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;         pZ = (double*)pX;&lt;br /&gt;&lt;br /&gt;         Console.WriteLine("x treated as a double = {0}", *pZ);&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;         Console.ReadLine();&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;这段代码声明了3个值变量：&lt;br /&gt;&lt;br /&gt;●       int x&lt;br /&gt;&lt;br /&gt;●       short y&lt;br /&gt;&lt;br /&gt;●       double z&lt;br /&gt;&lt;br /&gt;还声明了指向这3个值的指针：px、py、pz。&lt;br /&gt;&lt;br /&gt;然后显示这3个变量的值，以及它们的大小和地址。注意在获取px, py和pz的地址时，我们查看的是指针的指针，即值的地址的地址！还要注意，与显示地址的常见方式一致，在Console.WriteLine()命令中使用{0:X}格式说明符，确保该内存地址以16进制格式显示。&lt;br /&gt;&lt;br /&gt;最后，使用指针px把x的值改为20，执行一些指针转换，如果把x的内容当作double类型，就会得到无意义的结果。&lt;br /&gt;&lt;br /&gt;编译运行这段代码，在得到的结果中，我们将列出用/unsafe标志进行编译和不用/unsafe标志进行编译的结果：&lt;br /&gt;&lt;br /&gt;csc PointerPlayaround.cs&lt;br /&gt;&lt;br /&gt;Microsoft (R) Visual C# .NET Compiler version 7.10.3052.4&lt;br /&gt;&lt;br /&gt;for Microsoft (R) .NET Framework version 1.1.4322&lt;br /&gt;&lt;br /&gt;Copyright (C) Microsoft Corporation 2001–2002. All rights reserved.&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;PointerPlayaround.cs(7,26): error CS0227: Unsafe code may only appear if&lt;br /&gt;&lt;br /&gt;        compiling with /unsafe&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;csc /unsafe PointerPlayaround.cs&lt;br /&gt;&lt;br /&gt;Microsoft (R) Visual C# .NET Compiler version 7.10.3052.4&lt;br /&gt;&lt;br /&gt;for Microsoft (R) .NET Framework version 1.1.4322&lt;br /&gt;&lt;br /&gt;Copyright (C) Microsoft Corporation 2001-2002. All rights reserved.&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;PointerPlayaround&lt;br /&gt;&lt;br /&gt;Address of x is 0x12F8C4, size is 4, value is 10&lt;br /&gt;&lt;br /&gt;Address of y is 0x12F8C0, size is 2, value is -1&lt;br /&gt;&lt;br /&gt;Address of y2 is 0x12F8BC, size is 1, value is 4&lt;br /&gt;&lt;br /&gt;Address of z is 0x12F8B4, size is 8, value is 1.5&lt;br /&gt;&lt;br /&gt;Address of pX=&amp;x is 0x12F8B0, size is 4, value is 0x12F8C4&lt;br /&gt;&lt;br /&gt;Address of pY=&amp;y is 0x12F8AC, size is 4, value is 0x12F8C0&lt;br /&gt;&lt;br /&gt;Address of pZ=&amp;z is 0x12F8A8, size is 4, value is 0x12F8B4&lt;br /&gt;&lt;br /&gt;After setting *pX, x = 20&lt;br /&gt;&lt;br /&gt;*pX = 20&lt;br /&gt;&lt;br /&gt;x treated as a double = 2.63837073472194E-308&lt;br /&gt;&lt;br /&gt;检查这3个结果，可以证实我们在本章前面的“后台内存管理”一节描述的堆栈操作，即堆栈给变量向下分配内存。注意，这还证实了堆栈中的内存块总是按照4B的倍数进行分配的。例如，y是一个short(size = 2)，其地址是1243328，表示为该变量分配的内存区域是1243328~1243331。如果.NET运行库严格逐个排列变量，则y应只占用2个存储单元12433328和1243329。&lt;br /&gt;&lt;br /&gt;11. 给示例添加类和结构&lt;br /&gt;在本节中，使用第二个示例PointerPlayaround2介绍指针的算法，以及结构指针和类成员指针。开始时，定义一个结构CurrencyStruct，把货币值表示为美元和美分，再定义一个对应的类CurrencyClass：&lt;br /&gt;&lt;br /&gt;   struct CurrencyStruct&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      public long Dollars;&lt;br /&gt;&lt;br /&gt;      public byte Cents;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      public override string ToString()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         return "$" + Dollars + "." + Cents;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;   class CurrencyClass&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      public long Dollars;&lt;br /&gt;&lt;br /&gt;      public byte Cents;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      public override string ToString()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         return "$" + Dollars + "." + Cents;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;定义好了结构和类后，就可以对它们应用指针了。下面的代码是一个新的示例。这段代码比较长，我们对此将做详细讲解。首先显示CurrencyStruct结构的字节数，创建它的两个实例和一些指针，再使用pAmount指针初始化一个CurrencyStruct结构amount1，显示变量的   地址：&lt;br /&gt;&lt;br /&gt;public static unsafe void Main()&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   Console.WriteLine(&lt;br /&gt;&lt;br /&gt;      "Size of Currency struct is " + sizeof(CurrencyStruct));&lt;br /&gt;&lt;br /&gt;   CurrencyStruct amount1, amount2;&lt;br /&gt;&lt;br /&gt;   CurrencyStruct* pAmount = &amp;amount1;&lt;br /&gt;&lt;br /&gt;   long* pDollars = &amp;(pAmount-&gt;Dollars);&lt;br /&gt;&lt;br /&gt;   byte* pCents = &amp;(pAmount-&gt;Cents);&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;   Console.WriteLine("Address of amount1 is 0x{0:X}", (uint)&amp;amount1);&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("Address of amount2 is 0x{0:X}", (uint)&amp;amount2);&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("Address of pAmount is 0x{0:X}", (uint)&amp;pAmount);&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("Address of pDollars is 0x{0:X}", (uint)&amp;pDollars);&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("Address of pCents is 0x{0:X}", (uint)&amp;pCents);&lt;br /&gt;&lt;br /&gt;   pAmount–&gt;Dollars = 20;&lt;br /&gt;&lt;br /&gt;   *pCents = 50;&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("amount1 contains " + amount1);&lt;br /&gt;&lt;br /&gt;现在根据堆栈的工作方式，执行一些指针操作。由于变量是按顺序声明的，所以amount2存储在amount1后面紧邻的地址上，sizeof(CurrencyStruct)返回16(见后面的的屏幕输出)，所以CurrencyStruct占用的字节数是4的倍数。在递减了Currency指针后，它就指向amount2：&lt;br /&gt;&lt;br /&gt;   ––    pAmount;   // this should get it to point to amount2&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("amount2 has address 0x{0:X} and contains {1}", &lt;br /&gt;&lt;br /&gt;      (uint)pAmount, *pAmount);&lt;br /&gt;&lt;br /&gt;在调用Console.WriteLine()语句时，它显示了amount2的内容，但还没有对它进行初始化。显示出来的东西就是随机的垃圾—— 在执行该示例前存储在内存中该单元的内容。但这有一个要点：一般情况下，C#编译器会禁止使用未初始化的值，但在开始使用指针时，就很容易绕过所有通常的编译检查。此时我们这么做，是因为编译器无法知道我们实际上要显示的是amount2的内容。因为知道了堆栈的工作方式，所以可以说出递减pAmount的结果是什么。使用指针算法后，可以访问各种编译器通常禁止访问的变量和存储单元，因此指针算法是不安全的。&lt;br /&gt;&lt;br /&gt;在示例中，接下来在pCents指针上进行指针运算。pCents目前指向amount1.Cents，但此处的目的是使用指针算法让它指向amount2.Cents，而不是直接告诉编译器我们要做什么。为此，需要从pCents指针所包含的地址中减去sizeof(Currency)：&lt;br /&gt;&lt;br /&gt;   // do some clever casting to get pCents to point to cents&lt;br /&gt;&lt;br /&gt;   // inside amount2&lt;br /&gt;&lt;br /&gt;   CurrencyStruct* pTempCurrency = (CurrencyStruct*)pCents;&lt;br /&gt;&lt;br /&gt;   pCents = (byte*) (–– pTempCurrency );&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("Address of pCents is now 0x{0:X}", (uint)&amp;pCents);&lt;br /&gt;&lt;br /&gt;最后，使用fixed关键字创建一些指向类实例中字段的指针，使用这些指针设置这个实例的值。注意，这也是我们第一次能够查看存储在堆中(而不是堆栈)的项目地址：&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("\nNow with classes");&lt;br /&gt;&lt;br /&gt;   // now try it out with classes&lt;br /&gt;&lt;br /&gt;   CurrencyClass amount3 = new CurrencyClass();&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;   fixed(long* pDollars2 = &amp;(amount3.Dollars))&lt;br /&gt;&lt;br /&gt;   fixed(byte* pCents2 = &amp;(amount3.Cents))&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      Console.WriteLine(&lt;br /&gt;&lt;br /&gt;         "amount3.Dollars has address 0x{0:X}", (uint)pDollars2);&lt;br /&gt;&lt;br /&gt;      Console.WriteLine(&lt;br /&gt;&lt;br /&gt;         "amount3.Cents has address 0x{0:X}", (uint) pCents2);&lt;br /&gt;&lt;br /&gt;      *pDollars2 = -100;&lt;br /&gt;&lt;br /&gt;      Console.WriteLine("amount3 contains " + amount3);&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;编译并运行这段代码，得到如下所示的结果：&lt;br /&gt;&lt;br /&gt;csc /unsafe PointerPlayaround2.cs&lt;br /&gt;&lt;br /&gt;Microsoft (R) Visual C# .NET Compiler version 7.10.3052.4&lt;br /&gt;&lt;br /&gt;for Microsoft (R) .NET Framework version 1.1.4322&lt;br /&gt;&lt;br /&gt;Copyright (C) Microsoft Corporation 2001–2002. All rights reserved.&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;PointerPlayaround2&lt;br /&gt;&lt;br /&gt;Size of Currency struct is 16&lt;br /&gt;&lt;br /&gt;Address of amount1 is 0x12F698&lt;br /&gt;&lt;br /&gt;Address of amount2 is 0x12F688&lt;br /&gt;&lt;br /&gt;Address of pAmount is 0x12F684&lt;br /&gt;&lt;br /&gt;Address of pDollars is 0x12F680&lt;br /&gt;&lt;br /&gt;Address of pCents is 0x12F67C&lt;br /&gt;&lt;br /&gt;amount1 contains $20.50&lt;br /&gt;&lt;br /&gt;amount2 has address 0x12F688 and contains $0.236&lt;br /&gt;&lt;br /&gt;Address of pCents is now 0x12F67C&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;Now with classes&lt;br /&gt;&lt;br /&gt;amount3.Dollars has address 0xB8850C&lt;br /&gt;&lt;br /&gt;amount3.Cents has address 0x4B88514&lt;br /&gt;&lt;br /&gt;amount3 contains $–100.0&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;这些结果是使用.NET Framework 1.1版本得到的。如果在.NET的另一个版本上运行该例子，实际显示的地址会有所不同。&lt;br /&gt;&lt;br /&gt;注意在这个结果中，显示了未初始化的amount2值，CurrencyStruct结构的字节数是16，大于其字段的字节数(1 long(=8) + 1 byte(=1))。这是前面讨论的对齐单词的结果。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-4206760143638692665?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/4206760143638692665/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/04/c3.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/4206760143638692665'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/4206760143638692665'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/04/c3.html' title='c#教程（三十七）指针(3)'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-8320509448514986445</id><published>2009-04-13T17:27:00.001-07:00</published><updated>2009-04-13T17:27:55.293-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、指针'/><title type='text'>c#教程（三十六）指针(2)</title><content type='html'>由于指针实际上存储了一个表示地址的整数，所以任何指针中的地址都可以转换为任何整数类型。指针到整数类型的转换必须是显式指定的，隐式的转换是不允许的。例如，编写下面的代码是合法的：&lt;br /&gt;&lt;br /&gt;int x = 10;&lt;br /&gt;&lt;br /&gt;int* pX, pY;&lt;br /&gt;&lt;br /&gt;pX = &amp;x;&lt;br /&gt;&lt;br /&gt;pY = pX;&lt;br /&gt;&lt;br /&gt;*pY = 20;&lt;br /&gt;&lt;br /&gt;uint y = (uint)pX;&lt;br /&gt;&lt;br /&gt;int* pD = (int*)y;&lt;br /&gt;&lt;br /&gt;把指针pX中包含的地址转换为一个uint，存储在变量y中。接着把y转换回int*，存储在新变量pD中。因此pD也指向x的值。&lt;br /&gt;&lt;br /&gt;把指针的值转换为整数类型的主要原因是为了显示它。Console.Write()和Console. WriteLine()方法没有任何带指针的重载方法，所以必须把指针转换为整数类型，才能接受和显示它们：&lt;br /&gt;&lt;br /&gt;Console.WriteLine("Address is" + pX);   // wrong – will give a&lt;br /&gt;&lt;br /&gt;                                  // compilation error&lt;br /&gt;&lt;br /&gt;Console.WriteLine("Address is" + (uint) pX);   // OK&lt;br /&gt;&lt;br /&gt;可以把一个指针转换为任何整数类型，但是，因为在32位系统上，地址占用4B，把指针转换为不是uint、long 或 ulong的数据类型，肯定会导致溢出错误(int也可能导致这个问题，因为它的取值范围是–20亿到20亿，而地址的取值范围是0到40亿)。C#是用于64位处理器的，地址占用8B。因此在这样的系统上，把指针转换为非ulong的类型，就可能导致溢出错误。还要注意，checked关键字不能用于涉及指针的转换。对于这种转换，即使在checked情况下，发生溢出时也不会抛出异常。.NET运行库假定，如果要使用指针，就知道自己要做什么，并希望出现溢出。&lt;br /&gt;&lt;br /&gt;4. 指针类型之间的转换&lt;br /&gt;也可以在指向不同类型的指针之间进行显式的转换。例如：&lt;br /&gt;&lt;br /&gt;byte aByte = 8;&lt;br /&gt;&lt;br /&gt;byte* pByte= &amp;aByte;&lt;br /&gt;&lt;br /&gt;double* pDouble = (double*)pByte;&lt;br /&gt;&lt;br /&gt;这是一段合法的代码，但如果要执行这段代码，就要小心了。在上面的示例中，如果要查找指针pDouble指向的double，就会查找包含1B的内存，并和一些其他内存合并在一起，把它当作包含一个double的内存区域来对待—— 这不会得到一个有意义的值。但是，可以在类型之间转换，实现类型的统一，或者把指针转换为其他类型，例如把指针转换为sbyte，检查内存的单个字节。&lt;br /&gt;&lt;br /&gt;5. void指针&lt;br /&gt;如果要使用一个指针，但不希望指定它指向的数据类型，就可以把指针声明为void：&lt;br /&gt;&lt;br /&gt;int* pointerToInt;&lt;br /&gt;&lt;br /&gt;void* pointerToVoid;&lt;br /&gt;&lt;br /&gt;pointerToVoid = (void*)pointerToInt; &lt;br /&gt;&lt;br /&gt;void型指针的主要用途是调用需要void*型参数的API函数。在C#语言中，使用void指针的情况并不是很多。特殊情况下，如果试图使用*运算符间接引用void指针，编译器就会标记一个错误。&lt;br /&gt;&lt;br /&gt;6. 指针的算法&lt;br /&gt;可以给指针加减整数。但是，编译器很智能，知道如何执行这个操作。例如，假定有一个int指针，要在其值上加1。编译器会假定要查找int后面的存储单元，因此会给该值加上4B， 即加上int的字节数。如果这是一个double指针，加1就表示在指针的值上加8B，即double的字节数。只有指针是指向byte或 sbyte(都是1B)，才会给该指针的值加上1。&lt;br /&gt;&lt;br /&gt;可以对指针使用运算符+、–、+=、–=、++和–– ，这些运算符右边的变量必须是long或ulong类型。&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;不允许针对void指针执行算术运算。&lt;br /&gt;&lt;br /&gt;例如，假定有如下定义：&lt;br /&gt;&lt;br /&gt;uint u = 3;&lt;br /&gt;&lt;br /&gt;byte b = 8;&lt;br /&gt;&lt;br /&gt;double d = 10.0;&lt;br /&gt;&lt;br /&gt;uint* pUint= &amp;u;        // size of a uint is 4&lt;br /&gt;&lt;br /&gt;byte* pByte = &amp;b;       // size of a byte is 1&lt;br /&gt;&lt;br /&gt;double* pDouble = &amp;d;   // size of a double is 8&lt;br /&gt;&lt;br /&gt;下面假定这些指针的地址是：&lt;br /&gt;&lt;br /&gt;●       pUint：1243332&lt;br /&gt;&lt;br /&gt;●       pByte: 1243328&lt;br /&gt;&lt;br /&gt;●       pDouble: 1243320&lt;br /&gt;&lt;br /&gt;执行这段代码后：&lt;br /&gt;&lt;br /&gt;++pUint;              // adds (1*4)= 4 bytes to pUint&lt;br /&gt;&lt;br /&gt;pByte–= 3;            // subtracts (3*1)=3 bytes from pByte&lt;br /&gt;&lt;br /&gt;double* pDouble2 = pDouble + 4; // pDouble2 = pDouble + 32 bytes (4*8 bytes)&lt;br /&gt;&lt;br /&gt;指针应包含的内容是：&lt;br /&gt;&lt;br /&gt;●       pUint: 1243336&lt;br /&gt;&lt;br /&gt;●       pByte: 1243325&lt;br /&gt;&lt;br /&gt;●       pDouble2: 1243352&lt;br /&gt;&lt;br /&gt;提示：&lt;br /&gt;&lt;br /&gt;给类型为T的指针加上X，其中X的值为P，则得到的结果是P + X*(sizeof(T))。&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;使用这个规则时要小心。如果给定类型的连续值存储在连续的存储单元中，指针加法就允许在存储单元中移动。但如果类型是byte或char，其总字节数就不是4的倍数，在默认情况下，连续值就不是默认地存储在连续的存储单元中。&lt;br /&gt;&lt;br /&gt;如果两个指针都指向相同的数据类型，也可以把一个指针从另一个指针中减去。此时，结果是一个long，其值是指针值的差被该数据类型所占用的字节数整除的结果：&lt;br /&gt;&lt;br /&gt;double* pD1 = (double*)1243324;     // note that it is perfectly valid to &lt;br /&gt;&lt;br /&gt;                                 // initialize a pointer like this.&lt;br /&gt;&lt;br /&gt;double* pD2 = (double*)1243300;&lt;br /&gt;&lt;br /&gt;long L = pD1-pD2;                 // gives the result 3 (=24/sizeof(double))&lt;br /&gt;&lt;br /&gt;7. sizeof运算符&lt;br /&gt;在这一节中，将介绍如何确定各种数据类型的大小。如果需要在代码中使用类型的大小，就可以使用sizeof运算符，它的参数是数据类型的名称，返回该类型占用的字节数。例如：&lt;br /&gt;&lt;br /&gt;int x = sizeof(double);&lt;br /&gt;&lt;br /&gt;这将设置x的值为8。&lt;br /&gt;&lt;br /&gt;使用sizeof的优点是不必在代码中硬编码数据类型的大小，使代码的移植性更强。对于预定义的数据类型，sizeof返回表7-1所示的值。&lt;br /&gt;&lt;br /&gt;表  7-1&lt;br /&gt;&lt;br /&gt;sizeof(sbyte) = 1;&lt;br /&gt; sizeof(byte) = 1;&lt;br /&gt; &lt;br /&gt;sizeof(short) = 2;&lt;br /&gt; sizeof(ushort) = 2;&lt;br /&gt; &lt;br /&gt;sizeof(int) = 4;&lt;br /&gt; sizeof(uint) = 4;&lt;br /&gt; &lt;br /&gt;sizeof(long) = 8;&lt;br /&gt; sizeof(ulong) = 8;&lt;br /&gt; &lt;br /&gt;sizeof(char) = 2;&lt;br /&gt; sizeof(float) = 4;&lt;br /&gt; &lt;br /&gt;sizeof(double) = 8;&lt;br /&gt; sizeof(bool) = 1;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;也可以对自己定义的结构使用sizeof，但此时得到的结果取决于结构中的字段。不能对类使用sizeof。它只能用于不安全的代码块。&lt;br /&gt;&lt;br /&gt;8. 结构指针：指针成员访问运算符&lt;br /&gt;结构指针的工作方式与预定义值类型的指针的工作方式是一样的。但是这有一个条件：结构不能包含任何引用类型，这是因为前面介绍的一个限制—— 指针不能指向任何引用类型。为了避免这种情况，如果创建一个指针，它指向包含引用类型的结构，编译器就会标记一个错误。&lt;br /&gt;&lt;br /&gt;假定定义了如下结构：&lt;br /&gt;&lt;br /&gt;struct MyStruct&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   public long X;&lt;br /&gt;&lt;br /&gt;   public float F;&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;就可以给它定义一个指针：&lt;br /&gt;&lt;br /&gt;MyStruct* pStruct;&lt;br /&gt;&lt;br /&gt;对其进行初始化：&lt;br /&gt;&lt;br /&gt;MyStruct Struct = new MyStruct();&lt;br /&gt;&lt;br /&gt;pStruct = &amp;Struct;&lt;br /&gt;&lt;br /&gt;也可以通过指针访问结构的成员值：&lt;br /&gt;&lt;br /&gt;(*pStruct).X = 4;&lt;br /&gt;&lt;br /&gt;(*pStruct).F = 3.4f;&lt;br /&gt;&lt;br /&gt;但是，这个语法有点复杂。因此，C#定义了另一个运算符，用一种比较简单的语法，通过指针访问结构的成员，该语法称为指针成员访问运算符，其符号是一个短划线，后跟一个大于号：–&gt;。&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;C++开发人员会认出指针成员访问操作符。因为C++使用这些符号完成相同的任务。&lt;br /&gt;&lt;br /&gt;使用这个指针成员访问运算符，上述代码可以重写为：&lt;br /&gt;&lt;br /&gt;pStruct–&gt;X = 4;&lt;br /&gt;&lt;br /&gt;pStruct–&gt;F = 3.4f;&lt;br /&gt;&lt;br /&gt;也可以直接把合适类型的指针设置为指向结构中的一个字段：&lt;br /&gt;&lt;br /&gt;long* pL = &amp;(Struct.X);&lt;br /&gt;&lt;br /&gt;float* pF = &amp;(Struct.F);&lt;br /&gt;&lt;br /&gt;或者&lt;br /&gt;&lt;br /&gt;long* pL = &amp;(pStruct–&gt;X);&lt;br /&gt;&lt;br /&gt;float* pF = &amp;(pStruct–&gt;F);&lt;br /&gt;&lt;br /&gt;9. 类成员指针&lt;br /&gt;前面说过，不能创建指向类的指针，这是因为垃圾收集器不包含指针的任何信息，只包含引用的信息，因此创建指向类的指针会使垃圾收集器不能正常工作。&lt;br /&gt;&lt;br /&gt;但是，大多数类都包含值类型的成员，可以为这些值类型成员创建指针，但这需要一种特殊的语法。例如，假定把上面示例中的结构重写为类：&lt;br /&gt;&lt;br /&gt;class MyClass&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   public long X;&lt;br /&gt;&lt;br /&gt;   public float F;&lt;br /&gt;&lt;br /&gt;} &lt;br /&gt;&lt;br /&gt;然后就可以为它的字段X和F创建指针了，方法与前面一样。但这么做会抛出一个编译    错误：&lt;br /&gt;&lt;br /&gt;MyClass myObject = new MyClass();&lt;br /&gt;&lt;br /&gt;long* pL = &amp;( myObject.X);   // wrong– –compilation error&lt;br /&gt;&lt;br /&gt;float* pF = &amp;( myObject.F);   // wrong– –compilation error&lt;br /&gt;&lt;br /&gt;X和F本身都是非托管类型，它们嵌入在一个对象中，存储在堆上。在垃圾收集的过程中，垃圾收集器会把MyClass移动到内存的一个新单元上，这样， pL和pF就会指向错误的存储单元。由于存在这个问题，所以编译器不允许以这种方式把托管类型的成员地址分配给指针。&lt;br /&gt;&lt;br /&gt;解决这个问题的方法是使用fixed关键字，它会告诉垃圾收集器，类实例的某些成员有指向它们的指针，所以这些实例不能移动。如果要声明一个指针，使用fixed的语法如下所示：&lt;br /&gt;&lt;br /&gt;MyClass myObject = new MyClass();&lt;br /&gt;&lt;br /&gt;fixed (long* pObject = &amp;( myObject.X))&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   // do something&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;在关键字fixed后面的圆括号中，定义和初始化指针变量。这个指针变量(在本例中是pObject)现在就在fixed块的作用域内，这样，垃圾收集器知道，在执行fixed块中的代码时，不能移动MyObject对象。&lt;br /&gt;&lt;br /&gt;如果要声明多个这样的指针，可以在同一个代码块前放置多个fixed语句：&lt;br /&gt;&lt;br /&gt;MyClass myObject = new MyClass();&lt;br /&gt;&lt;br /&gt;fixed (long* pX = &amp;( myObject.X))&lt;br /&gt;&lt;br /&gt;fixed (float* pF = &amp;( myObject.F))&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   // do something&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;如果要在不同的阶段固定几个指针，还可以嵌套整个fixed块：&lt;br /&gt;&lt;br /&gt;MyClass myObject = new MyClass();&lt;br /&gt;&lt;br /&gt;fixed (long* pX = &amp;( myObject.X))&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   // do something with pX&lt;br /&gt;&lt;br /&gt;   fixed (float* pF = &amp;( myObject.F))&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      // do something else with pF&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;也可以在同一个fixed语句中初始化多个变量，但这些变量的类型必须相同：&lt;br /&gt;&lt;br /&gt;MyClass myObject = new MyClass();&lt;br /&gt;&lt;br /&gt;MyClass myObject2 = new MyClass();&lt;br /&gt;&lt;br /&gt;fixed (long* pX = &amp;( myObject.X), pX2 = &amp;( myObject2.X))&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   // etc.&lt;br /&gt;&lt;br /&gt;在上述情况中，是否声明不同的指针，让它们指向相同或不同对象中的字段，或者指向不与类实例相关的静态字段，这一点是不重要的。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-8320509448514986445?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/8320509448514986445/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/04/c2.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/8320509448514986445'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/8320509448514986445'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/04/c2.html' title='c#教程（三十六）指针(2)'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-489320599199457163</id><published>2009-04-09T17:33:00.000-07:00</published><updated>2009-04-09T17:34:15.133-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、指针'/><title type='text'>c#教程（三十六）指针(1)</title><content type='html'>下面把指针当作一个新论题来介绍，而实际上，指针并不是新东西，因为在代码中可以自由使用引用，而引用就是一个类型安全的指针。前面已经介绍了表示对象和数组的变量实际上包含存储相应数据(引用)的内存地址。指针只是一个以与引用相同的方式存储数据的变量。其区别是C#的引用语法不允许直接访问引用变量包含的地址。有了引用后，从语法上看，变量就可以存储引用的实际内容。&lt;br /&gt;&lt;br /&gt;C#引用主要用于使C#语言易于使用，防止用户无意中执行某些破坏内存中内容的操作，另一方面，使用指针，就可以访问实际内存地址，执行新类型的操作。例如，可以给地址加上4B，这样就可以查看甚至修改存储在新地址中的数据。&lt;br /&gt;&lt;br /&gt;下面是使用指针的两个主要原因：&lt;br /&gt;&lt;br /&gt;●       向后兼容性。尽管.NET运行库提供了许多工具，但仍可以调用旧的Windows API 函数。 对于某些操作来说，这可能是完成任务的惟一方式。这些API函数都是用C语言编写的，通常要求把指针作为其参数。但在许多情况下，还可以使用DllImport声明，以避免使用指针，例如使用System.IntPtr类。&lt;br /&gt;&lt;br /&gt;●       性能。在一些情况下，速度是最重要的，而指针可以提供最优性能。假定用户知道自己在做什么，就可以确保以最高效的方式访问或处理数据。但是，注意在代码的其他区域中，不使用指针，也可以对性能做必要的改进。请使用代码配置文件，查找代码中的瓶颈，代码配置文件随VS.NET一起安装。&lt;br /&gt;&lt;br /&gt;但是，这种低级内存访问也是有代价的。使用指针的语法比引用类型更复杂。而且，指针使用起来比较困难，需要非常高的编程技巧和很强的能力，仔细考虑代码所完成的逻辑操作，才能成功地使用指针。如果不仔细，使用指针很容易在程序中引入微妙的、难以查找的错误。例如很容易重写其他变量，导致堆栈溢出，访问某些没有存储变量的内存区域，甚至重写.NET运行库所需要的代码信息，因而使程序崩溃。&lt;br /&gt;&lt;br /&gt;另外，如果使用指针就必须为代码获取代码访问安全机制的高级别信任，否则就不能执行。在默认的代码访问安全策略中，只有代码运行在本地机器上，这才是可能的。如果代码必须运行在远程地点，例如Internet，用户就必须给代码授予额外的许可，代码才能工作。除非用户信任您和你的代码，否则他们不会授予这些许可，第14章将讨论代码访问安全性。&lt;br /&gt;&lt;br /&gt;尽管有这些问题，但指针在编写高效的代码时是一种非常强大和灵活的工具，这里就介绍指针的使用。&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;这里强烈建议不要使用指针，因为如果使用指针，代码不仅难以编写和调试，而且无法通过CLR的内存类型安全检查(详见第1章)。&lt;br /&gt;&lt;br /&gt;1. 编写不安全的代码&lt;br /&gt;因为使用指针会带来相关的风险，所以C#只允许在特别标记的代码块中使用指针。标记代码所用的关键字是unsafe。下面的代码把一个方法标记为unsafe：&lt;br /&gt;&lt;br /&gt;unsafe int GetSomeNumber()&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   // code that can use pointers&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;任何方法都可以标记为unsafe—— 无论该方法是否应用了其他修饰符(例如，静态方法、虚拟方法等)。在这种方法中，unsafe修饰符还会应用到方法的参数上，允许把指针用作参数。还可以把整个类或结构标记为unsafe，表示所有的成员都是不安全的：&lt;br /&gt;&lt;br /&gt;unsafe class MyClass&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   // any method in this class can now use pointers&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;同样，可以把成员标记为unsafe：&lt;br /&gt;&lt;br /&gt;class MyClass&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   unsafe int *pX;   // declaration of a pointer field in a class&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;也可以把方法中的一个代码块标记为unsafe：&lt;br /&gt;&lt;br /&gt;void MyMethod()&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   // code that doesn't use pointers&lt;br /&gt;&lt;br /&gt;   unsafe&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      // unsafe code that uses pointers here&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;   // more 'safe' code that doesn't use pointers&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;但要注意，不能把局部变量本身标记为unsafe：&lt;br /&gt;&lt;br /&gt;int MyMethod()&lt;br /&gt;&lt;br /&gt;{ &lt;br /&gt;&lt;br /&gt;   unsafe int *pX;   // WRONG&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;如果要使用不安全的局部变量，就需要在方法或不安全的语句块中声明和使用它。在使用指针前还有一步要完成。C#编译器会拒绝不安全的代码，除非告诉编译器代码包含不安全的代码块。标记所用的关键字是unsafe。因此，要编译包含不安全代码的文件MySource.cs(假定没有其他编译器选项)，就要使用下述命令：&lt;br /&gt;&lt;br /&gt;csc /unsafe MySource.cs&lt;br /&gt;&lt;br /&gt;或者&lt;br /&gt;&lt;br /&gt;csc –unsafe MySource.cs&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;如果使用Visual Studio .NET，就可以在项目属性中找到编译不安全代码的选项。对于本节中可下载示例的Visual Studio .NET版本，我们已经设置了不安全编译选项。&lt;br /&gt;&lt;br /&gt;2. 指针的语法&lt;br /&gt;把代码块标记为unsafe后，就可以使用下面的语法声明指针：&lt;br /&gt;&lt;br /&gt;int* pWidth, pHeight;&lt;br /&gt;&lt;br /&gt;double* pResult;&lt;br /&gt;&lt;br /&gt;byte*[] pFlags;&lt;br /&gt;&lt;br /&gt;这段代码声明了4个变量，pWidth和pHeight是整数指针，pResult是double型指针，pFlags是byte型的指针数组。我们常常在指针变量名的前面使用前缀p来表示这些变量是指针。在变量声明中，符号*表示声明一个指针，换言之，就是存储特定类型的变量的地址。&lt;br /&gt;&lt;br /&gt;提示：&lt;br /&gt;&lt;br /&gt;C++开发人员应注意，这个语法与C#中的语法是不同的。&lt;a href="http://futureangle.blogspot.com"&gt;C#&lt;/a&gt;语句中 int* pX, pY; 对应于C++ 语句中的 int *pX, *pY；在C#中，*符号与类型相关，而不是与变量名相关。&lt;br /&gt;&lt;br /&gt;声明了指针类型的变量后，就可以用与一般变量的方式使用它们，但首先需要学习另外两个运算符：&lt;br /&gt;&lt;br /&gt;●       &amp; 表示“取地址”，并把一个值数据类型转换为指针，例如int转换为*int。这个运算符称为寻址运算符。&lt;br /&gt;&lt;br /&gt;●       * 表示“获取地址的内容”，把一个指针转换为值数据类型(例如，*float转换为float)。这个运算符称为“间接寻址运算符”(有时称为“取消引用运算符”)。&lt;br /&gt;&lt;br /&gt;从这些定义中可以看出，&amp;和*的作用是相反的。&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;符号&amp;和*也表示按位AND(&amp;)和乘法(*)运算符，那么如何以这种方式使用它们？答案是在实际使用时它们是不会混淆的:用户和编译器总是知道在什么情况下这两个符号有什么含义，因为按照新指针的定义，这些符号总是以一元运算符的形式出现—— 它们只作用于一个变量，并出现在代码中变量的前面。另一方面，按位AND和乘法运算符是二元运算符，它们需要两个变量。&lt;br /&gt;&lt;br /&gt;下面的代码说明了如何使用这些运算符：&lt;br /&gt;&lt;br /&gt;int x = 10;&lt;br /&gt;&lt;br /&gt;int* pX, pY;&lt;br /&gt;&lt;br /&gt;pX = &amp;x;&lt;br /&gt;&lt;br /&gt;pY = pX;&lt;br /&gt;&lt;br /&gt;*pY = 20;&lt;br /&gt;&lt;br /&gt;首先声明一个整数x，接着声明两个整数指针pX和pY。然后把pX设置为指向x(换言之，把pX的内容设置为x的地址)。把pX的值赋予pY，所以pY也指向x。最后，在语句*pY = 20中，把值20赋予pY指向的地址。实际上是把x的内容改为20，因为pY指向x。注意在这里，变量pY和x之间没有任何关系。只是此时pY碰巧指向存储x的存储单元而已。&lt;br /&gt;&lt;br /&gt;要进一步理解这个过程，假定x存储在堆栈的存储单元0x12F8C4到0x12F8C7中(十进制就是1243332到1243335，即有4B，因为int占用4B)。因为堆栈向下分配内存，所以变量pX存储在0x12F8C0到 0x12F8C3的位置上，pY存储在0x12F8BC 到 0x12F8BF的位置上。注意，pX和pY也分别占用4B。这不是因为int占用4B，而是因为在32位处理器上，需要用4B存储一个地址。利用这些地址，在执行完上述代码后，堆栈应如图7-5所示。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;图  7-5&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;这个示例使用的是int来说明该过程，其中int存储在32位处理器中堆栈的连续空间上，但并不是所有的数据类型都会存储在连续的空间中。原因是32位处理器最擅长于在4B的内存块中获取数据。这种机器上的内存会分解为4字节的块，在Windows上，每个块都时常称为DWORD，因为这是32位无符号int在.NET出现之前的名字。这是从内存中获取DWORD的最高效的方式—— 跨越DWORD边界存储数据通常会降低硬件的性能。因此，.NET运行库通常会给某些数据类型加上一些空间，使它们占用的内存是4B的倍数。例如，short数据占用2B，但如果把一个short放在堆栈中，堆栈指针仍会减少4，而不是2，这样，下一个存储在堆栈中的变量就仍从DWORD的边界开始存储。&lt;br /&gt;&lt;br /&gt;可以把指针声明为任意一种数据类型—— 即任何预定义的数据类型uint、int和byte等，也可以声明为一个结构。但是不能把指针声明为一个类或数组，这是因为这么做会使垃圾收集器出现问题。为了正常工作，垃圾收集器需要知道在堆上创建了什么类实例，它们在什么地方。但如果代码使用指针处理类，将很容易破坏堆中.NET运行库为垃圾收集器维护的、与类相关的信息。在这里，垃圾收集器可以访问的数据类型称为托管类型，而指针只能声明为非托管类型，因为垃圾收集器不能处理它们。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-489320599199457163?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/489320599199457163/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/04/c1.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/489320599199457163'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/489320599199457163'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/04/c1.html' title='c#教程（三十六）指针(1)'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-2791734519088280137</id><published>2009-04-06T17:19:00.000-07:00</published><updated>2009-04-06T17:20:50.995-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、释放未托管的资源'/><title type='text'>c#教程（三十五） 释放未托管的资源</title><content type='html'>垃圾收集器的出现意味着，通常不需要担心不再需要的对象，只要让这些对象的所有引用都超出作用域，并允许垃圾收集器在需要时释放资源即可。但是，垃圾收集器不知道如何释放未托管的资源(例如文件句柄、网络连接和数据库连接)。托管类在封装对未托管资源的直接或间接引用时，需要制定专门的规则，确保未托管的资源在回收类的一个实例时释放。&lt;br /&gt;&lt;br /&gt;在定义一个类时，可以使用两种机制来自动释放未托管的资源。这些机制常常放在一起实现，因为每个机制都为问题提供了略为不同的解决方法。这两个机制是：&lt;br /&gt;&lt;br /&gt;●       声明一个析构函数，作为类的一个成员&lt;br /&gt;&lt;br /&gt;●       在类中实现System.IDisposable接口&lt;br /&gt;&lt;br /&gt;下面依次讨论这两个机制，然后介绍如何同时实现它们，以获得最佳的效果。&lt;br /&gt;&lt;br /&gt;7.2.1  析构函数&lt;br /&gt;前面介绍了构造函数可以指定必须在创建类的实例时进行的某些操作，在垃圾收集器删除对象时，也可以调用析构函数。由于执行这个操作，所以析构函数初看起来似乎是放置释放未托管资源、执行一般清理操作的代码的最佳地方。但是，事情并不是如此简单。&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;在讨论C#中的析构函数时，在底层的.NET结构中，这些函数称为Finalizers。在C#中定义析构函数时，编译器发送给程序集的实际上是Finalize()方法。这不会影响源代码，但如果需要查看程序集的内容，就应知道这个事实。&lt;br /&gt;&lt;br /&gt;C++开发人员应很熟悉析构函数的语法，它看起来类似于一个方法，与包含类同名，但前面加上了一个发音符号(~)。它没有返回类型，不带参数，没有访问修饰符。下面是一个    例子：&lt;br /&gt;&lt;br /&gt;class MyClass &lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   ~MyClass()  &lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      // implementation&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;C#编译器在编译析构函数时，会隐式地把析构函数的代码编译为Finalize()方法的对应代码，确保执行父类的Finalize()方法。下面列出了编译器为~MyClass()析构函数生成的IL的对应C#代码：&lt;br /&gt;&lt;br /&gt;protected override void Finalize()&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   try&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      // implementation&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;   finally&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      base. Finalize();   &lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;如上所示，在~MyClass()析构函数中执行的代码封装在Finalize()方法的一个try块中。对父类Finalize()方法的调用放在finally块中，确保该调用的执行。第11章会讨论try块和finally块。&lt;br /&gt;&lt;br /&gt;有经验的C++开发人员扩展了析构函数的用法，有时不仅用于清理资源，还提供调试信息或执行其他任务。C#析构函数的使用要比在C++中少得多，与C++析构函数相比，C#析构函数的问题是它们的不确定性。在删除C++对象时，其析构函数会立即运行。但由于垃圾收集器的工作方式，无法确定C#对象的析构函数何时执行。所以，不能在析构函数中放置需要在某一时刻运行的代码，也不应使用能以任意顺序对不同类实例调用的析构函数。如果对象占用了宝贵而重要的资源，应尽可能快地释放这些资源，此时就不能等待垃圾收集器来释放了。&lt;br /&gt;&lt;br /&gt;另一个问题是C#析构函数的执行会延迟对象最终从内存中删除的时间。没有析构函数的对象会在垃圾收集器的一次处理中从内存中删除，但有析构函数的对象需要两次处理才能删除：第一次调用析构函数时，没有删除对象，第二次调用才真正删除对象。另外，运行库使用一个线程来执行所有对象的Finalize()方法。如果频繁使用析构函数，而且使用它们执行长时间的清理任务，对性能的影响就会非常显著。&lt;br /&gt;&lt;br /&gt;7.2.2  IDisposable接口&lt;br /&gt;一个推荐替代析构函数的方式是使用System.IDisposable接口。IDisposable接口定义了一个模式(具有语言级的支持)，为释放未托管的资源提供了确定的机制，并避免产生析构函数固有的与垃圾函数器相关的问题。IDisposable接口声明了一个方法Dispose()，它不带参数，返回void，Myclass的方法Dispose()的执行代码如下：&lt;br /&gt;&lt;br /&gt;class Myclass : IDisposable&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   public void Dispose() &lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      // implementation&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;Dispose()的执行代码显式释放由对象直接使用的所有未托管资源，并在所有实现IDisposable接口的封装对象上调用Dispose()。这样，Dispose()方法在释放未托管资源时提供了精确的控制。&lt;br /&gt;&lt;br /&gt;假定有一个类ResourceGobbler，它使用某些外部资源，且执行IDisposable接口。如果要实例化这个类的实例，使用它，然后释放它，就可以使用下面的代码： &lt;br /&gt;&lt;br /&gt;ResourceGobbler theInstance = new ResourceGobbler();&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;   // do your processing &lt;br /&gt;&lt;br /&gt;  &lt;br /&gt;&lt;br /&gt;theInstance.Dispose();&lt;br /&gt;&lt;br /&gt;如果在处理过程中出现异常，这段代码就没有释放theInstance使用的资源，所以应使用try块(详见第11章)，编写下面的代码：&lt;br /&gt;&lt;br /&gt;ResourceGobbler theInstance = null;&lt;br /&gt;&lt;br /&gt;try&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   theInstance = new ResourceGobbler();&lt;br /&gt;&lt;br /&gt;// do your processing &lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;finally  &lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;  if (theInstance != null) theInstance.Dispose();&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;即使在处理过程中出现了异常，这个版本也可以确保总是在theInstance上调用Dispose()，总是释放由theInstance使用的资源。但是，如果总是要重复这样的结构，代码就很容易被混淆。C#提供了一种语法，可以确保在引用超出作用域时，在对象上自动调用Dispose()(但不是Close())。该语法使用了using关键字来完成这一工作—— 但目前，在完全不同的环境下，它与命名空间没有关系。下面的代码生成与try块相对应的IL代码：&lt;br /&gt;&lt;br /&gt;using (ResourceGobbler theInstance = new ResourceGobbler())&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   // do your processing &lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;using语句的后面是一对圆括号，其中是引用变量的声明和实例化，该语句使变量放在随附的复合语句中。另外，在变量超出作用域时，即使出现异常，也会自动调用其Dispose()方法。如果已经使用try块来捕获其他异常，就会比较清晰，如果避免使用using语句，仅在已有的try块的finally子句中调用Dispose()，还可以避免进行额外的缩进。&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;对于某些类来说，使用Close()要比Dispose()更富有逻辑性，例如，在处理文件或数据库连接时，就是这样。在这些情况下，常常实现IDisposable接口，再执行一个独立的Close()方法，来调用Dispose()。这种方法在类的使用上比较清晰，还支持C#提供的using语句。&lt;br /&gt;&lt;br /&gt;7.2.3  实现IDisposable接口和析构函数&lt;br /&gt;前面的章节讨论了类所使用的释放未托管资源的两种方式：&lt;br /&gt;&lt;br /&gt;●       利用运行库强制执行的析构函数，但析构函数的执行是不确定的，而且，由于垃圾收集器的工作方式，它会给运行库增加不可接受的系统开销。&lt;br /&gt;&lt;br /&gt;●       IDisposable接口提供了一种机制，允许类的用户控制释放资源的时间，但需要确保执行Dispose()。&lt;br /&gt;&lt;br /&gt;一般情况下，最好的方法是执行这两种机制，获得这两种机制的优点，克服其缺点。假定大多数程序员都能正确调用Dispose()，实现IDisposable接口，同时把析构函数作为一种安全的机制，以防没有调用Dispose()。下面是一个双重实现的例子：&lt;br /&gt;&lt;br /&gt;public class ResourceHolder : IDisposable&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   private bool isDispose ＝ false;&lt;br /&gt;&lt;br /&gt;public void Dispose() &lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      Dispose(true);&lt;br /&gt;&lt;br /&gt;      GC.SuppressFinalize(this); &lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;   protected virtual void Dispose(bool disposing) &lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;     if (!isDisposed)&lt;br /&gt;&lt;br /&gt;     {&lt;br /&gt;&lt;br /&gt;      if (disposing) &lt;br /&gt;&lt;br /&gt;{ &lt;br /&gt;&lt;br /&gt;         // Cleanup managed objects by calling their Dispose() methods.&lt;br /&gt;&lt;br /&gt;         }&lt;br /&gt;&lt;br /&gt;       // Cleanup unmanaged objects&lt;br /&gt;&lt;br /&gt;     }&lt;br /&gt;&lt;br /&gt;     isDisposed=true;&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;   ~ResourceHolder()&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      Dispose (false);&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;可以看出，Dispose()有第二个protected重载方法，它带一个bool参数，这是真正完成清理工作的方法。Dispose(bool)由析构函数和IDisposable.Dispose()调用。这个方式的重点是确保所有的清理代码都放在一个地方。&lt;br /&gt;&lt;br /&gt;传递给Dispose(bool)的参数表示Dispose(bool)是由析构函数调用，还是由IDisposable.Dispose()调用——Dispose(bool)不应从代码的其他地方调用，其原因是：&lt;br /&gt;&lt;br /&gt;●       如果客户调用IDisposable.Dispose()，该客户就指定应清理所有与该对象相关的资源，包括托管和非托管的资源。&lt;br /&gt;&lt;br /&gt;●       如果调用了析构函数，在原则上，所有的资源仍需要清理。但是在这种情况下，析构函数必须由垃圾收集器调用，而且不应访问其他托管的对象，因为我们不再能确定它们的状态了。在这种情况下，最好清理已知的未托管资源，希望引用的托管对象还有析构函数，执行自己的清理过程。&lt;br /&gt;&lt;br /&gt;isDispose成员变量表示对象是否已被删除，并允许确保不多次删除成员变量。这个简单的方法不是线程安全的，需要调用者确保在同一时刻只有一个线程调用方法。要求客户进行同步是一个合理的假定，在整个.NET类库中反复使用了这个假定(例如在集合类中)。第15章将讨论线程和同步。&lt;br /&gt;&lt;br /&gt;最后，IDisposable.Dispose()包含一个对System.GC. SuppressFinalize()方法的调用。SuppressFinalize()方法则告诉垃圾收集器有一个类不再需要调用其析构函数了。因为Dispose()已经完成了所有需要的清理工作，所以析构函数不需要做任何工作。调用SuppressFinalize()就意味着垃圾收集器认为这个对象根本没有析构函数。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-2791734519088280137?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/2791734519088280137/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/04/c_06.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/2791734519088280137'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/2791734519088280137'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/04/c_06.html' title='c#教程（三十五） 释放未托管的资源'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-1202959468963471940</id><published>2009-04-01T17:32:00.000-07:00</published><updated>2009-04-01T17:35:28.443-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、后台内存管理'/><title type='text'>c#教程（三十四）后台内存管理</title><content type='html'>C#编程的一个优点是程序员不需要担心具体的内存管理，尤其是垃圾收集器会处理所有的内存清理工作。用户可以得到像C++语言那样的效率，而不需要考虑像在C++中那样内存管理工作的复杂性。虽然不必手工管理内存，但如果要编写高效的代码，就仍需理解后台发生的事情。本节要介绍给变量分配内存时计算机内存中发生的情况。&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;本节的许多内容是没有经过事实证明的。您应把这一节看作是一般规则的简化向导，而不是实现的确切说明。&lt;br /&gt;&lt;br /&gt;7.1.1  值数据类型&lt;br /&gt;Windows使用一个系统：虚拟寻址系统，该系统把程序可用的内存地址映射到硬件内存中的实际地址上，这些任务完全由Windows在后台管理，其实际结果是32位处理器上的每个进程都可以使用4GB的内存—— 无论计算机上有多少硬盘空间。(在64位处理器上，这个数字会更大)。这个4GB内存实际上包含了程序的任何一部分—— 包括可执行代码、代码加载的所有DLL，以及程序运行时使用的所有变量的内容。这个4GB内存称为虚拟地址空间，或虚拟内存，为了方便起见，我们继续把它当作一般内存来使用。&lt;br /&gt;&lt;br /&gt;4GB中的每个存储单元都是从0开始往上排序的。要把一个值存储在内存的某个空间中，就需要提供表示该存储单元的数字。在任何高级语言中，例如C#、VB、C++和Java，编译器负责把人们可以理解的名称转换为处理器可以理解的内存地址。&lt;br /&gt;&lt;br /&gt;在进程的虚拟内存中，有一个区域称为堆栈。堆栈存储不是对象成员的值数据类型。另外，在调用一个方法时，也使用堆栈复制传递给方法的所有参数。为了理解堆栈的工作原理，需要注意在C#中变量的作用域。如果变量a在变量b之前进入作用域，b就会先出作用域。下面的代码：&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      int a;&lt;br /&gt;&lt;br /&gt;      // do something&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         int b;&lt;br /&gt;&lt;br /&gt;         // do something else&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;首先声明a。在内部的代码块中声明了b。然后内部的代码块终止，b就出作用域，最后a出作用域。所以b的生存期会完全包含在a的生存期中。在解除变量时，其顺序总是与给它们分配内存的顺序相反，这就是堆栈的工作方式。&lt;br /&gt;&lt;br /&gt;我们不知道堆栈在地址空间的什么地方，这些信息在进行C#开发是不需要知道的。堆栈指针(操作系统维护的一个变量) 包含堆栈中下一个自由空间的地址。程序第一次运行时，堆栈指针指向堆栈保留的内存块末尾。堆栈实际上是向下填充的，即从高内存地址向低内存地址填充。当数据入栈后，堆栈指针就会随之调整，以始终指向下一个自由空间。这种情况如图7-1所示。在该图中，显示了堆栈指针800000(16进制的0xC3500)，下一个自由空间是地址79999。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;图  7-1&lt;br /&gt;&lt;br /&gt;在下面的代码中，我们已告诉编译器需要一些存储单元以存储一个整数和一个双精度浮点数，这些存储单元会分别分配给nRacingCars和engineSize，声明每个变量的代码表示开始请求访问这个变量，闭合花括号表示不再请求其他变量。&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      int nRacingCars = 10;&lt;br /&gt;&lt;br /&gt;      double engineSize = 3000.0;&lt;br /&gt;&lt;br /&gt;      // do calculations;&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;假定使用如图7-1所示的堆栈。变量nRacingCars放在内存中，其值是10，这个值放在存储单元799996~799999上，这4个字节就在堆栈指针所指空间的下面。有4个字节是因为存储int要使用4个字节。为了容纳该int，应从堆栈指针中减去4，所以它现在指向位置799996，即下一个自由空间之后(79995)。&lt;br /&gt;&lt;br /&gt;当engineSize出作用域时，计算机就知道不再需要这个变量了。因为变量的生存期总是嵌套的，可以保证，当engineSize在作用域中时，无论发生什么情况，堆栈指针总是会指向存储engineSize的空间。为了从内存中删除这个变量，应给堆栈指针递增8，现在指向engineSize使用过的空间。此处就是放置闭合花括号的地方，当nRacingCars也出作用域时，堆栈指针就再次递增4，此时如果内存中又放入另一个变量，从799999开始的存储单元就会被覆盖，这些空间以前是存储nRacingCars的。&lt;br /&gt;&lt;br /&gt;如果编译器遇到像int i、j这样的代码，则这两个变量进入作用域的顺序就是不确定的：两个变量是同时声明的，也是同时出作用域的。此时，变量以什么顺序从内存中删除就不重要了。编译器在内部会确保先放在内存中的那个变量后删除，这样就能保证该规则不会与变量的生存期冲突。&lt;br /&gt;&lt;br /&gt;7.1.2  引用数据类型&lt;br /&gt;堆栈有非常高的性能，但对于所有的变量来说还是不太灵活。变量的生存期必须嵌套，在许多情况下，这种要求都过于苛刻。通常我们希望使用一个方法分配内存，来存储一些数据，并在方法退出后的很长一段时间内数据仍是可以使用的。只要是用new运算符来请求存储空间，就存在这种可能性——例如所有的引用类型。此时就要使用托管堆。&lt;br /&gt;&lt;br /&gt;如果以前编写过需要管理低级内存的C++代码，就会很熟悉堆(heap)。托管堆和C++使用的堆不同，它在垃圾收集器的控制下工作，与传统的堆相比有很显著的性能优势。&lt;br /&gt;&lt;br /&gt;托管堆(或简称为堆)是进程的可用4GB中的另一个内存区域。要了解堆的工作原理和如何为引用数据类型分配内存，看看下面的代码：&lt;br /&gt;&lt;br /&gt;   void DoWork()&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      Customer arabel;&lt;br /&gt;&lt;br /&gt;      arabel = new Customer();&lt;br /&gt;&lt;br /&gt;      Customer mrJones = new Nevermore60Customer();&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;在这段代码中，假定存在两个类Customer 和 Nevermore60Customer。这些类实际上取自于附录A中的Mortimer Phones例子(在www.wrox.com上)。&lt;br /&gt;&lt;br /&gt;首先，声明一个Customer引用，该引用名为arabel，在堆栈上给这个引用分配存储空间，但这仅是一个引用，而不是实际的Customer对象。arabel引用占用4个字节的空间，包含了存储Customer对象的地址(需要4个字节把0到4GB之间的地址存储为一个整数值)。&lt;br /&gt;&lt;br /&gt;然后看下一行代码：&lt;br /&gt;&lt;br /&gt;      arabel = new Customer();&lt;br /&gt;&lt;br /&gt;这行代码完成了以下操作：首先，分配堆上的内存，以存储Customer实例(一个真正的实例，不只是一个地址)。然后把变量arabel的值设置为分配给新Customer对象的内存地址(它还调用合适的Customer()构造函数初始化类实例中的字段，但我们不必担心这部分)。&lt;br /&gt;&lt;br /&gt;Customer实例没有放在堆栈中，而是放在内存的堆中。在这个例子中，现在还不知道一个Customer对象占用多少字节，但为了讨论方便，假定是32B。这32B包含了Customer实例字段，和.NET用于识别和管理其类实例的一些信息。&lt;br /&gt;&lt;br /&gt;为了在堆上找到一个存储新Customer对象的存储位置，.NET运行环境在堆中搜索，选取第一个未使用的、32B的连续块。为了讨论方便，假定其地址是200000，arabel引用占用堆栈中的799996~799999位置。这表示在实例化arabel对象前，内存的内容应如图7-2所示。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;图  7-2&lt;br /&gt;&lt;br /&gt;给Customer对象分配空间后，内存内容应如图7-3所示。注意，与堆栈不同，堆上的内存是向上分配的，所以自由空间在已用空间的上面。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;图  7-3&lt;br /&gt;&lt;br /&gt;下一行代码声明了一个Customer引用，并实例化一个Customer对象。在这个例子中，需要在堆栈上为mrJones引用分配空间，同时，也需要在堆上为它分配空间：&lt;br /&gt;&lt;br /&gt;      Customer mrJones = new Nevermore60Customer();&lt;br /&gt;&lt;br /&gt;该行把堆栈上的4B分配给mrJones引用，它存储在799992~799995位置上，而mrJones实例在堆上从200032开始向上分配空间。&lt;br /&gt;&lt;br /&gt;从这个例子可以看出，建立引用变量的过程要比建立值变量的过程更复杂，且不能避免性能的降低。实际上，我们对这个过程进行了过份的简化，因为.NET运行库需要保存堆的状态信息，在堆中添加新数据时，这些信息也需要更新。尽管有这些性能损失，但仍有一种机制，在给变量分配内存时，不会受到堆栈的限制。把一个引用变量的值赋予另一个相同类型的变量，就有两个引用内存中同一对象的变量了。当一个引用变量出作用域时，它会从堆栈中删除，如上一节所述，但引用对象的数据仍保留在堆中，一直到程序停止，或垃圾收集器删除它为止，而只有在该数据不再被任何变量引用时，才会被删除。&lt;br /&gt;&lt;br /&gt;7.1.3  垃圾收集&lt;br /&gt;由上面的讨论和图可以看出，托管堆的工作方式非常类似于堆栈，在某种程度上，连续的对象会在内存中一个挨一个地放置，这样就很容易使用指向下一个空闲存储单元的堆指针，来确定下一个对象的位置。在堆上添加更多的对象时，也容易调整。但这比较复杂，因为基于堆的对象的生存期与引用它们的基于堆栈的对象的作用域不匹配。&lt;br /&gt;&lt;br /&gt;在垃圾收集器运行时，会在堆中删除不再引用的所有对象。在完成删除动作后，堆会立即把对象分散开来，与已经释放的内存混合在一起，如图7-4所示。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;图  7-4&lt;br /&gt;&lt;br /&gt;如果托管的堆也是这样，在其上给新对象分配内存就成为一个很难处理的过程，运行库必须搜索整个堆，才能找到足够大的内存块来存储每个新对象。但是，垃圾收集器不会让堆处于这种状态。只要它释放了能释放的所有对象，就会压缩其他对象，把它们都移动回堆的端部，再次形成一个连续的块。因此，对于在什么地方存储新对象，堆可以继续像堆栈那样工作。当然，在移动对象时，这些对象的所有引用都需要用正确的新地址来更新，但垃圾收集器也会处理更新问题。&lt;br /&gt;&lt;br /&gt;垃圾收集器的这个压缩操作是托管的堆与旧未托管的堆的区别所在。使用托管的堆，就只需要读取堆指针的值即可，而不是搜索链接地址列表，来查找一个地方来放置新数据。因此，在.NET下实例化对象要快得多。有趣的是，访问它们也比较快，因为对象会压缩到堆上相同的内存区域，这样需要交换的页面较少。Microsoft相信，尽管垃圾收集器需要做一些工作，修改它移动的所有对象引用，致使性能降低，但这些性能会得到弥补。&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;一般情况下，垃圾收集器在.NET运行库认为需要时运行。可以通过调用System.GC.Collect()，强迫垃圾收集器在代码的某个地方运行，System.GC是一个表示垃圾收集器的.NET基类， Collect()方法则调用垃圾收集器。但是，这种方式适用的场合很少，例如，代码中有大量的对象刚刚停止引用，就适合调用垃圾收集器。但是，垃圾收集器的逻辑不能保证在一次垃圾收集过程中，所有未引用的对象都从堆中删除。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-1202959468963471940?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/1202959468963471940/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/04/c.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/1202959468963471940'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/1202959468963471940'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/04/c.html' title='c#教程（三十四）后台内存管理'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-6770675135073803652</id><published>2009-03-30T17:32:00.000-07:00</published><updated>2009-03-30T17:34:00.719-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、事件'/><title type='text'>c#教程（三十三） 生成事件</title><content type='html'>接收事件并响应它们仅是事件的一个方面。为了使事件真正发挥作用，还需要在代码中生成和引发事件。下面的例子将介绍如何创建、引发、接收和取消事件。&lt;br /&gt;&lt;br /&gt;这个例子包含一个窗体，它会引发另一个类正在监听的事件。在引发事件后，接收对象就确定是否执行一个过程，如果该过程未能继续，就取消事件。本例的目标是确定当前时间的秒数是大于30还是小于30。如果秒数小于30，就用一个表示当前时间的字符串设置一个属性；如果秒数大于30，就取消事件，把时间字符串设置为空。&lt;br /&gt;&lt;br /&gt;用于生成事件的窗体包含一个按钮和一个标签。下载的示例代码把按钮命名为btnRaise，标签命名为lblInfo，您也可以给标签使用其他名称。在创建窗体，添加两个控件后，就可以创建事件和相应的委托了。在窗体类的类声明部分，添加如下代码：&lt;br /&gt;&lt;br /&gt;public delegate void ActionEventHandler(object sender, ActionCancelEventArgs ev);&lt;br /&gt;&lt;br /&gt;public static event ActionEventHandler Action;&lt;br /&gt;&lt;br /&gt;这两行代码的作用是什么？首先，我们声明了一种新的委托类型ActionEventHandler。必须创建一种新委托，而不使用Framework预定义的委托，其原因是后面要使用定制的EventArgs类。方法签名必须与委托匹配。有了一个要使用的委托后，下一行代码就定义事件。在本例中定义了Action事件，定义事件的语法要求指定与事件相关的委托。还可以使用在Framework中定义的委托。从EventArgs类中派生出了近100个类，应该可以找到一个自己能使用的类。但本例使用的是定制的EventArgs类，所以必须创建一个与之匹配的新委托类型。&lt;br /&gt;&lt;br /&gt;基于EventArgs的新类ActionCancelEventArgs实际上派生于CancelEventArgs，而CancelEventArgs派生于EventArgs。CancelEventArgs添加了Cancel属性，该属性是一个布尔值，它通知sender对象，接收器希望取消或停止事件的处理。在ActionCancelEventArgs类中，还添加了Message属性，这是一个字符串属性，包含事件处理状态的文本信息。下面是ActionCancelEventHandler类的代码：&lt;br /&gt;&lt;br /&gt;public class ActionCancelEventHandler : System.ComponentModel. CancelEventArgs&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;  string _msg = "";&lt;br /&gt;&lt;br /&gt;  public ActionCancelEventArgs()  : base() {}&lt;br /&gt;&lt;br /&gt;  public ActionCancelEventArgs(bool cancel)  : base(cancel) {}&lt;br /&gt;&lt;br /&gt;  public ActionCancelEventArgs(bool cancel, string message)  : base(cancel)&lt;br /&gt;&lt;br /&gt; {&lt;br /&gt;&lt;br /&gt;   _msg = message;&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;  public string Message&lt;br /&gt;&lt;br /&gt;  {&lt;br /&gt;&lt;br /&gt;    get {return _msg;}&lt;br /&gt;&lt;br /&gt;    set {_msg = value;}&lt;br /&gt;&lt;br /&gt;  }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;可以看出，所有基于EventArgs的类都负责在发送器和接收器之间来回传送事件的信息。在大多数情况下，EventArgs类中使用的信息都被事件处理程序中的接收器对象使用。但是，有时事件处理程序可以把信息添加到EventArgs类中，使之可用于发送器。这就是本例使用EventArgs类的方式。注意在EventArgs类中有两个可用的构造函数。这种额外的灵活性增加了该类的可用性。&lt;br /&gt;&lt;br /&gt;目前声明了一个事件，定义了一个委托，并创建了ActionCancelEventArgs类。下一步需要引发事件。真正需要做的是用正确的参数调用事件，如本例所示：&lt;br /&gt;&lt;br /&gt;ActionCancelEventArgs ev = new CancelEventArgs();&lt;br /&gt;&lt;br /&gt;Action(this, ev);&lt;br /&gt;&lt;br /&gt;这非常简单。创建新的ActionCancelEventArgs类，并把它作为一个参数传递给事件。但是，这有一个小问题。如果事件不会在任何地方使用，该怎么办？如果还没有为事件定义处理程序，该怎么办？Action事件实际上是空的。如果试图引发该事件，就会得到一个空引用异常。如果要派生一个新的窗体类，并使用该窗体，把Action事件定义为基事件，则只要引发了Action事件，就必须执行其他一些操作。目前，我们必须在派生的窗体中激活另一个事件处理程序，这样才能访问它。为了使这个过程容易一些，并捕获空引用错误，就必须创建一个名为OnEventName的方法，其中EventName是事件名。在这个例子中，有一个名为OnAction的方法，下面是OnAction方法的完整代码：&lt;br /&gt;&lt;br /&gt;protected void OnAction(object sender, ActionCancelEventArgs ev)&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;  if(Action != null)&lt;br /&gt;&lt;br /&gt;     Action(sender, ev);&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;代码并不多，但完成了需要的工作。把该方法声明为protected，就只有派生类可以访问它。事件在引发之前还会进行空引用测试。如果派生一个包含该方法和事件的新类，就必须重写OnAction方法，然后再连接事件。为此，必须在重写代码中调用base.OnAction()。否则就不会引发该事件。在整个.NET Framework中都用这个命名约定，并在.NET SDK文档中对这一命名规则进行了说明。&lt;br /&gt;&lt;br /&gt;注意传送给OnAction方法的两个参数。它们看起来很熟悉，因为它们与需要传送给事件的参数相同。如果事件需要从另一个对象中引发，而不是从定义方法的对象中引发，就需要把访问修饰符设置为internal或public，而不能设置为protected。有时让类只包含事件声明和从其他类中调用的事件是有意义的。仍可以创建OnEventName方法，但它们是静态方法。&lt;br /&gt;&lt;br /&gt;目前，我们已经引发了事件，还需要一些代码来处理它。在项目中创建一个新类，在这个例子中把该类称为BusEntity。本项目的目的是检查当前时间的秒数，如果它小于30，就把一个字符串值设置为时间；如果它大于30，就把字符串设置为""，并取消事件。下面是代码：&lt;br /&gt;&lt;br /&gt;using System;&lt;br /&gt;&lt;br /&gt;using System.IO;&lt;br /&gt;&lt;br /&gt;using System.ComponentModel;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;namespace SimpleEvent&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;  public class BusEntity&lt;br /&gt;&lt;br /&gt;  {&lt;br /&gt;&lt;br /&gt;     string _time ="";&lt;br /&gt;&lt;br /&gt;     public BusEntity()&lt;br /&gt;&lt;br /&gt;     {&lt;br /&gt;&lt;br /&gt;        Form1.Action += new Form1.ActionEventHandler(Form1_Action);&lt;br /&gt;&lt;br /&gt;     }&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;     private void Form1_Action(object sender, ActionCancelEventArgs ev)&lt;br /&gt;&lt;br /&gt;     {&lt;br /&gt;&lt;br /&gt;        ev.Cancel = !DoAction();&lt;br /&gt;&lt;br /&gt;        if(ev.Cancel)&lt;br /&gt;&lt;br /&gt;          ev.Message ="Wasn’t the right time.";&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      private bool DoAction()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         bool retVal = false;&lt;br /&gt;&lt;br /&gt;         DateTime tm = DateTime.Now;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;         if(tm.second) &lt; 30)&lt;br /&gt;&lt;br /&gt;         {&lt;br /&gt;&lt;br /&gt;            _time ="The time is" + DateTime.Now.ToLongTimeString();&lt;br /&gt;&lt;br /&gt;            retVal = true;&lt;br /&gt;&lt;br /&gt;          }&lt;br /&gt;&lt;br /&gt;          else&lt;br /&gt;&lt;br /&gt;            _time = "";&lt;br /&gt;&lt;br /&gt;      &lt;br /&gt;&lt;br /&gt;         return retVal;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      public string TimeString&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         get {return _time;}&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;在构造函数中声明了Form1.Action事件的处理程序。注意其语法非常类似于前面的Click事件的语法。由于声明事件使用的模式都是相同的，所以语法也应保持一致。还要注意如何获取Action事件的引用，而无需在BusEntity类中有对Form1的引用。在Form1类中，将Action事件声明为静态，这并不是必需的，但这样可以更容易创建处理程序。我们可以把事件声明为public，但接着需要引用Form1的一个实例。&lt;br /&gt;&lt;br /&gt;在构造函数中编写事件时，调用添加到委托列表中的方法Form1_Action，并遵循命名标准。在处理程序中，需要确定是否取消事件。DoActions方法根据前面描述的时间条件返回一个布尔值，并把_time字符串设置为正确的值。&lt;br /&gt;&lt;br /&gt;在DoActions返回值后，就把该值赋给ActionCancelEventArgs的Cancel属性。EventArgs类一般仅在事件发送器和接收器之间来回传递值。如果取消了事件(ev.Cancel = true)，Message属性就设置为一个字符串值，以说明事件为什么被取消。&lt;br /&gt;&lt;br /&gt;如果再次查看btnRaise_Click事件处理程序的代码，就可以看出Cancel属性的使用方式：&lt;br /&gt;&lt;br /&gt;private void btnRaise_Click(object sender, EventArgs e)&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   ActionCancelEventArgs cancelEvent = new ActionCancelEventArgs();&lt;br /&gt;&lt;br /&gt;   OnAction(this, cancelEvent);&lt;br /&gt;&lt;br /&gt;   If(cancelEvent.Cancel)&lt;br /&gt;&lt;br /&gt;      lblInfo.Text = cancelEvent.Message;&lt;br /&gt;&lt;br /&gt;   else&lt;br /&gt;&lt;br /&gt;      lblInfo.Text = _busEntity.TimeString;&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;注意创建了ActionCancelEventArgs对象。接着引发了事件Action，并传递了新建的ActionCancelEventArgs对象。在调用OnAction方法，引发事件Action时，BusEntity对象中Action事件处理程序的代码就会执行。如果还有其他对象注册了事件Action，它们也会执行。记住，如果其他对象也处理这个事件，它们就会看到同一个ActionCancelEventArgs对象。如果需要确定是哪个对象取消了事件，而且如果有多个对象取消了事件，就需要在ActionCancelEventArgs类中包含某种基于列表的数据结构。&lt;br /&gt;&lt;br /&gt;在与事件委托一起注册的处理程序执行完毕后，就可以查询ActionCancelEventArgs对象，确定它是否被取消了。如果是，lblInfo就包含Message属性值；如果事件没有被取消，lblInfo就会显示当前时间。&lt;br /&gt;&lt;br /&gt;本节这基本上说明了如何利用事件和事件中基于EventArgs的对象，在应用程序中传递信息。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-6770675135073803652?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/6770675135073803652/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/03/c_30.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/6770675135073803652'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/6770675135073803652'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/03/c_30.html' title='c#教程（三十三） 生成事件'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-199907060055295483</id><published>2009-03-29T18:33:00.000-07:00</published><updated>2009-03-29T18:35:47.280-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、事件'/><title type='text'>c#教程（三十三） 事件</title><content type='html'>事件接收器是指在发生某些事情时被通知的任何应用程序、对象或组件。当然，有事件接收器，就有事件发送器。发送器的作用是引发事件。发送器可以是应用程序中的另一个对象或程序集，在系统事件中，例如鼠标单击或键盘按键，发送器就是.NET运行库。注意，事件的发送器并不知道接收器是谁。这就使事件非常有用。&lt;br /&gt;&lt;br /&gt;现在，在事件接收器的某个地方有一个方法，它负责处理事件。在每次发生已注册的事件时，就执行这个事件处理程序。此时就要使用委托了。由于发送器对接收器一无所知，所以无法设置两者之间的引用类型，而是使用委托作为中介。发送器定义接收器要使用的委托，接收器将事件处理程序注册到事件中。连接事件处理程序的过程称为封装事件。封装Click事件的简单例子有助于说明这个过程。&lt;br /&gt;&lt;br /&gt;首先创建一个简单的Windows窗体应用程序，把一个按钮控件从工具箱拖放到窗体上。在属性窗口中把按钮重命名为btnOne。在代码编辑器中把下面的代码添加到Form1构造函数中：&lt;br /&gt;&lt;br /&gt;btnOne.Click += new EventHandler(Button_Click);&lt;br /&gt;&lt;br /&gt;在Visual Studio中，注意在输入+=运算符之后，就只需按下Tab键两次，编辑器就会完成剩余的输入工作。在大多数情况下这很不错。但在这个例子中，不使用默认的处理程序名，所以应自己输入文本。&lt;br /&gt;&lt;br /&gt;这将告诉运行库，在引发btnOne的Click事件时，应执行Button_Click方法。EventHandler是事件用于把处理程序(Button_Click)赋予事件(Click)的委托。注意使用+=运算符把这个新方法添加到委托列表中。这类似于本章前面介绍的多播示例。也就是说，可以为事件添加多个事件处理程序。由于这是一个多播委托，所以要遵循添加多个方法的所有规则，如前所述，这并不能保证调用方法的顺序。下面在窗体上再添加一个按钮，把它重命名为btnTwo。把btnTwo的Click事件也连接到同一个Button_Click方法上，如下所示：&lt;br /&gt;&lt;br /&gt;btnOne.Click += new EventHandler(Button_Click);&lt;br /&gt;&lt;br /&gt;btnTwo.Click += new EventHandler(Button_Click);&lt;br /&gt;&lt;br /&gt;EventHandler委托已在Framework中定义了。它位于System命名空间，所有在Framework中定义的事件都使用它。如前所述，委托要求添加到委托列表中的所有方法都必须有相同的签名。显然事件委托也有这个要求。下面是Button_Click方法的定义：&lt;br /&gt;&lt;br /&gt;Private void Button_Click(object sender, Eventargs e)&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;这个方法有几个重要的地方。首先，它总是没有返回值。事件处理程序不能有返回值。其次是参数。只要使用EventHandler委托，参数就应是object和EventArgs。第一个参数是引发事件的对象，在这个例子中是btnOne或btnTwo，这取决于被单击的按钮。通过把一个引用发送给引发事件的对象，就可以把同一个的事件处理程序赋予多个对象。例如，可以为几个按钮定义一个按钮单击处理程序，接着根据sender参数确定单击了哪个按钮。&lt;br /&gt;&lt;br /&gt;第二个参数EventArgs是包含有关事件的其他有用信息的对象。这个参数可以是任意类型，只要它派生于EventArgs即可。MouseDown事件使用MouseDownEventArgs，它包含所使用按钮的属性、指针的XY坐标，以及与事件相关的其他信息。注意，其命名模式是在类型的后面加上EventArgs。本章的后面将介绍如何创建和使用基于EventArgs的定制对象。&lt;br /&gt;&lt;br /&gt;方法的命名也应注意，按照约定，事件处理程序应遵循“对象-事件”的命名约定。对象就是引发事件的对象，而事件就是被引发的事件。从可读性来看，应遵循这个命名约定。&lt;br /&gt;&lt;br /&gt;本例最后在处理程序中添加了一些代码，以完成一些工作。记住有两个按钮使用同一个处理程序。所以首先必须确定是哪个按钮引发了事件，接着调用应执行的操作。在本例中，只是在窗体的一个标签控件上输出一些文本。把一个标签控件从工具箱拖放到窗体上，并将其命名为lblInfo，然后在Button_Click方法中编写如下代码：&lt;br /&gt;&lt;br /&gt;if(((Button)sender).Name == "btnOne")&lt;br /&gt;&lt;br /&gt;   lblInfo.Text = "Button One was pressed";&lt;br /&gt;&lt;br /&gt;else&lt;br /&gt;&lt;br /&gt;   lblInfo.Text = "Button Two was pressed";&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;注意，由于sender参数作为对象发送，所以必须把它的数据类型转换为引发事件的对象类型，在本例中就是Button。本例使用Name属性确定是哪个按钮引发了对象，也可以使用其他属性。例如Tag属性就可以处理这种情形，因为它可以包含任何内容。为了了解事件委托的多播功能，给btnTwo的Click事件添加另一个方法，使用默认的方法名。窗体的构造函数如下所示：&lt;br /&gt;&lt;br /&gt;btnOne.Click += new EventHandler(Button_Click);&lt;br /&gt;&lt;br /&gt;btnTwo.Click += new EventHandler(Button_Click);&lt;br /&gt;&lt;br /&gt;btnTwo.Click += new EventHandler(btnTwo_Click);&lt;br /&gt;&lt;br /&gt;如果让Visual Studio为您创建存根(stub)，就会在源文件的末尾得到如下方法。但是，必须添加对MessageBox函数的调用：&lt;br /&gt;&lt;br /&gt;Private void btnTwo_Click(object sender, EventArgs e)&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;MessageBox.Show("This only happens in button 2 click event");&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;在运行这个例子时，单击btnOne会改变标签上的文本。单击btnTwo不仅会改变文本，还会显示消息框。注意，不能保证标签文本在消息框显示之前改变，所以不要在处理程序中编写具有顺序依赖性的代码。&lt;br /&gt;&lt;br /&gt;我们已经学习了许多概念，但要在接收器中编写的代码量是很小的。记住，编写事件接收器常常比编写事件发送器要频繁得多。至少在Windows用户界面上，Microsoft已经编写了所有需要的事件发送器(它们都在.NET基类中，在Windows.Forms命名空间中)。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-199907060055295483?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/199907060055295483/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/03/c_29.html#comment-form' title='2 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/199907060055295483'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/199907060055295483'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/03/c_29.html' title='c#教程（三十三） 事件'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-2412866301968218648</id><published>2009-03-26T17:29:00.000-07:00</published><updated>2009-03-26T17:30:10.413-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、委托'/><title type='text'>c#教程（三十二） 多播委托</title><content type='html'>前面使用的每个委托都只包含一个方法调用。调用委托的次数与调用方法的次数相同。如果要调用多个方法，就需要多次显式调用这个委托。委托也可以包含多个方法。这种委托称为多播委托。如果调用多播委托，就可以按顺序连续调用多个方法。为此，委托的签名就必须返回void(否则，返回值应送到何处？)。实际上，如果编译器发现某个委托返回void，就会自动假定这是一个多播委托。下面的代码取自于SimpleDelegate示例，尽管其语法与以前相同，但实际上它实例化了一个多播委托Operations：&lt;br /&gt;&lt;br /&gt;   delegate void DoubleOp(double value);&lt;br /&gt;&lt;br /&gt;//   delegate double DoubleOp(double value);   // can't do this now&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;   class MainEntryPoint&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      static void Main()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         DoubleOp operations = new DoubleOp(MathOperations.MultiplyByTwo);&lt;br /&gt;&lt;br /&gt;         operations += new DoubleOp(MathOperations.Square);&lt;br /&gt;&lt;br /&gt;在前面的示例中，要存储对两个方法的引用，所以实例化了一个委托数组。而这里只是在同一个委托中添加两个操作。多播委托可以识别运算符+和+=。还可以扩展上述代码中的最后两行，它们具有相同的效果：&lt;br /&gt;&lt;br /&gt;   DoubleOp operation1 = new DoubleOp(MathOperations.MultiplyByTwo);&lt;br /&gt;&lt;br /&gt;   DoubleOp operation2 = new DoubleOp(MathOperations.Square);&lt;br /&gt;&lt;br /&gt;   DoubleOp operations = operation1 + operation2;&lt;br /&gt;&lt;br /&gt;多播委托还识别运算符–和–=，以从委托中删除方法调用。&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;根据后面的内容，多播委托是一个派生于System.MulticastDelegate的类，它派生于基类System.Delegate。System.MulticastDelegate的其他成员允许把多个方法调用链接在一起，成为一个列表。&lt;br /&gt;&lt;br /&gt;为了说明多播委托的用法，下面把SimpleDelegate示例改写为一个新示例MulticastDelegate。现在需要把委托表示为返回void的方法，就应重写MathOperations类中的方法，让它们显示其结果，而不是返回它们：&lt;br /&gt;&lt;br /&gt;   class MathOperations&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      public static void MultiplyByTwo(double value)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         double result = value*2;&lt;br /&gt;&lt;br /&gt;         Console.WriteLine(&lt;br /&gt;&lt;br /&gt;            "Multiplying by 2: {0} gives {1}", value, result);&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      public static void Square(double value)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         double result = value*value;&lt;br /&gt;&lt;br /&gt;         Console.WriteLine("Squaring: {0} gives {1}", value, result);&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;为了适应这个改变，也必须重写ProcessAndDisplayNumber：&lt;br /&gt;&lt;br /&gt;static void ProcessAndDisplayNumber(DoubleOp action, double value)&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("\nProcessAndDisplayNumber called with value = " +&lt;br /&gt;&lt;br /&gt;                      value);&lt;br /&gt;&lt;br /&gt;   action(value);&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;下面测试多播委托，其代码如下：&lt;br /&gt;&lt;br /&gt;      static void Main()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         DoubleOp operations = new DoubleOp(MathOperations.MultiplyByTwo);&lt;br /&gt;&lt;br /&gt;         operations += new DoubleOp(MathOperations.Square);&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;         ProcessAndDisplayNumber(operations, 2.0);&lt;br /&gt;&lt;br /&gt;         ProcessAndDisplayNumber(operations, 7.94);&lt;br /&gt;&lt;br /&gt;         ProcessAndDisplayNumber(operations, 1.414);&lt;br /&gt;&lt;br /&gt;         Console.WriteLine();&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;现在，每次调用ProcessAndDisplayNumber时，都会显示一个信息，说明它已经被调用。然后，下面的语句会按顺序调用action委托实例中的每个方法：&lt;br /&gt;&lt;br /&gt;   action(value);&lt;br /&gt;&lt;br /&gt;运行这段代码，得到如下所示的结果：&lt;br /&gt;&lt;br /&gt;MulticastDelegate&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;ProcessAndDisplayNumber called with value = 2&lt;br /&gt;&lt;br /&gt;Multiplying by 2: 2 gives 4&lt;br /&gt;&lt;br /&gt;Squaring: 2 gives 4&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;ProcessAndDisplayNumber called with value = 7.94&lt;br /&gt;&lt;br /&gt;Multiplying by 2: 7.94 gives 15.88&lt;br /&gt;&lt;br /&gt;Squaring: 7.94 gives 63.0436&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;ProcessAndDisplayNumber called with value = 1.414&lt;br /&gt;&lt;br /&gt;Multiplying by 2: 1.414 gives 2.828&lt;br /&gt;&lt;br /&gt;Squaring: 1.414 gives 1.999396&lt;br /&gt;&lt;br /&gt;如果使用多播委托，就应注意对同一个委托调用方法链的顺序并未正式定义，因此应避免编写依赖于以任意特定顺序调用方法的代码。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-2412866301968218648?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/2412866301968218648/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/03/c_26.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/2412866301968218648'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/2412866301968218648'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/03/c_26.html' title='c#教程（三十二） 多播委托'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-7111139256613130647</id><published>2009-03-25T17:26:00.000-07:00</published><updated>2009-03-25T17:37:42.802-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、BubbleSorter、委托'/><title type='text'>c#教程（三十一） BubbleSorter委托示例</title><content type='html'>&lt;a href="http://3.bp.blogspot.com/__frkXe_FZc8/ScrN5UAB2VI/AAAAAAAAAB8/vbhyL95-kNA/s1600-h/%E6%9C%AA%E5%91%BD%E5%90%8D.jpg"&gt;&lt;img style="float:left; margin:0 10px 10px 0;cursor:pointer; cursor:hand;width: 320px; height: 270px;" src="http://3.bp.blogspot.com/__frkXe_FZc8/ScrN5UAB2VI/AAAAAAAAAB8/vbhyL95-kNA/s320/%E6%9C%AA%E5%91%BD%E5%90%8D.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5317288694569359698" /&gt;&lt;/a&gt;&lt;br /&gt;下面的示例将说明委托的用途。我们要编写一个类BubbleSorter，它执行一个静态方法Sort()，这个方法的第一个参数是一个对象数组，把该数组按照升序重新排列。换言之，假定传递的是int数组：{0, 5, 6, 2, 1}，则返回的结果应是{0, 1, 2, 5, 6}。&lt;br /&gt;&lt;br /&gt;冒泡排序算法非常著名，是一种排序的简单方法。它最适合于一小组数字，因为对于大量的数字(超过10个)，还有更高效的算法。冒泡排序算法重复遍历数组，比较每一对数字，按照需要交换它们的位置，把最大的数字逐步移动到数组的最后。对于给int排序，进行冒泡排序的方法如下所示：&lt;br /&gt;&lt;br /&gt;   // Note that this isn't part of the sample&lt;br /&gt;&lt;br /&gt;   for (int i = 0; i &lt; sortArray.Length; i++)&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      for (int j = i + 1; j &lt; sortArray.Length; j++)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         if (sortArray[j] &lt; sortArray[i])   // problem with this test&lt;br /&gt;&lt;br /&gt;         {&lt;br /&gt;&lt;br /&gt;            int temp = sortArray[i];   // swap ith and jth entries&lt;br /&gt;&lt;br /&gt;            sortArray[i] = sortArray[j];&lt;br /&gt;&lt;br /&gt;            sortArray[j] = temp;&lt;br /&gt;&lt;br /&gt;         }&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;它非常适合于int，但我们希望Sort()方法可以给任何对象排序。换言之，如果某段客户机代码包含Currency结构数组或其他类和结构，就需要对该数组排序。这样，上面代码中的if(sortArray[j] &lt; sortArray[i])就有问题了，因为它需要比较数组中的两个对象，看看哪一个更大。可以对int进行这样的比较，但如何对直到运行期间才知道或确定的新类进行比较？答案是客户机代码知道类在委托中传递的是什么方法，封装这个方法就可以进行比较。&lt;br /&gt;&lt;br /&gt;定义如下的委托：&lt;br /&gt;&lt;br /&gt;   delegate bool CompareOp(object lhs, object rhs);&lt;br /&gt;&lt;br /&gt;给Sort方法指定下述签名：&lt;br /&gt;&lt;br /&gt;   static public void Sort(object [] sortArray, CompareOp gtMethod)&lt;br /&gt;&lt;br /&gt;这个方法的文档说明强调，gtMethod必须表示一个静态方法，该方法带有两个参数，如果第二个参数的值“大于”第一个参数(换言之，它应放在数组中靠后的位置)，就返回true。&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;这里使用的是委托，但也可以使用接口来解决这个问题。.NET提供的IComparer接口就用于此目的。但是这里使用委托是因为这种问题本身要求使用委托。&lt;br /&gt;&lt;br /&gt;设置完毕后，下面定义类BubbleSorter：&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;为了使用这个类，需要定义一些其他类，建立要排序的数组。在本例中，假定Mortimer Phones移动电话公司有一个员工列表，要对照他们的薪水进行排序。每个员工分别由类的一个实例Employee表示，如下所示：&lt;br /&gt;&lt;br /&gt;   class Employee&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      private string name;&lt;br /&gt;&lt;br /&gt;      private decimal salary;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      public Employee(string name, decimal salary)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         this.name = name;&lt;br /&gt;&lt;br /&gt;         this.salary = salary;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      public override string ToString()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         return string.Format(name + ", {0:C}", salary);&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      public static bool RhsIsGreater(object lhs, object rhs)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         Employee empLhs = (Employee) lhs;&lt;br /&gt;&lt;br /&gt;         Employee empRhs = (Employee) rhs;&lt;br /&gt;&lt;br /&gt;         return (empRhs.salary &gt; empLhs.salary) ? true : false;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;注意，为了匹配CompareOp委托的签名，在这个类中必须定义RhsIsGreater，它的参数是两个对象引用，而不是Employee引用。必须把这些参数的数据类型转换为Employee引用，才能进行比较。&lt;br /&gt;&lt;br /&gt;下面编写一些客户机代码，完成排序：&lt;br /&gt;&lt;br /&gt;using System;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;namespace Wrox.ProCSharp.AdvancedCSharp&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   delegate bool CompareOp(object lhs, object rhs);&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;   class MainEntryPoint&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      static void Main()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         Employee [] employees =&lt;br /&gt;&lt;br /&gt;            {&lt;br /&gt;&lt;br /&gt;               new Employee("Bugs Bunny", 20000), &lt;br /&gt;&lt;br /&gt;               new Employee("Elmer Fudd ", 10000),&lt;br /&gt;&lt;br /&gt;               new Employee("Daffy Duck", 25000),&lt;br /&gt;&lt;br /&gt;               new Employee("Wiley Coyote", (decimal)1000000.38),&lt;br /&gt;&lt;br /&gt;               new Employee("Foghorn Leghorn", 23000),&lt;br /&gt;&lt;br /&gt;               new Employee("RoadRunner'", 50000)};&lt;br /&gt;&lt;br /&gt;         CompareOp employeeCompareOp = new CompareOp(Employee.RhsIsGreater);&lt;br /&gt;&lt;br /&gt;         BubbleSorter.Sort(employees, employeeCompareOp);&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;运行这段代码，正确显示按照薪水排列的Employee，如下所示：&lt;br /&gt;&lt;br /&gt;BubbleSorter&lt;br /&gt;&lt;br /&gt;Elmer Fudd, $10,000.00&lt;br /&gt;&lt;br /&gt;Bugs Bunny, $20,000.00&lt;br /&gt;&lt;br /&gt;Foghorn Leghorn, $23,000.00&lt;br /&gt;&lt;br /&gt;Daffy Duck, $25,000.00&lt;br /&gt;&lt;br /&gt;RoadRunner, $50,000.00&lt;br /&gt;&lt;br /&gt;Wiley Coyote, $1,000,000.38&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-7111139256613130647?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/7111139256613130647/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/03/c-bubblesorter.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/7111139256613130647'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/7111139256613130647'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/03/c-bubblesorter.html' title='c#教程（三十一） BubbleSorter委托示例'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://3.bp.blogspot.com/__frkXe_FZc8/ScrN5UAB2VI/AAAAAAAAAB8/vbhyL95-kNA/s72-c/%E6%9C%AA%E5%91%BD%E5%90%8D.jpg' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-2148114919666871134</id><published>2009-03-23T17:33:00.000-07:00</published><updated>2009-03-23T23:44:43.672-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、委托'/><title type='text'>c#教程（三十） 简单的委托示例</title><content type='html'>在这个示例中，定义一个类MathsOperations，它有两个静态方法，对double类型的值执行两个操作，然后使用该委托调用这些方法。这个数学类如下所示：&lt;br /&gt;&lt;br /&gt;   class MathsOperations&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      public static double MultiplyByTwo(double value)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         return value*2;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      public static double Square(double value)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         return value*value;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;下面调用这些方法：&lt;br /&gt;&lt;br /&gt;using System;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;namespace SimpleDelegate&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   delegate double DoubleOp(double x);&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;   class MainEntryPoint&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      static void Main()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         DoubleOp [] operations = &lt;br /&gt;&lt;br /&gt;            {&lt;br /&gt;&lt;br /&gt;               new DoubleOp(MathsOperations.MultiplyByTwo),&lt;br /&gt;&lt;br /&gt;               new DoubleOp(MathsOperations.Square)&lt;br /&gt;&lt;br /&gt;            };&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;         &lt;!--for (int i=0 ; i&lt;operations.Length ; i++)--&gt;&lt;br /&gt;&lt;br /&gt;         {&lt;br /&gt;&lt;br /&gt;            Console.WriteLine("Using operations[{0}]:", i);&lt;br /&gt;&lt;br /&gt;            ProcessAndDisplayNumber(operations[i], 2.0);&lt;br /&gt;&lt;br /&gt;            ProcessAndDisplayNumber(operations[i], 7.94);&lt;br /&gt;&lt;br /&gt;            ProcessAndDisplayNumber(operations[i], 1.414);&lt;br /&gt;&lt;br /&gt;            Console.WriteLine();&lt;br /&gt;&lt;br /&gt;         }&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      static void ProcessAndDisplayNumber(DoubleOp action, double value)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         double result = action(value);&lt;br /&gt;&lt;br /&gt;         Console.WriteLine(&lt;br /&gt;&lt;br /&gt;            "Value is {0}, result of operation is {1}", value, result);&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;在这段代码中，实例化了一个委托数组DoubleOp (记住，一旦定义了委托类，就基本上可以实例化它的实例，就像处理一般的类那样—— 所以把一些委托的实例放在数组中是可以的)。该数组的每个元素都初始化为由MathsOperations类执行的不同操作。然后循环这个数组，把每个操作应用到3个不同的值上。这说明了使用委托的一种方式—— 把方法组合到一个数组中，这样就可以在循环中调用不同的方法了。&lt;br /&gt;&lt;br /&gt;这段代码的关键代码行是把委托传递给ProcessAndDisplayNumber()方法，例如：&lt;br /&gt;&lt;br /&gt;            ProcessAndDisplayNumber(operations[i], 2.0);&lt;br /&gt;&lt;br /&gt;其中传递了委托名，但不带任何参数，假定operations[i]是一个委托，其语法是：&lt;br /&gt;&lt;br /&gt;●       "operations[i] " 表示 "这个委托"。换言之，就是委托代表的方法。&lt;br /&gt;&lt;br /&gt;●       "operations[i](2.0)" 表示 "调用这个方法，参数放在括号中"。&lt;br /&gt;&lt;br /&gt;ProcessAndDisplayNumber()方法定义为把一个委托作为其第一个参数：&lt;br /&gt;&lt;br /&gt;      static void ProcessAndDisplayNumber(DoubleOp action, double value)&lt;br /&gt;&lt;br /&gt;在这个方法中，调用：&lt;br /&gt;&lt;br /&gt;         double result = action(value);&lt;br /&gt;&lt;br /&gt;这实际上是调用action委托实例封装的方法，其返回结果存储在result中。&lt;br /&gt;&lt;br /&gt;运行这个示例，得到如下所示的结果：&lt;br /&gt;&lt;br /&gt;SimpleDelegate&lt;br /&gt;&lt;br /&gt;Using operations[0]:&lt;br /&gt;&lt;br /&gt;Value is 2, result of operation is 4&lt;br /&gt;&lt;br /&gt;Value is 7.94, result of operation is 15.88&lt;br /&gt;&lt;br /&gt;Value is 1.414, result of operation is 2.828&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;Using operations[1]:&lt;br /&gt;&lt;br /&gt;Value is 2, result of operation is 4&lt;br /&gt;&lt;br /&gt;Value is 7.94, result of operation is 63.0436&lt;br /&gt;&lt;br /&gt;Value is 1.414, result of operation is 1.999396&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-2148114919666871134?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/2148114919666871134/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/03/c_23.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/2148114919666871134'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/2148114919666871134'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/03/c_23.html' title='c#教程（三十） 简单的委托示例'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-5891949786576554085</id><published>2009-03-23T17:31:00.000-07:00</published><updated>2009-03-23T17:33:13.639-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、委托'/><title type='text'>c#教程（二十九） 在C#中使用委托</title><content type='html'>在C#中使用一个类时，分两个阶段。首先需要定义这个类，即告诉编译器这个类由什么字段和方法组成。然后(除非只使用静态方法)实例化类的一个对象。使用委托时，也需要经过这两个步骤。首先定义要使用的委托，对于委托，定义它就是告诉编译器这种类型代表了哪种类型的方法，然后创建该委托的一个或多个实例。&lt;br /&gt;&lt;br /&gt;定义委托的语法如下：&lt;br /&gt;&lt;br /&gt;delegate void VoidOperation(uint x); &lt;br /&gt;&lt;br /&gt;在这个示例中，定义了一个委托VoidOperation，并指定该委托的每个实例都包含一个方法的细节，该方法带有一个uint参数，并返回void。理解委托的一个要点是它们的类型安全性非常高。在定义委托时，必须给出它所代表的方法的全部细节。&lt;br /&gt;&lt;br /&gt;提示：&lt;br /&gt;&lt;br /&gt;理解委托的一种好方式是把委托的作用当作是给方法签名指定名称。&lt;br /&gt;&lt;br /&gt;假定要定义一个委托TwoLongsOp ，该委托代表的函数有两个long型参数，返回类型为double型，可以编写如下代码：&lt;br /&gt;&lt;br /&gt;delegate double TwoLongsOp(long first, long second);&lt;br /&gt;&lt;br /&gt;或者定义一个委托，它代表的方法不带参数，返回一个string型的值，则可以编写如下代码：&lt;br /&gt;&lt;br /&gt;delegate string GetAString();&lt;br /&gt;&lt;br /&gt;其语法类似于方法的定义，但没有方法体，定义的前面要加上关键字delegate。因为定义委托基本上是定义一个新类，所以可以在定义类的任何地方定义委托，既可以在另一个类的内部定义，也可以在任何类的外部定义，还可以在命名空间中把委托定义为顶层对象。根据定义的可见性，可以在委托定义上添加一般的访问修饰符：public、 private和 protected等：&lt;br /&gt;&lt;br /&gt;public delegate string GetAString();&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;实际上，“定义一个委托”是指“定义一个新类”。委托实现为派生于基类System.MulticastDelegate的类，System.MulticastDelegate又派生于基类System.Delegate。C#编译器知道这个类，使用其委托语法，因此我们不需要了解这个类的具体执行情况，这是C#与基类共同合作，使编程更易完成的另一个示例。&lt;br /&gt;&lt;br /&gt;定义好委托后，就可以创建它的一个实例，以存储特定方法的细节。&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;此处，在术语方面有一个问题。类有两个不同的术语:“类”表示较广义的定义，“对象”表示类的实例。但委托只有一个术语。在创建委托的实例时，所创建的委托的实例仍称为委托。您需要从上下文中确定委托的确切含义。&lt;br /&gt;&lt;br /&gt;下面的代码段说明了如何使用委托。这是在int上调用ToString()方法的一种相当冗长的方式：&lt;br /&gt;&lt;br /&gt;private delegate string GetAString();&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;static void Main(string[] args)&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   int x = 40;&lt;br /&gt;&lt;br /&gt;   GetAString firstStringMethod = new GetAString(x.ToString);&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("String is" + firstStringMethod());&lt;br /&gt;&lt;br /&gt;   // With firstStringMethod initialized to x.ToString(), &lt;br /&gt;&lt;br /&gt;   // the above statement is equivalent to saying &lt;br /&gt;&lt;br /&gt;   // Console.WriteLine("String is" + x.ToString());&lt;br /&gt;&lt;br /&gt;在这段代码中，实例化了类型为GetAString的一个委托，并对它进行初始化，使它引用整型变量x的ToString()方法。在C#中，委托在语法上总是带有一个参数的构造函数，这个参数就是委托引用的方法。这个方法必须匹配最初定义委托时的签名。所以在这个示例中，如果用不带参数、返回一个字符串的方法来初始化firstStringMethod，就会产生一个编译错误。注意，int.ToString()是一个实例方法(不是静态方法)，所以需要指定实例(x)和方法名来正确初始化委托。&lt;br /&gt;&lt;br /&gt;下一行代码使用这个&lt;a href="http://futureangle.blogspot.com"&gt;委托&lt;/a&gt;来显示字符串。在任何代码中，都应提供委托实例的名称，后面的括号中应包含调用该委托中的方法时使用的参数。所以在上面的代码中，Console.WriteLine()语句完全等价于注释语句中的代码行。&lt;br /&gt;&lt;br /&gt;委托的一个特征是它们的类型是安全的，可以确保被调用的方法签名是正确的。但有趣的是，它们不关心调用该方法的是什么类型的对象，甚至不考虑该方法是静态方法，还是实例方法。&lt;br /&gt;&lt;br /&gt;提示：&lt;br /&gt;&lt;br /&gt;给定委托的实例可以表示任何类型的任何对象上的实例方法或静态方法—— 只要方法的特征匹配于委托的特征即可。&lt;br /&gt;&lt;br /&gt;为了说明这一点，我们扩展上面的代码，让它使用firstStringMethod委托在另一个对象上调用其他两个方法，其中一个方法是实例方法，另一个方法是静态方法。为此，再次使用本章前面定义的Currency结构。&lt;br /&gt;&lt;br /&gt;   struct Currency&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      public uint Dollars;&lt;br /&gt;&lt;br /&gt;      public ushort Cents;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      public Currency(uint dollars, ushort cents)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         this.Dollars = dollars;&lt;br /&gt;&lt;br /&gt;         this.Cents = cents;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;      public override string ToString()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         return string.Format("${0}.{1,–2:00}", Dollars,Cents);&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;      public static explicit operator Currency (float value)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         checked&lt;br /&gt;&lt;br /&gt;         {&lt;br /&gt;&lt;br /&gt;            uint dollars =(uint)value;&lt;br /&gt;&lt;br /&gt;            ushort cents =(ushort)((value-dollars)*100);&lt;br /&gt;&lt;br /&gt;            return new Currency(dollars,cents);&lt;br /&gt;&lt;br /&gt;          }&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;      public static implicit operator float (Currency value)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         return value.Dollars + (value.Cents/100.0f);&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;public static implicit operator Currency (uint value)&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   return new Currency(value, 0);&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;      public static implicit operator uint (Currency value)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         return value.Dollars;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;Currency结构已经有了自己的ToString()重载方法。为了说明如何使用带有静态方法的委托，再增加一个静态方法，其签名与Currency的签名相同：&lt;br /&gt;&lt;br /&gt;   struct Currency&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      public static string GetCurrencyUnit()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         return "Dollar";&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;下面使用GetAString 实例，代码如下所示：&lt;br /&gt;&lt;br /&gt;      private delegate string GetAString();&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      static void Main(string[] args)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         int x = 40;&lt;br /&gt;&lt;br /&gt;         GetAString firstStringMethod = new GetAString(x.ToString);&lt;br /&gt;&lt;br /&gt;         Console.WriteLine("String is " + firstStringMethod());&lt;br /&gt;&lt;br /&gt;         Currency balance = new Currency(34, 50);&lt;br /&gt;&lt;br /&gt;         firstStringMethod = new GetAString(balance.ToString);&lt;br /&gt;&lt;br /&gt;         Console.WriteLine("String is " + firstStringMethod());&lt;br /&gt;&lt;br /&gt;         firstStringMethod = new GetAString(Currency.GetCurrencyUnit);&lt;br /&gt;&lt;br /&gt;         Console.WriteLine("String is " + firstStringMethod());&lt;br /&gt;&lt;br /&gt;这段代码说明了如何通过委托来调用方法，然后重新给委托指定在类的不同实例上执行的不同方法，甚至可以指定静态方法，或者在类的不同类型的实例上执行的方法，只要每个方法的特征匹配于委托定义即可。&lt;br /&gt;&lt;br /&gt;但是，我们还没有说明把一个委托传递给另一个方法的具体过程，也没有给出任何有用的结果。调用int和Currency对象的ToString()的方法要比使用委托直观得多！在真正领会到委托的用途前，需要用一个相当复杂的示例来说明委托的本质。下面就是两个委托的示例。第一个示例仅使用委托来调用两个不同的操作，说明了如何把委托传递给方法，如何使用委托数组，但这仍没有很好地说明没有委托，就不能完成很多简单的工作。第二个示例就复杂得多了，它有一个类BubbleSorter，执行一个方法，按照升序排列一个对象数组，这个类没有委托是很难编写出来的。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-5891949786576554085?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/5891949786576554085/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/03/c-c.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/5891949786576554085'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/5891949786576554085'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/03/c-c.html' title='c#教程（二十九） 在C#中使用委托'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-4038650434867945335</id><published>2009-03-22T17:23:00.000-07:00</published><updated>2009-03-22T17:24:55.149-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、委托、事件'/><title type='text'>c#教程（二十八） 委托和事件</title><content type='html'>回调(call back)函数是Windows编程的一个重要部分。如果您具备C或C++编程背景，就曾在许多Windows API中使用过回调。VB6添加了AddressOf关键字后，开发人员就可以利用以前一度受到限制的API了。回调函数实际上是方法调用的指针，也称为函数指针，是一个非常强大的编程特性。.NET以委托的形式实现了函数指针的概念。它们的特殊之处是，与C函数指针不同，.NET委托是类型安全的。这说明，C中的函数指针只不过是一个指向存储单元的指针，我们无法说出这个指针实际指向什么，像参数和返回类型等就更无从知晓了。如本章所述，.NET把委托作为一种类型安全的操作。本章后面将学习.NET如何将委托用作实现事件的方式。&lt;br /&gt;&lt;br /&gt;6.1  委托&lt;br /&gt;最好将C#中的委托看作是对象的一种新类型，它与类有一些相似之处。当要把方法传送给其他方法时，需要使用它们。要了解它们的含义，可以看看下面的代码：&lt;br /&gt;&lt;br /&gt;int i = int.Parse("99");&lt;br /&gt;&lt;br /&gt;我们习惯于把数据作为参数传递给方法，如上面的例子所示。而有时某个方法执行的操作并不是针对数据进行的，而是要对另一个方法进行操作，这就比较复杂了。在编译时我们不知道第二个方法是什么，这个信息只能在运行时得到，所以需要把第二个方法作为参数传递给第一个方法，这听起来很令人迷惑，下面用几个示例来说明：&lt;br /&gt;&lt;br /&gt;●       启动线程—— 在C#中，可以告诉计算机并行运行某些新的执行序列。这种序列就称为线程，在基类System.Threading.Thread的一个实例上使用方法Start()，就可以开始执行一个线程。如果要告诉计算机开始一个新的执行序列，就必须说明要在哪里执行该序列。必须为计算机提供开始执行的方法的细节，即Thread.Start()方法必须带有一个参数，该参数定义了要由线程调用的方法。&lt;br /&gt;&lt;br /&gt;●       通用类库—— 当然，有许多库包含执行各种标准任务的代码。这些库通常可以自我包含。这样在编写库时，就会知道任务该如何执行。但是有时在任务中还包含子任务，只有分别使用该库的客户机代码才知道如何执行这些子任务。例如编写一个类，它带有一个对象数组，并把它们按升序排列。但是，排序的部分过程会涉及到重复使用数组中的两个对象，比较它们，看看哪一个应放在前面。因为要编写的类必须能给任何对象数组排序，所以无法提前告诉计算机应如何比较对象。处理类中对象数组的客户机代码也必须告诉类如何比较要排序的对象。换言之，客户机代码必须传递某个可以进行这种比较的合适方法的类的细节。&lt;br /&gt;&lt;br /&gt;●       事件—— 一般是通知代码发生了什么事件。GUI编程主要是处理事件。在发生事件时，运行时需要知道应执行哪个方法。这就需要把处理事件的方法传送为委托的一个参数。这些将在本章后面讨论。&lt;br /&gt;&lt;br /&gt;前面建立了有时把方法的细节作为参数传递给其他方法的规则。下面需要指出如何完成这一过程。最简单的方式是把方法名作为参数传递出去。例如在前面的&lt;a href="http://futureangle.blogspot.com"&gt;线程&lt;/a&gt;示例中，假定要启动一个新线程，且有一个叫作EntryPoint()的方法，该方法是开始运行线程时的地方。&lt;br /&gt;&lt;br /&gt;void EntryPoint()&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   // do whatever the new thread needs to do&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;也可以用下面的代码开始执行新线程：&lt;br /&gt;&lt;br /&gt;Thread NewThread = new Thread();&lt;br /&gt;&lt;br /&gt;Thread.Start(EntryPoint);                   // WRONG&lt;br /&gt;&lt;br /&gt;实际上，这是一种很简单的方式，在一些语言如C和C++中使用的就是这种方式(在C和C++中，参数EntryPoint是一个函数指针)。&lt;br /&gt;&lt;br /&gt;但这种直接的方法会导致一些问题，例如类型的安全性，在进行面向对象编程时，方法很少是孤立存在的，在调用前，通常需要与类实例相关联。而这种方法并没有考虑到这个问题。所以.NET Framework在语法上不允许使用这种直接的方法。如果要传递方法，就必须把方法的细节包装在一种新类型的对象中，即委托。委托只是一种特殊的对象类型，其特殊之处在于，我们以前定义的所有对象都包含数据，而委托包含的只是方法的细节。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-4038650434867945335?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/4038650434867945335/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/03/c_22.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/4038650434867945335'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/4038650434867945335'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/03/c_22.html' title='c#教程（二十八） 委托和事件'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-4032733410141062691</id><published>2009-03-19T17:40:00.000-07:00</published><updated>2009-03-19T17:46:12.401-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、用户定义的数据类型转换'/><title type='text'>c#教程（二十七） 用户定义的数据类型转换(2)</title><content type='html'>Currency示例仅涉及到与float来回转换(一种预定义的数据示例)的类。实际上任何简单数据类型的转换都是可以自定义的。定义不同结构或类之间的数据类型转换是允许的，但有两个限制：&lt;br /&gt;&lt;br /&gt;q●   如果某个类直接或间接继承了另一个类，就不能在这两个类之间进行数据类型转换(这些类型的类型转换已经存在)。&lt;br /&gt;&lt;br /&gt;q●   数据类型转换必须在源或目标数据类型定义的内部定义。&lt;br /&gt;&lt;br /&gt;要说明这些要求，假定有如图5-1所示的类层次结构。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;图  5-1 &lt;br /&gt;&lt;br /&gt;换言之，类C和D间接派生于A。在这种情况下，在A、B、C或D之间惟一合法的类型转换就是类C和D之间的转换，因为这些类并没有互相派生。这段代码如下所示(假定希望数据类型转换是显式的，这通常是确定用户定义的数据类型转换的情况)：&lt;br /&gt;&lt;br /&gt;   public static explicit operator D(C value)&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      // and so on&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;   public static explicit operator C(D value)&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      // and so on&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;对于这些数据类型转换，可以选择放置定义的地方—— 在C的类定义内部，或者在D的类定义内部，但不能在其他地方定义。C#要求把数据类型转换的定义放在源类(或结构)或目标类(或结构)的内部。它的边界效应是不能定义两个类之间的数据类型转换，除非可以编辑它们的源代码。这是因为，这样可以防止第三方把数据类型转换引入类中。&lt;br /&gt;&lt;br /&gt;一旦在一个类的内部定义了数据类型转换，就不能在另一个类中定义相同的数据类型转换。显然，只能有一个数据类型转换，否则编译器就不知道该选择哪个数据类型转换了。&lt;br /&gt;&lt;br /&gt;2. 基类和派生类之间的数据类型转换&lt;br /&gt;要了解这些数据类型转换是如何工作的，首先看看源和目标的数据类型都是引用类型，再考虑两个类MyBase 和 MyDerived，其中MyDerived直接或间接派生于MyBase。&lt;br /&gt;&lt;br /&gt;首先是从MyDerived 到 MyBase的转换，代码如下(假定可以使用构造函数)：&lt;br /&gt;&lt;br /&gt;MyDerived derivedObject = new MyDerived();&lt;br /&gt;&lt;br /&gt;MyBase baseCopy = derivedObject;&lt;br /&gt;&lt;br /&gt;在本例中，是从MyDerived 隐式地转换为 MyBase。这是因为对类MyBase的任何引用都可以引用类MyBase的对象或派生于MyBase的对象。在OO编程中，派生类的实例实际上是基类的实例，但加上了一些额外的信息。在基类上定义的所有函数和字段也都在派生类上定义了。&lt;br /&gt;&lt;br /&gt;下面看看另一种方式，编写下面的代码：&lt;br /&gt;&lt;br /&gt;MyBase derivedObject = new MyDerived();&lt;br /&gt;&lt;br /&gt;MyBase baseObject = new MyBase();&lt;br /&gt;&lt;br /&gt;MyDerived derivedCopy1 = (MyDerived) derivedObject;   // OK&lt;br /&gt;&lt;br /&gt;MyDerived derivedCopy2 = (MyDerived) baseObject;      // Throws exception&lt;br /&gt;&lt;br /&gt;上面的代码都是合法的C#代码(从句法的角度来看，是合法的)，是把基类转换为派生类。但是，最后的一个语句在执行时会抛出一个异常。在进行数据类型转换时，会执行被引用的对象。因为基类引用实际上可以引用一个派生类实例，所以这个对象可能是要转换的派生类的一个实例。如果是这样，转换就会成功，派生的引用被设置为引用这个对象。但如果该对象不是派生类(或者派生于这个类的其他类)的一个实例，转换就会失败，抛出一个异常。&lt;br /&gt;&lt;br /&gt;注意，编译器已经提供了基类和派生类之间的转换，这种转换实际上并没有对对象进行任何数据转换。如果要进行的转换是合法的，它们也仅是把新引用设置为对对象的引用。这些转换在本质上与自己定义的转换不同。例如，在前面的SimpleCurrency示例中，我们定义了Currency结构和float之间的转换，在float到Currency的转换中，则创建了一个新Currency结构，并用要求的值进行初始化。在基类和派生类之间的预定义转换则不是这样。如果要把MyBase实例转换为MyDerived对象，其值根据MyBase实例的内容来确定，就不能使用数据类型转换语法，最合适的选项通常是定义一个派生类的构造函数，它的参数是一个基类实例，让这个构造函数执行相关的初始化：&lt;br /&gt;&lt;br /&gt;class DerivedClass : BaseClass&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   public DerivedClass(BaseClass rhs)&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      // initialize object from the Base instance&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;   // etc.&lt;br /&gt;&lt;br /&gt;3. 装箱和取消装箱数据类型转换&lt;br /&gt;前面主要讨论了基类和派生类之间的数据类型转换，其中，基类和派生类都是引用类型。其规则也适用于转换值类型，但转换值类型时，不是仅仅复制引用，还必须复制一些数据。&lt;br /&gt;&lt;br /&gt;当然，不能从结构或基本值类型中派生。所以基本结构和派生结构之间的转换总是基本类型或结构与System.Object之间的转换(理论上可以在结构和System.ValueType之间进行转换，但一般很少这么做)。&lt;br /&gt;&lt;br /&gt;在结构(或基本类型)到object的转换总是一种隐式转换，因为这种转换是从派生类型到基本类型的转换，即第2章中简要介绍的装箱过程。例如，Currency结构：&lt;br /&gt;&lt;br /&gt;Currency balance = new Currency(40,0);&lt;br /&gt;&lt;br /&gt;object baseCopy = balance;&lt;br /&gt;&lt;br /&gt;在执行上述隐式转换时，balance的内容被复制到堆上，放在一个装箱的对象上，BaseCopy对象引用设置为该对象。在后台发生的情况是：在最初定义Currency结构时，.NET Framework隐式地提供另一个(隐式)类，即装箱的Currency类，它包含与Currency结构相同的所有字段，但却是一个引用类型，存储在堆上。无论这个值类型是一个结构，还是一个枚举，定义它时都存在类似的装箱引用类型，对应于所有的基本值类型，如int、double和 uint。不能也不必在源代码中直接编程访问这些装箱类型，但在把一个值类型转换为object时，它们是在后台工作的对象。在隐式地把Currency 转换为 object时，会实例化一个装箱 Currency实例，并用Currency结构中的所有数据进行初始化。在上面的代码中，BaseCopy对象引用的就是这个已装箱的Currency实例。通过这种方式，就可以实现从派生类到基类的转换，并且，值类型的语法与引用类型的语法一样。&lt;br /&gt;&lt;br /&gt;转换的另一种方式称为取消装箱。与在基本引用类型和派生引用类型之间的转换一样，这是一种显式转换，因为如果要转换的对象不是正确的类型，会抛出一个异常：&lt;br /&gt;&lt;br /&gt;object derivedObject = new Currency(40,0);&lt;br /&gt;&lt;br /&gt;object baseObject = new object();&lt;br /&gt;&lt;br /&gt;Currency derivedCopy1 = (Currency)derivedObject;   // OK&lt;br /&gt;&lt;br /&gt;Currency derivedCopy2 = (Currency)baseObject;     // Exception thrown&lt;br /&gt;&lt;br /&gt;上述代码的工作方式与前面的引用类型一样。把DerivedObject转换为 Currency会成功进行，因为DerivedObject实际上引用的是装箱 Currency实例—— 转换的过程是把已装箱的 Currency对象的字段复制到一个新的Currency结构中。第二个转换会失败，因为BaseObject没有引用已装箱的 Currency对象。&lt;br /&gt;&lt;br /&gt;在使用装箱和取消装箱时，这两个过程都把数据复制到新装箱和取消装箱的对象上，理解这一点是非常重要的。这样，对装箱对象的操作就不会影响原来值类型的内容。&lt;br /&gt;&lt;br /&gt;5.5.2  多重数据类型转换&lt;br /&gt;在定义数据类型转换时必须考虑的一个问题是，如果在进行要求的数据类型转换时， C#编译器没有可用的直接转换方式，就会寻找一种方式，把几种转换合并起来。例如，在Currency结构中，假定编译器遇到下面的代码：&lt;br /&gt;&lt;br /&gt;Currency balance = new Currency(10,50);&lt;br /&gt;&lt;br /&gt;long amount = (long)balance;&lt;br /&gt;&lt;br /&gt;double amountD = balance;&lt;br /&gt;&lt;br /&gt;首先初始化一个Currency实例，再把它转换为一个long。问题是不能定义这样的转换。但是，这段代码仍可以编译成功。因为编译器知道我们要定义一个从Currency到float的隐式转换，而且它知道如何显式地从float 转换为long。所以它会把这行代码编译为中间语言代码，首先把balance转换为float，再把结果转换为long。上述代码的最后一行也是这样，把balance转换为double型时，因为从Currency到 float的转换和float 到double的转换都是隐式的，就可以在代码中把这个转换当作一种隐式转换。如果要显式地指定转换过程，可以编写如下代码：&lt;br /&gt;&lt;br /&gt;Currency balance = new Currency(10,50);&lt;br /&gt;&lt;br /&gt;long amount = (long)(float)balance;&lt;br /&gt;&lt;br /&gt;double amountD = (double)(float)balance;&lt;br /&gt;&lt;br /&gt;但是，在大多数情况下，这会使代码变得比较复杂，因此是不必要的。下面的代码会产生一个编译错误：&lt;br /&gt;&lt;br /&gt;Currency balance = new Currency(10,50);&lt;br /&gt;&lt;br /&gt;long amount = balance;&lt;br /&gt;&lt;br /&gt;原因是编译器可以找到的最佳匹配的转换仍是首先转换为flost，再转换为long，但从float到long的转换需要显式指定。&lt;br /&gt;&lt;br /&gt;所有这些都不会带来太多的麻烦。转换的规则是非常直观的，主要是为了防止在开发人员不知情的情况下丢失数据。但是，在定义数据类型转换时如果不小心，编译器就有可能指定一条导致不期望的结果的路径。例如，假定编写Currency类的其他小组成员要把一个uint转换为Currency，而该uint中包含了美分的总数(美分不是美元，因为我们不希望丢掉美元的小数部分)，为此应编写如下代码：&lt;br /&gt;&lt;br /&gt;public static implicit operator Currency (uint value)&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   return new Currency(value/100u, (ushort)(value%100));&lt;br /&gt;&lt;br /&gt;} // Don't do this!&lt;br /&gt;&lt;br /&gt;注意，在这段代码中，第一个100后面的u可以确保把value/100u解释为uint。如果写成value/100，编译器就会把它解释为一个int型的值，而不是uint型的值。&lt;br /&gt;&lt;br /&gt;我们已经很清楚地在这段代码中注释了“不要这么做”。看看下面的代码段，它把包含350的uint转换为一个Currency，再转换回uint。那么在执行完这段代码后，bal2中又将包含什么？&lt;br /&gt;&lt;br /&gt;uint bal = 350;&lt;br /&gt;&lt;br /&gt;Currency balance = bal;&lt;br /&gt;&lt;br /&gt;uint bal2 = (uint)balance;&lt;br /&gt;&lt;br /&gt;答案不是350，而是3！这是符合逻辑的。我们把350隐式地转换为Currency，得到balance.Dollars=3，balance.Cents=50。然后编译器进行通常的操作，为转换回uint指定最佳路径。balance最终会被隐式地转换为float型(其值为3.5)，然后显式地转换为uint型，其值为3。&lt;br /&gt;&lt;br /&gt;当然，转换为另一个数据类型后，再转换回来有时会丢失数据。例如，把包含5.8的float转换为int，再转换回float，会丢失数字中的小数部分，得到5，但这和一个整数被100整除的情况略有区别。Currency现在成了一种相当危险的类，它会对整数进行一些奇怪的操作。&lt;br /&gt;&lt;br /&gt;问题是，在转换过程中如何解释整数是有矛盾的。从Currency到float的转换会把整数1解释为1美元，但uint到Currency的转换会把这个整数解释为1美分，这是很糟糕的。如果希望类易于使用，就应确保所有的转换都按一种手工兼容的方式执行，即这些转换应得到相同的结果。在本例中，显然要重新编写uint到Currency的转换，把整数值1解释为1美元：&lt;br /&gt;&lt;br /&gt;public static implicit operator Currency (uint value)&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   return new Currency(value, 0);&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;偶尔也会觉得这种新的转换方式可能根本不需要。但实际上这种转换方式是非常有用的。没有它，编译器在执行从uint到Currency的转换时，就只能通过float来进行。此时直接转换的效率要高得多，所以进行这种额外转换会提高性能，但需要确保它的结果与通过float进行转换得到的结果相同。在其他情况下，也可以为不同的预定义数据类型分别定义转换，让更多的转换隐式执行，而不是显式地执行，本例不是这样。&lt;br /&gt;&lt;br /&gt;测试这种转换是否成功，应确定无论使用什么转换路径，它都能得到相同的结果(而不是像在从float到int的转换过程中丢失数据那样)。Currency类就是一个很好的示例。下面的代码：&lt;br /&gt;&lt;br /&gt;Currency balance = new Currency(50, 35);&lt;br /&gt;&lt;br /&gt;ulong bal = (ulong) balance;&lt;br /&gt;&lt;br /&gt;目前，编译器只能采用一种方式来执行这个转换：把Currency隐式地转换为float。再显式地转换为ulong。从float到ulong的转换需要显式指定，本例就显式指定了这个转换，所以编译是成功的。&lt;br /&gt;&lt;br /&gt;但假定要添加另一个转换，从Currency隐式地转换为uint，就需要修改Currency结构，添加从uint到Currency的转换和从Currency到uint的转换，这段代码可以下载，作为SimpleCurrency2示例：&lt;br /&gt;&lt;br /&gt;      public static implicit operator Currency (uint value)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         return new Currency(value, 0);&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      public static implicit operator uint (Currency value)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         return value.Dollars;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;现在，编译器从Currency转换到 ulong可以使用另一条路径：先从Currency隐式地转换为uint，再隐式地转换为ulong。该采用哪条路径？ C#有一些规则(本书不详细讨论这些规则，您可参阅MSDN文档说明)，告诉编译器如何确定哪条是最佳路径。但最好自己设计转换，让所有的转换都得到相同的结果(但没有精确度的损失)，此时编译器选择哪条路径就不重要了(在本例中，编译器会选择Currency→uint→ulong路径，而不是Currency→float→ulong路径)。&lt;br /&gt;&lt;br /&gt;为了测试SimpleCurrency2示例，给SimpleCurrency的测试程序添加如下代码：&lt;br /&gt;&lt;br /&gt;try&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   Currency balance = new Currency(50,35);&lt;br /&gt;&lt;br /&gt;   Console.WriteLine(balance);&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("balance is " + balance);&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("balance is (using ToString()) " + balance.ToString());&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;   uint balance3 = (uint) balance;&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("Converting to uint gives " + balance3);&lt;br /&gt;&lt;br /&gt;运行这个示例，得到如下所示的结果：&lt;br /&gt;&lt;br /&gt;SimpleCurrency2&lt;br /&gt;&lt;br /&gt;50&lt;br /&gt;&lt;br /&gt;balance is $50.35&lt;br /&gt;&lt;br /&gt;balance is (using ToString()) $50.35&lt;br /&gt;&lt;br /&gt;Converting to uint gives 50&lt;br /&gt;&lt;br /&gt;After converting to float, = 50.35&lt;br /&gt;&lt;br /&gt;After converting back to Currency, = $50.34&lt;br /&gt;&lt;br /&gt;Now attempt to convert out of range value of–$100.00 to a Currency:&lt;br /&gt;&lt;br /&gt;Exception occurred: Arithmetic operation resulted in an overflow.&lt;br /&gt;&lt;br /&gt;这个结果显示了到uint的转换是成功的，但丢失了Currency的美分部分(小数部分)，把负的float 转换为 Currency也会产生意想不到的溢出异常，因为float到Currency的转换本身定义了一个checked环境。&lt;br /&gt;&lt;br /&gt;但是，这个输出结果也说明了进行转换时最后一个要注意的潜在问题：结果的第一行没有正确显示结余，应显示50，而不是$50.35。在下面的代码中：&lt;br /&gt;&lt;br /&gt;   Console.WriteLine(balance);&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("balance is " + balance);&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("balance is (using ToString()) " + balance.ToString());&lt;br /&gt;&lt;br /&gt;只有最后两行把Currency正确显示为一个字符串。这是为什么？问题是在把转换和方法重载合并起来时，会出现另一个不希望的错误源。下面用倒序的方式解释这段代码。&lt;br /&gt;&lt;br /&gt;第三行的Console.WriteLine()语句显式调用Currency.ToString()方法，以确保Currency显示为一个字符串。第二行代码没有这么做。字符串"balance is "传送给Console.WriteLine()，告诉编译器这个参数解释为字符串，因此要隐式地调用Currency.ToString()方法。&lt;br /&gt;&lt;br /&gt;但第一行的Console.WriteLine()方法只是把原来的Currency结构传送给Console.Write Line()。目前Console.WriteLine()有许多重载，但它们的参数都不是Currency结构。所以编译器会到处搜索，看看它能把Currency转换为什么，以与Console.WriteLine()的一个重载方法匹配。如上所示，Console.WriteLine()的一个重载方法可以快速而高效地显示uint，且其参数是一个uint。因此应把Currency隐式地转换为uint。&lt;br /&gt;&lt;br /&gt;实际上，Console.WriteLine()有另一个重载方法，它的参数是一个double，结果是显示该double的值。如果仔细看看第一个SimpleCurrency示例的结果，就会发现该结果的第一行就是使用这个重载方法把Currency显示为一个double。在这个示例中，没有直接把Currency转换为uint，所以编译器选择Currency→float→double作为可用于Console.WriteLine()重载方法的首选转换方式。但在SimpleCurrency2中可以直接转换为uint，所以编译器会选择后者。&lt;br /&gt;&lt;br /&gt;如果方法调用带有多个重载方法，并要给该方法传送参数，而该参数的数据类型不匹配任何重载方法，就可以迫使编译器确定该使用哪些转换方式进行数据转换，决定使用哪个重载方法(并进行相应的数据转换)。当然，编译器总是按逻辑和严格的规则来工作，但结果并不是我们所期望的。如果可能会出问题，最好显式指定转换路径。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-4032733410141062691?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/4032733410141062691/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/03/c-2_19.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/4032733410141062691'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/4032733410141062691'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/03/c-2_19.html' title='c#教程（二十七） 用户定义的数据类型转换(2)'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-31245393318307001</id><published>2009-03-18T18:07:00.000-07:00</published><updated>2009-03-18T18:08:08.883-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、用户定义的数据类型转换'/><title type='text'>c#教程（二十六） 用户定义的数据类型转换(1)</title><content type='html'>隐式转换和显式转换。&lt;br /&gt;&lt;br /&gt;显式转换要在代码中显式标记转换，其方法是在圆括号中写出目标数据类型：&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;   int I = 3;&lt;br /&gt;&lt;br /&gt;   long l = I;             // implicit&lt;br /&gt;&lt;br /&gt;   short s = (short)I;       // explicit&lt;br /&gt;&lt;br /&gt;对于预定义的数据类型，当数据类型转换可能失败或丢失某些数据时，需要显式转换。    例如：&lt;br /&gt;&lt;br /&gt;●       把int转换为short时，因为short可能不够大，不能包含转换的数值。&lt;br /&gt;&lt;br /&gt;●       把有符号的数据转换为无符号的数据，如果有符号的变量包含一个负值，会得到不正确的结果&lt;br /&gt;&lt;br /&gt;●       在把浮点数转换为整数数据类型时，数字的小数部分会丢失。&lt;br /&gt;&lt;br /&gt;此时应在代码中进行显式转换，告诉编译器你知道这会有丢失数据的危险，因此编写代码时要把这种可能性考虑在内。&lt;br /&gt;&lt;br /&gt;C#允许定义自己的数据类型(结构和类)，这意味着需要某些工具支持在自己的数据类型之间进行数据类型转换。方法是把数据类型转换定义为相关类的一个成员运算符，数据类型转换必须标记为隐式或显式，以说明如何使用它。我们应遵循与预定义数据类型转换相同的规则，如果知道无论在源变量中存储什么值，数据类型转换总是安全的，就可以把它定义为隐式转换。另一方面，如果某些数值可能会出错，例如丢失数据或抛出异常，就应把数据类型转换定义为显式转换。&lt;br /&gt;&lt;br /&gt;提示：&lt;br /&gt;&lt;br /&gt;如果源数据值会使数据类型转换失败，或者可能会抛出异常，就应把定制数据类型转换定义为显式转换。&lt;br /&gt;&lt;br /&gt;定义数据类型转换的语法类似于本章第一部分介绍的重载运算符。但它们是不一致的，数据类型转换在某种情况下可以看作是一种运算符，其作用是从源类型转换为目标类型。为了说明这个语法，下面的代码是从本节后面介绍的示例中节选的：&lt;br /&gt;&lt;br /&gt;public static implicit operator float (Currency value)&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   // processing&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;运算符的返回类型定义了数据类型转换操作的目标类型。这里定义的数据类型转换可以隐式地把Currency的值转换为float型。注意，如果数据类型转换声明为隐式，编译器可以隐式或显式地使用这个转换。如果数据类型转换声明为显式，编译器就只能显式地使用转换。其他运算符重载一样，数据类型转换必须声明为public和static。&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;C++开发人员应注意，这种情况与C++是不同的，在C++中，数据类型转换是类的成员    实例。&lt;br /&gt;&lt;br /&gt;5.5.1  执行用户定义的类型转换&lt;br /&gt;本节将在示例SimpleCurrency(和往常一样，其代码可以下载)中介绍隐式和显式使用用户定义的数据类型转换。在这个示例中，定义一个结构Currency，它包含一个正的US$钱款。C#为此提供了decimal类型，但如果要进行比较复杂的财务处理，仍可以编写自己的结构和类来表示钱款，在这样的类上执行特定的方法。&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;数据类型转换的语法对于结构和类是一样的。我们的示例定义了一个结构，但如果把Currency 声明为类，也是可以的。&lt;br /&gt;&lt;br /&gt;首先，结构Currency的定义如下所示。&lt;br /&gt;&lt;br /&gt;   struct Currency&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      public uint Dollars;&lt;br /&gt;&lt;br /&gt;      public ushort Cents;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      public Currency(uint dollars, ushort cents)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         this.Dollars = dollars;&lt;br /&gt;&lt;br /&gt;         this.Cents = cents;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;      public override string ToString()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         return string.Format("${0}.{1,–2:00}", Dollars,Cents);&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;Dollars和Cents字段使用无符号的数据类型，可以确保Currency实例只能包含正值。这样限制，是为了在后面说明显式转换的一些要点。可以像这样使用一个类来存储公司员工的薪水信息。人们的薪水不会是负值！为了使类比较简单，我们把字段声明为public，但通常应把它们声明为private，并为Dollars和Cents字段定义相应的属性。&lt;br /&gt;&lt;br /&gt;下面先假定要把Currency实例转换为float值，其中float值的整数部分表示美元，换言之，应编写下面的代码：&lt;br /&gt;&lt;br /&gt;   Currency balance = new Currency(10,50);&lt;br /&gt;&lt;br /&gt;   float f = balance; // We want f to be set to 10.5&lt;br /&gt;&lt;br /&gt;为此，需要定义一个数据类型转换。给Currency定义添加下述代码：&lt;br /&gt;&lt;br /&gt;      public static implicit operator float (Currency value)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         return value.Dollars + (value.Cents/100.0f);&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;这个数据类型转换是隐式的。在本例中这是一个合理的选择，因为在Currency定义中，这应是很清晰的，可以存储在Currency中的值也都可以存储在float中。在这个转换中，不应出现任何错误。&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;这里有一点欺骗性:实际上，当把uint转换为float时，就会有精确度的损失，但Microsoft认为这种错误并不重要，因此把从uint到float的转换都当做隐式转换。&lt;br /&gt;&lt;br /&gt;但是，如果把一个浮点数(float)转换为货币值，就不能保证转换肯定能成功了；float可以存储负值，而Currency实例不能，float存储的数值的量级要比Currency的(uint) Dollars字段大得多。所以，如果float包含一个不合适的值，把它转换为Currency就会得到意想不到的结果。因此，从float转换到Currency就应定义为显式转换。下面是我们的第一次尝试，这次不会得到正确的结果，但对第一次尝试是有帮助的：&lt;br /&gt;&lt;br /&gt;      public static explicit operator Currency (float value)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         uint dollars = (uint)value;&lt;br /&gt;&lt;br /&gt;         ushort cents = (ushort)((value–dollars)*100);&lt;br /&gt;&lt;br /&gt;         return new Currency(dollars, cents);&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;下面的代码可以成功编译：&lt;br /&gt;&lt;br /&gt;   float amount = 45.63f;&lt;br /&gt;&lt;br /&gt;   Currency amount2 = (Currency)amount;&lt;br /&gt;&lt;br /&gt;但是，下面的代码会抛出一个编译错误，因为试图隐式地使用一个显式的数据类型转换：&lt;br /&gt;&lt;br /&gt;   float amount = 45.63f;&lt;br /&gt;&lt;br /&gt;   Currency amount2 = amount;   // wrong&lt;br /&gt;&lt;br /&gt;把数据类型转换声明为显式，就是警告开发人员要小心，因为可能会丢失数据。但这不是我们希望的Currency结构的执行方式。下面编写一个测试程序，运行示例。其中有一个Main()方法，它实例化了一个Currency结构，试图进行几个转换。在这段代码的开头，以两种不同的方式计算balance的值(因为要使用它们来说明后面的内容)：&lt;br /&gt;&lt;br /&gt;static void Main()&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   try&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      Currency balance = new Currency(50,35);&lt;br /&gt;&lt;br /&gt;      Console.WriteLine(balance);&lt;br /&gt;&lt;br /&gt;      Console.WriteLine("balance is " + balance);&lt;br /&gt;&lt;br /&gt;      Console.WriteLine("balance is (using ToString()) " +&lt;br /&gt;&lt;br /&gt;         balance.ToString());&lt;br /&gt;&lt;br /&gt;      float balance2= balance;&lt;br /&gt;&lt;br /&gt;      Console.WriteLine("After converting to float, = " + balance2);&lt;br /&gt;&lt;br /&gt;      balance = (Currency) balance2;&lt;br /&gt;&lt;br /&gt;      Console.WriteLine("After converting back to Currency, = " + balance);&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      Console.WriteLine("Now attempt to convert out of range value of " +&lt;br /&gt;&lt;br /&gt;                        "–$100.00 to a Currency:");&lt;br /&gt;&lt;br /&gt;      checked&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         balance = (Currency) (–50.5);&lt;br /&gt;&lt;br /&gt;         Console.WriteLine("Result is " + balance.ToString());&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;   catch(Exception e)&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      Console.WriteLine("Exception occurred: " + e.Message);&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;注意，我们把所有的代码都放在一个try块中，来捕获在数据类型转换过程中发生的任何异常。在checked块中还添加了把超出范围的值转换为Currency的测试代码，所以，负值是肯定会被捕获的。运行这段代码，得到如下所示的结果：&lt;br /&gt;&lt;br /&gt;SimpleCurrency&lt;br /&gt;&lt;br /&gt;50.35&lt;br /&gt;&lt;br /&gt;Balance is $50.35&lt;br /&gt;&lt;br /&gt;Balance is (using ToString()) $50.35&lt;br /&gt;&lt;br /&gt;After converting to float, = 50.35&lt;br /&gt;&lt;br /&gt;After converting back to Currency, = $50.34&lt;br /&gt;&lt;br /&gt;Now attempt to convert out of range value of–$100.00 to a Currency:&lt;br /&gt;&lt;br /&gt;Result is $4294967246.60486&lt;br /&gt;&lt;br /&gt;这个结果表示代码并没有像我们希望的那样工作。首先，从float转换回Currency得到一个错误的结果$50.34，而不是$50.35。其次，在试图转换明显超出范围的值时，没有生成异常。&lt;br /&gt;&lt;br /&gt;第一个问题是由圆整错误引起的。如果类型转换用于把float转换为uint，计算机就会截去多余的数字，而不是圆整它。计算机以二进制方式存储数字，而不是十进制，小数部分0.35不能用二进制小数来表示(像1/3这样的小数不能表示为小数部分，它应等于循环小数0.3333)。所以，计算机最后存储了一个略小于0.35的值，它可以用二进制格式表示。把该数字乘以100，就会得到一个小于35的数字，截去了美分34，显然在本例中，这种由截去引起的错误是很严重的，避免该错误的方式是确保在数字转换过程中执行智能圆整操作。Microsoft编写了一个类System.Convert来完成该任务。System.Convert包含大量的静态方法来执行各种数字转换，我们需要使用的是Convert.To UInt16()。注意，在使用System.Convert方法时会产生额外的性能损失，所以只应在需要时才使用它们。&lt;br /&gt;&lt;br /&gt;下面看看为什么没有抛出期望的溢出异常。此处的问题是异常实际发生的位置根本不在Main()例程中——这是在转换运算符的代码中发生的，该代码在Main()方法中调用，而且没有标记为checked。&lt;br /&gt;&lt;br /&gt;其解决方法是确保类型转换本身也在checked环境下进行。进行了这两个修改后，修订后的转换代码如下所示。&lt;br /&gt;&lt;br /&gt;      public static explicit operator Currency (float value)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         checked&lt;br /&gt;&lt;br /&gt;         {&lt;br /&gt;&lt;br /&gt;            uint dollars = (uint)value;&lt;br /&gt;&lt;br /&gt;            ushort cents = Convert.ToUInt16((value–dollars)*100);&lt;br /&gt;&lt;br /&gt;            return new Currency(dollars, cents);&lt;br /&gt;&lt;br /&gt;         }&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;注意，使用Convert.ToUInt16()计算小数，如上所示，但没有使用它计算数字的美元部分。在计算美元值时不需要使用System.Convert，因为在此我们希望截去float值。&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;System.Convert方法还执行它们自己的溢出检查。因此对于本例的情况，不需要把对Convert.ToUInt16()的调用放在checked环境下。但把value显式转换为美元值仍需要checked环境。&lt;br /&gt;&lt;br /&gt;这里没有给出这个新checked转换的结果，因为在本节后面还要对SimpleCurrency示例进行一些修改。&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;如果定义了一个使用非常频繁的数据类型转换，其性能也非常好，就可以不进行任何错误检查，如果对用户定义的转换和没有检查的错误进行了清晰的说明，这也是一种合法的解决方案。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-31245393318307001?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/31245393318307001/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/03/c-1_18.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/31245393318307001'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/31245393318307001'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/03/c-1_18.html' title='c#教程（二十六） 用户定义的数据类型转换(1)'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-6367100553610060623</id><published>2009-03-17T17:28:00.000-07:00</published><updated>2009-03-17T17:29:31.063-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、运算符重载'/><title type='text'>c#教程（二十五） 运算符重载(2)</title><content type='html'>矢量除了可以相加之外，还可以相乘、相减，比较它们的值。本节通过添加几个运算符重载，扩展了这个例子。这并不是一个功能全面的真实的Vector类型，但足以说明运算符重载的其他方面了。首先要重载乘法运算符，以支持一个标量和多个矢量的相乘以及一个矢量和多个矢量的相乘。&lt;br /&gt;&lt;br /&gt;一个矢量乘以一个标量只是矢量的元素分别与标量相乘，例如，2 * (1.0, 2.5, 2.0)就等于(2.0, 5.0, 4.0)。相关的运算符重载如下所示。&lt;br /&gt;&lt;br /&gt;      public static Vector operator * (double lhs, Vector rhs)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         return new Vector(lhs * rhs.x, lhs * rhs.y, lhs * rhs.z);&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;但这还不够，如果a和b被声明为Vector 类型，就可以编写下面的代码：&lt;br /&gt;&lt;br /&gt;b = 2 * a;&lt;br /&gt;&lt;br /&gt;编译器会隐式地把整数2转换为double类型，以符合运算符重载的要求。但不能编译下面的代码：&lt;br /&gt;&lt;br /&gt;b = a * 2;&lt;br /&gt;&lt;br /&gt;编译器处理运算符重载的方式和处理方法重载的方式是一样的。它会查看给定运算符的所有可用重载，找到与之最匹配的那个运算符重载。上面的语句要求第一个参数是一个Vector，第二个参数是一个整数，或者可以隐式转换为整数的其他数据类型。我们没有提供这样一个重载。有一个运算符重载，其参数是一个double和一个Vector，但编译器不能改变参数的顺序，所以这是不行的。还需要显式定义一个运算符重载，其参数是一个Vector和一个double，有两种方式可以定义这样一个运算符重载，第一种方式和处理所有的运算符的方式一样，显式中断矢量相乘操作：&lt;br /&gt;&lt;br /&gt;      public static Vector operator * (Vector lhs, double rhs)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         return new Vector(rhs * lhs.x, rhs * lhs.y, rhs *lhs.z);&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;假定已经编写了执行相乘操作的代码，最好重复使用该代码：&lt;br /&gt;&lt;br /&gt;      public static Vector operator * (Vector lhs, double rhs)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         return rhs * lhs;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;这段代码会告诉编译器，如果有Vector和double的相乘操作，编译器就使参数的顺序反序，调用另一个运算符重载。在某种程度上，喜欢哪一个是由用户自己决定的。在本章的示例代码中，我们使用第二个版本，它看起来比较简洁。利用这个版本可以编写出维护性更好的代码，因为不需要复制代码，就可在两个独立的重载中执行相乘操作。&lt;br /&gt;&lt;br /&gt;要重载的下一个运算符是矢量相乘。在数学上，矢量相乘有两种方式，但这里我们感兴趣的是点积或内积，其结果实际上是一个标量。这就是我们介绍这个例子的原因，所以算术运算符不必返回与定义它们的类相同的类型。&lt;br /&gt;&lt;br /&gt;在数学上，如果有两个矢量(x, y, z)和(X, Y, Z)，其内积就是x*X + y*Y + z*Z的值。两个矢量这样相乘是很奇怪的，但这是很有效的，因为它可以用于计算各种其他的数。当然，如果要使用Direct3D 或DirectDraw编写代码来显示复杂的3D图形，在计算对象放在屏幕上的什么位置时，常常需要编写代码来计算矢量的内积，作为中间步骤。这里我们关心的是编写出double X = a*b，其中a和b是矢量，并计算出它们的点积。相关的运算符重载如下所示：&lt;br /&gt;&lt;br /&gt;      public static double operator * (Vector lhs, Vector rhs)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;定义了算术运算符后，就可以用一个简单的测试方法来看看它们是否能正常运行：&lt;br /&gt;&lt;br /&gt;static void Main()&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   // stuff to demonstrate arithmetic operations&lt;br /&gt;&lt;br /&gt;   Vector vect1, vect2, vect3;&lt;br /&gt;&lt;br /&gt;   vect1 = new Vector(1.0, 1.5, 2.0);&lt;br /&gt;&lt;br /&gt;   vect2 = new Vector(0.0, 0.0,–10.0);&lt;br /&gt;&lt;br /&gt;   vect3 = vect1 + vect2;&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("vect1 = " + vect1);&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("vect2 = " + vect2);&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("vect3 = vect1 + vect2 = " + vect3);&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("2*vect3 = " + 2*vect3);&lt;br /&gt;&lt;br /&gt;   vect3 += vect2;&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("vect3+=vect2 gives " + vect3);&lt;br /&gt;&lt;br /&gt;   vect3 = vect1*2;&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("Setting vect3=vect1*2 gives " + vect3);&lt;br /&gt;&lt;br /&gt;   double dot = vect1*vect3;&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("vect1*vect3 = " + dot);&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;运行代码，得到如下所示的结果：&lt;br /&gt;&lt;br /&gt;Vectors2&lt;br /&gt;&lt;br /&gt;vect1 = ( 1 , 1.5 , 2 )&lt;br /&gt;&lt;br /&gt;vect2 = ( 0 , 0 ,–10 )&lt;br /&gt;&lt;br /&gt;vect3 = vect1 + vect2 = ( 1 , 1.5 ,–8 )&lt;br /&gt;&lt;br /&gt;2*vect3 = ( 2 , 3 ,–16 )&lt;br /&gt;&lt;br /&gt;vect3+=vect2 gives ( 1 , 1.5 ,–18 )&lt;br /&gt;&lt;br /&gt;Setting vect3=vect1*2 gives ( 2 , 3 , 4 )&lt;br /&gt;&lt;br /&gt;vect1*vect3 = 14.5&lt;br /&gt;&lt;br /&gt;这说明，运算符重载会给出正确的结果，但如果仔细看看测试代码，就会惊奇地注意到，实际上我们使用的是没有重载的运算符—— 相加赋值运算符+=：&lt;br /&gt;&lt;br /&gt;   vect3 += vect2;&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("vect3 += vect2 gives " + vect3);&lt;br /&gt;&lt;br /&gt;虽然+=一般用作运算符，但实际上其操作分为两个部分：相加和赋值。与C++不同，C#不允许重载=运算符，但如果重载+运算符，编译器就会自动使用+运算符的重载来执行+=运算符的操作。–=、&amp;=、*=和/=赋值运算符也遵循此规则。&lt;br /&gt;&lt;br /&gt;2. 比较运算符重载&lt;br /&gt;C#中有6个比较运算符，它们分为3对：&lt;br /&gt;&lt;br /&gt;●       == 和 !=&lt;br /&gt;&lt;br /&gt;●       &gt; 和 &lt;&lt;br /&gt;&lt;br /&gt;●       &gt;= 和 &lt;=&lt;br /&gt;&lt;br /&gt;C#要求成对重载比较运算符。如果重载了==，也必须重载！=，否则会产生编译错误。另外，比较运算符必须返回bool类型的值。这是它们与算术运算符的根本区别。两个数相加或相减的结果，理论上取决于数的类型。而两个Vector的相乘会得到一个标量。另一个例子是.NET基类System.DateTime，两个DateTime相减，得到的结果不是DateTime，而是一个System.TimeSpan实例，但比较运算得到的如果不是bool类型的值，就没有任何意义。&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;在重载==和!=时，还应重载从System.Object中继承的Equals()和GetHashCode()方法，否则会产生一个编译警告。原因是Equals()方法应执行与==运算符相同的相等逻辑。&lt;br /&gt;&lt;br /&gt;除了这些区别外，重载比较运算符所遵循的规则与算术运算符相同。但比较两个数并不像想象的那么简单，例如，如果比较两个对象引用，就是比较存储对象的内存地址。比较运算符很少进行这样的比较，所以必须编写运算符，比较对象的值，返回相应的布尔结果。下面给Vector类重载== 和 !=运算符。首先是== 的执行代码：&lt;br /&gt;&lt;br /&gt;      public static bool operator = = (Vector lhs, Vector rhs)  &lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         if (lhs.x = = rhs.x &amp;&amp; lhs.y = = rhs.y &amp;&amp; lhs.z = = rhs.z)&lt;br /&gt;&lt;br /&gt;            return true;&lt;br /&gt;&lt;br /&gt;         else&lt;br /&gt;&lt;br /&gt;            return false;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;这种方式仅根据矢量组成部分的值，来对它们进行等于比较。对于大多数结构，这就是我们希望的，但在某些情况下，可能需要仔细考虑等于的含义，例如，如果有嵌入的类，是应比较对同一个对象的引用(浅度比较)，还是应比较对象的值是否相等(深度比较)？&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;不要通过调用从System.Object中继承的Equals()方法的实例版本，来重载比较运算符，如果这么做，就会在objA是null时计算(objA==objB)，这会产生一个异常，因为.NET运行库会试图计算null.Equals(objB)。采用其他方法(重写Equals()方法，调用比较运算符)比较安全。&lt;br /&gt;&lt;br /&gt;还需要重载运算符!=，采用的方式如下：&lt;br /&gt;&lt;br /&gt;      public static bool operator != (Vector lhs, Vector rhs)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;      return ! (lhs == rhs);&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;像往常一样，用一些测试代码检查重写方法的工作情况，这次定义3个Vector，并进行比较：&lt;br /&gt;&lt;br /&gt;      static void Main()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         Vector vect1, vect2, vect3;&lt;br /&gt;&lt;br /&gt;         vect1 = new Vector(3.0, 3.0,–10.0);&lt;br /&gt;&lt;br /&gt;         vect2 = new Vector(3.0, 3.0,–10.0);&lt;br /&gt;&lt;br /&gt;         vect3 = new Vector(2.0, 3.0, 6.0);&lt;br /&gt;&lt;br /&gt;         Console.WriteLine("vect1= =vect2 returns  " + (vect1= =vect2));&lt;br /&gt;&lt;br /&gt;         Console.WriteLine("vect1= =vect3 returns  " + (vect1= =vect3));&lt;br /&gt;&lt;br /&gt;         Console.WriteLine("vect2= =vect3 returns  " + (vect2= =vect3));&lt;br /&gt;&lt;br /&gt;         Console.WriteLine();&lt;br /&gt;&lt;br /&gt;         Console.WriteLine("vect1!=vect2 returns  " + (vect1!=vect2));&lt;br /&gt;&lt;br /&gt;         Console.WriteLine("vect1!=vect3 returns  " + (vect1!=vect3));&lt;br /&gt;&lt;br /&gt;         Console.WriteLine("vect2!=vect3 returns  " + (vect2!=vect3));&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;编译这些代码，会得到一个编译器警告，因为我们没有为Vector重写Equals()，对于本例，这是不重要的，所以我们会忽略它。&lt;br /&gt;&lt;br /&gt;csc Vectors3.cs&lt;br /&gt;&lt;br /&gt;Microsoft (R) Visual C# .NET Compiler version 7.00.9466&lt;br /&gt;&lt;br /&gt;for Microsoft (R) .NET Framework version 1.0.3705&lt;br /&gt;&lt;br /&gt;Copyright (C) Microsoft Corporation 2001. All rights reserved.&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;Vectors3.cs(5,11): warning CS0660: 'Wrox.ProCSharp.OOCSharp.Vector' defines&lt;br /&gt;&lt;br /&gt;        operator = = or operator != but does not override Object.Equals(object o)&lt;br /&gt;&lt;br /&gt;Vectors3.cs(5,11): warning CS0661: 'Wrox.ProCSharp.OOCSharp.Vector' defines&lt;br /&gt;&lt;br /&gt;        operator = = or operator != but does not override Object.GetHashCode()&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;在命令行上运行该示例，生成如下结果：&lt;br /&gt;&lt;br /&gt;Vectors3&lt;br /&gt;&lt;br /&gt;vect1= =vect2 returns  True&lt;br /&gt;&lt;br /&gt;vect1= =vect3 returns  False&lt;br /&gt;&lt;br /&gt;vect2= =vect3 returns  False&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;vect1!=vect2 returns  False&lt;br /&gt;&lt;br /&gt;vect1!=vect3 returns  True&lt;br /&gt;&lt;br /&gt;vect2!=vect3 returns  True&lt;br /&gt;&lt;br /&gt;3.  可以重载的运算符&lt;br /&gt;并不是所有的运算符都可以重载。可以重载的运算符如表5-4所示。&lt;br /&gt;&lt;br /&gt;表  5-4&lt;br /&gt;&lt;br /&gt;类    别&lt;br /&gt; 运 算 符&lt;br /&gt; 限    制&lt;br /&gt; &lt;br /&gt;算术二元运算符&lt;br /&gt; +, *, /, –, %&lt;br /&gt; 无&lt;br /&gt; &lt;br /&gt;算术一元运算符&lt;br /&gt; +, –, ++, ––&lt;br /&gt; 无&lt;br /&gt; &lt;br /&gt;按位二元运算符&lt;br /&gt; &amp;, |, ^, &lt;&lt;, &gt;&gt;&lt;br /&gt; 无&lt;br /&gt; &lt;br /&gt;按位一元运算符&lt;br /&gt; !, ~, true, false&lt;br /&gt; true和false运算符必须成对重载&lt;br /&gt; &lt;br /&gt;比较运算符&lt;br /&gt; ==, !=, &gt;=, &lt;, &lt;=, &gt;&lt;br /&gt; 必须成对重载&lt;br /&gt; &lt;br /&gt;赋值运算符&lt;br /&gt; +=,–=,*=,/=,&gt;&gt;=,&lt;&lt;=,%=&lt;br /&gt;&lt;br /&gt;,&amp;=,|=,^=&lt;br /&gt; 不能显式重载这些运算符，在重写单个运算符如+,–,%等时，它们会被隐式重写&lt;br /&gt; &lt;br /&gt;索引运算符&lt;br /&gt; []&lt;br /&gt; 不能直接重载索引运算符。第2章介绍的索引器成员类型允许在类和结构上支持索引运算符&lt;br /&gt; &lt;br /&gt;数据类型转换运算符&lt;br /&gt; ()&lt;br /&gt; 不能直接重载数据类型转换运算符。用户定义的数据类型转换(在本章的第2部分介绍)允许定义定制的数据类型转换&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-6367100553610060623?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/6367100553610060623/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/03/c-2_17.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/6367100553610060623'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/6367100553610060623'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/03/c-2_17.html' title='c#教程（二十五） 运算符重载(2)'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-6305378467819091329</id><published>2009-03-16T17:25:00.000-07:00</published><updated>2009-03-16T17:26:37.244-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、运算符重载'/><title type='text'>c#教程（二十四） 运算符重载(1)</title><content type='html'>C++开发人员应很熟悉运算符重载。但是，因为这个概念对Java和VB开发人员来说是全新的，所以这里要解释一下。C++开发人员可以直接跳到主要示例上。&lt;br /&gt;&lt;br /&gt;运算符重载的关键是在类实例上不能总是只调用方法或属性，有时还需要做一些其他的工作，例如对数值进行相加、相乘或逻辑操作，如比较对象等。例如，假定要定义一个类，表示一个数学矩阵，在数学中，矩阵可以相加和相乘，就像数字一样。所以可以编写下面的代码：&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;Matrix a, b, c;&lt;br /&gt;&lt;br /&gt;// assume a, b and c have been initialized &lt;br /&gt;&lt;br /&gt;Matrix d = c * (a + b);&lt;br /&gt;&lt;br /&gt;通过重载运算符，就可以告诉编译器，+和*对Matrix进行什么操作，以编写上面的代码。如果用不支持运算符重载的语言编写代码，就必须定义一个方法，以执行这些操作，结果肯定不太直观，如下所示。&lt;br /&gt;&lt;br /&gt;Matrix d = c.Multiply(a.Add(b));&lt;br /&gt;&lt;br /&gt;学习到现在，像+和*这样的运算符只能用于预定义的数据类型，原因很简单：编译器认为所有常见的运算符都是用于这些数据类型的，例如，它知道如何把两个long加起来，或者如何从一个double中减去另一个double，并生成合适的中间语言代码。但在定义自己的类或结构时，必须告诉编译器：什么方法可以调用，每个实例存储了什么字段等所有的信息。同样，如果要在自己的类上使用运算符，就必须告诉编译器相关的运算符在这个类中的含义。此时就要定义运算符重载。&lt;br /&gt;&lt;br /&gt;要强调的另一个问题是重载不仅仅限于算术运算符。还需要考虑比较运算符 ==、&lt;、&gt;、!=、&gt;=和&lt;=。例如，语句if(a==b)。对于类，这个语句在默认状态下会比较引用a和b，检测这两个引用是否指向内存中的同一个地址，而不是检测两个实例是否相同的数据。对于string类，这种操作就会重写，比较字符串实际上就是比较每个字符串的内容。可以对自己的类进行这样的操作。对于结构，==运算符在默认状态下不做任何工作，要比较两个结构，看看它们是否相等，如果相等就会产生一个编译错误，除非显式重载了==，告诉编译器如何进行比较。&lt;br /&gt;&lt;br /&gt;在许多情况下，重载运算符允许生成可读性更高、更直观的代码，包括：&lt;br /&gt;&lt;br /&gt;    ●   在数学领域中，几乎包括所有的数学对象：坐标、矢量、矩阵、张量和函数等。如果编写一个程序执行某些数学或物理建模，肯定会用类表示这些对象。&lt;br /&gt;&lt;br /&gt;●       图形程序在计算屏幕上的位置时，也使用数学或相关的坐标对象。&lt;br /&gt;&lt;br /&gt;●       表示大量金钱的类(例如，在财务程序中)。&lt;br /&gt;&lt;br /&gt;●       字处理或文本分析程序也有表示语句、子句等的类，可以使用运算符把语句连接在一起(这是字符串连接的一种比较复杂的版本)。&lt;br /&gt;&lt;br /&gt;另外，有许多类与运算符重载并不相关。不恰当地使用运算符重载，会使使用类型的代码很难理解。例如，把两个DateTime对象相乘，在概念上没有任何意义。&lt;br /&gt;&lt;br /&gt;5.4.1  运算符的工作方式&lt;br /&gt;为了理解运算符是如何重载的，考虑一下在编译器遇到运算符时会发生什么样的情况是很有用的——我们用相加运算符+作为例子来讲解。假定编译器遇到下面的代码：&lt;br /&gt;&lt;br /&gt;int a = 3;&lt;br /&gt;&lt;br /&gt;uint b = 2;&lt;br /&gt;&lt;br /&gt;double d = 4.0;&lt;br /&gt;&lt;br /&gt;long l = a + b;&lt;br /&gt;&lt;br /&gt;double x = d + a; &lt;br /&gt;&lt;br /&gt;会发生什么情况：&lt;br /&gt;&lt;br /&gt;long l = a + b;&lt;br /&gt;&lt;br /&gt;编译器知道它需要把两个整数加起来，并把结果赋予long。调用一个方法把数字加在一起时，表达式a+b是一种非常直观、方便的语法。该方法带有两个参数a和b，并返回它们的和。所以它完成的任务与任何方法调用是一样的—— 给定了参数类型后，它会查找最匹配的+运算符重载，把两个整数加在一起是很简单的。与一般的重载方法一样，预定义的返回类型不会因为调用的方法版本而影响编译器的选择。在本例中调用的重载方法带两个int类型参数，返回一个int，这个返回值随后会转换为long。&lt;br /&gt;&lt;br /&gt;下一行代码让编译器使用+运算符的另一个重载：&lt;br /&gt;&lt;br /&gt;double x = d + a;&lt;br /&gt;&lt;br /&gt;在这个例子中，参数是一个double类型的数据和一个int类型的数据，但+运算符没有带这种复合参数的重载形式，所以编译器认为，最匹配的+&lt;a href="http://futureangle.blogspot.com"&gt;运算符重载&lt;/a&gt;是把两个double作为其参数的版本，并隐式地把int转换为double。把两个double加在一起与把两个整数加在一起完全不同，浮点数存储为一个尾数和一个指数。把它们加在一起要按位移动一个double的尾数，让两个指数有相同的值，然后把尾数加起来，然后移动所得尾数的位，调整其指数，保证答案有尽可能高的精度。&lt;br /&gt;&lt;br /&gt;现在，看看如果编译器遇到下面的代码，会发生什么：&lt;br /&gt;&lt;br /&gt;Vector vect1, vect2, vect3;&lt;br /&gt;&lt;br /&gt;// initialise vect1 and vect2&lt;br /&gt;&lt;br /&gt;vect3 = vect1 + vect2;&lt;br /&gt;&lt;br /&gt;vect1 = vect1*2;&lt;br /&gt;&lt;br /&gt;其中，Vector是结构，稍后再定义它。编译器知道它需要把两个Vector加起来，即vect1 和 vect2。它会查找+运算符的重载，把两个Vector作为参数。&lt;br /&gt;&lt;br /&gt;如果编译器找到这样的运算符定义，就调用它的实现。如果找不到，就要看看有没有可以用作最佳匹配的其他+运算符重载，例如某个运算符重载的参数是其他数据类型，但可以隐式地转换为Vector实例。如果编译器找不到合适的运算符重载，就会产生一个编译错误，就像找不到其他方法调用的合适重载一样。&lt;br /&gt;&lt;br /&gt;5.4.2  运算符重载的示例：Vector结构&lt;br /&gt;本节将开发一个结构Vector，来演示运算符重载，这个结构Vector表示一个三维矢量。如果数学不是你的强项，不必担心，我们会使这个例子尽可能简单。三维矢量只是三个数字的一个集合，说明物体和原点之间的距离，表示数字的变量是x、y和z，x表示物体与原点在x方向上的距离，y表示它与原点在y方向上的距离，z表示高度。把这3个数字组合起来，就得到总距离。例如，如果x=3.0, y=3.0, z=1.0，一般可以写作(3.0, 3.0, 1.0)，表示物体与原点在x方向上的距离是3，与原点在y方向上的距离是3，高度为1。&lt;br /&gt;&lt;br /&gt;矢量可以与矢量或数字相加或相乘。在这里我们使用术语“标量”(scalar)，它是数字的数学用语—— 在C#中，就是一个double。相加的作用是很明显的。如果先移动(3.0, 3.0, 1.0)，再移动(2.0, –4.0, –4.0)，总移动量就是把这两个矢量加起来。矢量的相加是指把每个元素分别相加，因此得到(5.0, –1.0,–3.0)。此时，数学表达式总是写成c=a+b，其中a和b是矢量，c是结果矢量。这与使用Vector结构的方式是一样的。&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;这个例子是作为一个结构来开发的，而不是类，但这并不重要。运算符重载用于结构和类时，其工作方式是一样的。&lt;br /&gt;&lt;br /&gt;下面是Vector的定义—— 包含成员字段、构造函数和一个ToString()重写方法，以便查看Vector的内容，最后是运算符重载：&lt;br /&gt;&lt;br /&gt;namespace Wrox.ProCSharp.OOCSharp&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   struct Vector&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      public double x, y, z;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      public Vector(double x, double y, double z)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         this.x = x;&lt;br /&gt;&lt;br /&gt;         this.y = y;&lt;br /&gt;&lt;br /&gt;         this.z = z;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      public Vector(Vector rhs)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         x = rhs.x;&lt;br /&gt;&lt;br /&gt;         y = rhs.y;&lt;br /&gt;&lt;br /&gt;         z = rhs.z;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      public override string ToString()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         return "( " + x + " , " + y + " , " + z + " )"; &lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;这里提供了两个构造函数，通过传递每个元素的值，或者提供另一个复制其值的Vector，来指定矢量的初始值。第二个构造函数带一个Vector参数，通常称为复制构造函数，因为它们允许通过复制另一个实例来初始化一个类或结构实例。注意，为了简单起见，把字段设置为public。也可以把它们设置为private，编写相应的属性来访问它们，这样做不会改变这个程序的功能，只是代码会复杂一些。&lt;br /&gt;&lt;br /&gt;下面是Vector结构的有趣部分—— 为+运算符提供支持的运算符重载：&lt;br /&gt;&lt;br /&gt;      public static Vector operator + (Vector lhs, Vector rhs)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         Vector result = new Vector(lhs);&lt;br /&gt;&lt;br /&gt;         result.x += rhs.x;&lt;br /&gt;&lt;br /&gt;         result.y += rhs.y;&lt;br /&gt;&lt;br /&gt;         result.z += rhs.z;&lt;br /&gt;&lt;br /&gt;         return result;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;运算符声明的方式与方法声明的方式相同，但operator关键字告诉编译器，它实际上是一个运算符重载，后面是相关运算符的符号，在本例中就是+。返回类型是在使用这个运算符时获得的类型。在本例中，把两个矢量加起来会得到另一个矢量，所以返回类型就是Vector。对于这个+运算符重载，返回类型与包含类一样，但这种情况并不是必需的。两个参数就是要操作的对象。对于二元运算符(带两个参数)，如+和－运算符，第一个参数是放在运算符左边的对象或值，第二个参数是放在运算符右边的对象或值。&lt;br /&gt;&lt;br /&gt;C#要求所有的运算符都声明为public和static，这表示它们与它们的类或结构相关联，而不是与对象相关联，所以运算符重载的代码体不能访问非静态类成员，也不能访问this指针；这是可以的，因为参数提供了运算符执行任务所需要知道的所有数据。&lt;br /&gt;&lt;br /&gt;前面介绍了声明运算符+的语法，下面看看运算符内部的情况：&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         Vector result = new Vector(lhs);&lt;br /&gt;&lt;br /&gt;         result.x += rhs.x;&lt;br /&gt;&lt;br /&gt;         result.y += rhs.y;&lt;br /&gt;&lt;br /&gt;         result.z += rhs.z;&lt;br /&gt;&lt;br /&gt;         return result;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;这部分代码与声明方法的代码是完全相同的，显然，它返回一个矢量，其中包含前面定义的lhs和rhs的和，即把x、y和z分别相加。&lt;br /&gt;&lt;br /&gt;下面需要编写一些简单的代码，测试Vector结构：&lt;br /&gt;&lt;br /&gt;      static void Main()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         Vector vect1, vect2, vect3;&lt;br /&gt;&lt;br /&gt;         vect1 = new Vector(3.0, 3.0, 1.0);&lt;br /&gt;&lt;br /&gt;         vect2 = new Vector(2.0,­­­–4.0,–4.0);&lt;br /&gt;&lt;br /&gt;         vect3 = vect1 + vect2;&lt;br /&gt;&lt;br /&gt;         Console.WriteLine("vect1 = " + vect1.ToString());&lt;br /&gt;&lt;br /&gt;         Console.WriteLine("vect2 = " + vect2.ToString());&lt;br /&gt;&lt;br /&gt;         Console.WriteLine("vect3 = " + vect3.ToString());&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;把这些代码保存为Vectors.cs，编译并运行它，结果如下：&lt;br /&gt;&lt;br /&gt;Vectors&lt;br /&gt;&lt;br /&gt;vect1 = ( 3 , 3 , 1 )&lt;br /&gt;&lt;br /&gt;vect2 = ( 2 ,–4 ,–4 )&lt;br /&gt;&lt;br /&gt;vect3 = ( 5 ,–1 ,–3 )&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-6305378467819091329?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/6305378467819091329/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/03/c-1.html#comment-form' title='1 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/6305378467819091329'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/6305378467819091329'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/03/c-1.html' title='c#教程（二十四） 运算符重载(1)'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-5462190260557122570</id><published>2009-03-15T17:26:00.000-07:00</published><updated>2009-03-15T17:27:36.556-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、对象'/><title type='text'>c#教程（二十三） 对象的相等比较</title><content type='html'>5.3.1  引用类型的相等比较&lt;br /&gt;System.Object的一个初看上去令人惊讶的方面是它定义了3个不同的方法，来比较对象的相等性：ReferenceEquals()和Equals()的两个版本。再加上比较运算符==，实际上有4种进行相等比较的方式。这些方法有一些微妙的区别，下面就介绍这些方法。&lt;br /&gt;&lt;br /&gt;5.3.2  ReferenceEquals()方法&lt;br /&gt;ReferenceEquals()是一个静态方法，测试两个引用是否指向类的同一个实例，特别是两个引用是否包含内存中的相同地址。作为静态方法，它不能重写，所以只能使用System.Object实现。如果提供的两个引用指向同一个对象实例，ReferenceEquals()总是返回true，否则就返回false。但是它认为null等于null：&lt;br /&gt;&lt;br /&gt;SomeClass x, y;&lt;br /&gt;&lt;br /&gt;x = new SomeClass();&lt;br /&gt;&lt;br /&gt;y = new SomeClass();&lt;br /&gt;&lt;br /&gt;bool B1 = ReferenceEquals(null, null);                //return true&lt;br /&gt;&lt;br /&gt;bool B2 = ReferenceEquals(null, x);                //return false&lt;br /&gt;&lt;br /&gt;bool B3 = ReferenceEquals(x, y);                  //return false because x and y &lt;br /&gt;&lt;br /&gt;                                            //point to different objects&lt;br /&gt;&lt;br /&gt;5.3.3  虚拟的Equals()方法&lt;br /&gt;Equals()虚拟版本的System.Object实现也比较引用。但因为这个方法是虚拟的，所以可以在自己的类中重写它，按值来比较对象。特别是如果希望类的实例用作字典中的键，就需要重写这个方法，以比较值，否则，根据重写Object.GetHashCode()的方式，包含对象的字典类要么不工作，要么工作的效率非常低。在重写Equals()方法时要注意，重写的代码不会抛出异常。这是因为如果抛出异常，字典类就会出问题，可能会包含其他在内部调用这个方法的.NET基类。&lt;br /&gt;&lt;br /&gt;5.3.4  静态的Equals()方法&lt;br /&gt;Equals()的静态版本与其虚拟实例版本的作用相同，其区别是静态版本带有两个参数，并对它们进行相等比较。这个方法可以处理两个对象中有一个是null的情况，因此，如果一个对象可能是null，这个方法就提供了禁止抛出异常的额外保护。静态重载首先要检查它传送的引用是否为null。如果它们都是null，就返回false。如果两个引用都不指向null，它就调用Equals()的虚拟实例版本。这表示在重写Equals()的实例版本时，其效果相当于也重写了静态版本。&lt;br /&gt;&lt;br /&gt;5.3.5  比较运算符＝＝&lt;br /&gt;最好将比较运算符看作是严格值比较和严格引用比较之间的中间选项。在大多数情况下，下面的代码：&lt;br /&gt;&lt;br /&gt;bool b = (x == y);          //x, y object references&lt;br /&gt;&lt;br /&gt;表示比较引用。但是，如果把一些类看作值，其含义就会比较直观。在这些情况下，最好重写比较运算符，以执行值的比较。后面将讨论运算符的重载，但显然它的一个例子是System.String类，Microsoft重写了这个运算符，比较字符串的内容，而不是它们的引用。&lt;br /&gt;&lt;br /&gt;5.3.6  值类型的相等比较&lt;br /&gt;在进行值类型的相等比较时，采用与引用类型相同的规则：ReferenceEquals()用于比较引用，Equals()用于比较值，比较运算符可以看作是一个中间项。但最大的区别是值类型需要装箱，才能把它们转换为引用，才能对它们执行方法。另外，Microsoft已经在System.ValueType类中重载了实例方法Equals()，以便对值类型进行合适的相等测试。如果调用sA.Equals(sB)，其中sA和sB是某个结构的实例，则根据sA和sB是否在其所有的字段中包含相同的值，而返回true或false。另一方面，在默认情况下，可以对自己的结构使用==的未重载版本。在表达式中使用(sA==sB)会导致一个编译错误，除非在代码中为结构提供了==的重载版本。&lt;br /&gt;&lt;br /&gt;另外，ReferenceEquals()在应用于值类型时，总是返回false，因为为了调用这个方法，值类型需要装箱到对象中。即使使用下面的代码：&lt;br /&gt;&lt;br /&gt;bool b = ReferenceEquals(v, v);          //v is a variable of some value type&lt;br /&gt;&lt;br /&gt;也会返回false，因为在转换每个参数时，v都会被单独装箱，这意味着会得到不同的引用。调用ReferenceEquals()来比较值类型实际上没有什么意义。&lt;br /&gt;&lt;br /&gt;尽管System.ValueType提供的Equals()默认重载肯定足以应付绝大多数自定义的结构，但仍可以为自己的结构重写它，以提高性能。另外，如果值类型包含作为字段的引用类型，就需要重写Equals()，以便为这些字段提供合适的语义，因为Equals()的默认重写仅比较它们的地址。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-5462190260557122570?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/5462190260557122570/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/03/c_15.html#comment-form' title='2 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/5462190260557122570'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/5462190260557122570'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/03/c_15.html' title='c#教程（二十三） 对象的相等比较'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-8260519141229711563</id><published>2009-03-12T18:12:00.000-07:00</published><updated>2009-03-12T18:13:14.152-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、类型、安全性'/><title type='text'>c#教程（二十二） 类型的安全性</title><content type='html'>5.2.1  类型转换&lt;br /&gt;我们常常需要把数据从一种类型转换为另一种类型。考虑下面的代码：&lt;br /&gt;&lt;br /&gt;byte value1 = 10;&lt;br /&gt;&lt;br /&gt;byte value2 = 23;&lt;br /&gt;&lt;br /&gt;byte total;&lt;br /&gt;&lt;br /&gt;total = value1 + value2;&lt;br /&gt;&lt;br /&gt;Console.WriteLine(total);&lt;br /&gt;&lt;br /&gt;在编译这些代码时，会产生一个错误：&lt;br /&gt;&lt;br /&gt;Cannot implicitly convert type 'int' to 'byte' (不能把int类型隐式地转换为byte类型)。&lt;br /&gt;&lt;br /&gt;问题是，我们把两个byte加在一起时，应返回int型结果，而不是另一个byte。这是因为byte包含的数据只能为8位，所以把两个byte加在一起很容易得到不能存储在一个byte中的值。如果要把结果存储在一个byte变量中，就必须把它转换回byte。有两种转换方式，隐式转换方式和显式转换方式。&lt;br /&gt;&lt;br /&gt;1. 隐式转换方式&lt;br /&gt;如果能保证值不会发生任何变化，类型转换就可以自动进行。这就是前面代码失败的原因：试图从int转换为byte，而潜在地丢失了3个字节的数据。编译器不会告诉我们该怎么做，除非我们显式地告诉它这就是我们希望的！如果在long型变量中存储结果，而不是byte型变量中，就不会有问题了：&lt;br /&gt;&lt;br /&gt;byte value1 = 10;&lt;br /&gt;&lt;br /&gt;byte value2 = 23;&lt;br /&gt;&lt;br /&gt;long total;               // this will compile fine&lt;br /&gt;&lt;br /&gt;total = value1 + value2;&lt;br /&gt;&lt;br /&gt;Console.WriteLine(total);&lt;br /&gt;&lt;br /&gt;这是因为long型变量包含的数据字节比int型多，所以数据没有丢失的危险。在这些情况下，编译器会很顺利地进行转换，我们也不需要显式提出要求。&lt;br /&gt;&lt;br /&gt;表5-4介绍了C#支持的隐式类型转换。&lt;br /&gt;&lt;br /&gt;表  5-4&lt;br /&gt;&lt;br /&gt;源  类  型&lt;br /&gt; 目 的 类 型&lt;br /&gt; &lt;br /&gt;sbyte&lt;br /&gt; short、int、long、float、double、decimal&lt;br /&gt; &lt;br /&gt;byte&lt;br /&gt; short、ushort、int、uint、long、ulong、float、double、decimal&lt;br /&gt; &lt;br /&gt;short&lt;br /&gt; int、long、float、double、decimal&lt;br /&gt; &lt;br /&gt;ushort&lt;br /&gt; int、uint、long、ulong、float、double、decimal&lt;br /&gt; &lt;br /&gt;int&lt;br /&gt; long、float、double、decimal&lt;br /&gt; &lt;br /&gt;uint&lt;br /&gt; long、ulong、float、double、decimal&lt;br /&gt; &lt;br /&gt;long、ulong&lt;br /&gt; float、double、decimal&lt;br /&gt; &lt;br /&gt;float&lt;br /&gt; double&lt;br /&gt; &lt;br /&gt;char&lt;br /&gt; ushort、int、uint、long、ulong、float、double、decimal&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;注意，只能从较小的整数类型隐式地转换为较大的整数类型，不能从较大的整数类型隐式地转换为较小的整数类型。也可以在整数和浮点数之间转换，其规则略有不同，可以在相同大小的类型之间转换，例如int/uint转换为 float，long/ulong转换为double，也可以从long/ulong转换回float。这样做可能会丢失4个字节的数据，但这仅表示得到的float值比使用double得到的值精度低，编译器认为这是一种可以接受的错误，而其值的大小是不会受到影响的。&lt;br /&gt;&lt;br /&gt;无符号的变量可以转换为有符号的变量，只要无符号的变量值的大小在有符号的变量的范围之内即可。&lt;br /&gt;&lt;br /&gt;2. 显式转换方式&lt;br /&gt;有许多场合不能隐式地转换类型，否则编译器会报告错误。下面是不能进行隐式转换的一些场合：&lt;br /&gt;&lt;br /&gt;●   int转换为short—— 会丢失数据&lt;br /&gt;&lt;br /&gt;●   int转换为uint—— 会丢失数据&lt;br /&gt;&lt;br /&gt;●   uint转换为int—— 会丢失数据&lt;br /&gt;&lt;br /&gt;●   float转换为int—— 会丢失小数点后面的所有数据&lt;br /&gt;&lt;br /&gt;●   任何数字类型转换为char—— 会丢失数据&lt;br /&gt;&lt;br /&gt;●   decimal转换为任何数字类型—— 因为decimal 类型的内部结构不同于整数和浮点数&lt;br /&gt;&lt;br /&gt;但是，可以使用cast显示执行这些转换。在把一种类型转换为另一种类型时，要迫使编译器进行转换。类型转换的一般语法如下：&lt;br /&gt;&lt;br /&gt;long val = 30000;&lt;br /&gt;&lt;br /&gt;int i = (int)val;   // A valid cast. The maximum int is 2147483647&lt;br /&gt;&lt;br /&gt;这表示，把转换的目标类型名放在要转换的值之前的圆括号中。对于熟悉C的程序员来说，这是数据类型转换的典型语法。对于熟悉C++特殊数据类型转换关键字(如static_cast)的程序员来说，这些关键字是C#中不存在，必须使用旧的C风格的语法。&lt;br /&gt;&lt;br /&gt;这种类型转换是一种比较危险的操作，即使在从long转换为int这样简单的转换过程中，如果原来long的值比int的最大值还大，就会出问题：&lt;br /&gt;&lt;br /&gt;long val = 3000000000;&lt;br /&gt;&lt;br /&gt;int i = (int)val;         // An invalid cast. The maximum int is 2147483647&lt;br /&gt;&lt;br /&gt;在本例中，不会报告错误，也得不到期望的结果。如果运行上面的代码，结果存储在i中，则其值为：&lt;br /&gt;&lt;br /&gt;–1294967296&lt;br /&gt;&lt;br /&gt;最好假定显式数据转换不会给出希望的结果。如前所述，C#提供了一个checked运算符，使用它可以测试操作是否会产生算术溢出。使用这个运算符可以检查数据类型转换是否安全，如果不安全，就会让运行时抛出一个溢出异常：&lt;br /&gt;&lt;br /&gt;long val = 3000000000;&lt;br /&gt;&lt;br /&gt;int i = checked ((int)val);&lt;br /&gt;&lt;br /&gt;记住，所有的显式数据类型转换都可能不安全，在应用程序中应包含这样的代码，处理可能失败的数据类型转换。第11章将使用try和 catch语句引入结构化异常处理。&lt;br /&gt;&lt;br /&gt;使用数据类型转换可以把大多数数据从一种基本类型转换为另一种基本类型。例如：&lt;br /&gt;&lt;br /&gt;double price = 25.30;&lt;br /&gt;&lt;br /&gt;int approximatePrice = (int)(price + 0.5);&lt;br /&gt;&lt;br /&gt;这么做的代价是把价格四舍五入为最接近的美元数。但在这个转换过程中，小数点后面的所有数据都会丢失。因此，如果要使用这个修改过的价格进行更多的计算，最好不要使用这种转换；如果要输出全部计算完或部分计算完的近似值，且如果不希望用小数点后面的数据去麻烦用户，这种转换是很好的。&lt;br /&gt;&lt;br /&gt;这个例子说明了把一个无符号的整数转换为char型时，会发生的情况：&lt;br /&gt;&lt;br /&gt;ushort c = 43;&lt;br /&gt;&lt;br /&gt;char symbol = (char)c;&lt;br /&gt;&lt;br /&gt;Console.WriteLine(symbol);&lt;br /&gt;&lt;br /&gt;结果是ASCII数为43的字符，即+号。可以尝试数字类型之间的任何转换(包括char)，这种转换是成功的，例如把decimal转换为char，或把char转换为decimal。&lt;br /&gt;&lt;br /&gt;值类型之间的转换并不仅限于孤立的变量。还可以把类型为double的数组元素转换为类型为int的结构成员变量：&lt;br /&gt;&lt;br /&gt;struct ItemDetails &lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   public string Description;&lt;br /&gt;&lt;br /&gt;   public int ApproxPrice;&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;//...&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;double[] Prices = { 25.30, 26.20, 27.40, 30.00 };&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;ItemDetails id;&lt;br /&gt;&lt;br /&gt;id.Description = "Whatever";&lt;br /&gt;&lt;br /&gt;id.ApproxPrice = (int)(Prices[0] + 0.5);&lt;br /&gt;&lt;br /&gt;使用显式的数据类型转换方式，并小心使用这种技术，就可以把简单的值类型的任何实例转换为另一种类型。但在进行显式的类型转换时有一些限制，例如值类型，只能在数字、char类型和enum类型之间转换。不能直接把Boolean数据类型转换为其他类型，也不能把其他类型转换为Boolean数据类型。&lt;br /&gt;&lt;br /&gt;如果需要在数字和字符串之间转换，.NET类库提供了一些方法。Object类有一个ToString()方法，该方法在所有的.NET预定义类型中都进行了重写，返回对象的字符串表示：&lt;br /&gt;&lt;br /&gt;int i = 10;&lt;br /&gt;&lt;br /&gt;string s = i.ToString();&lt;br /&gt;&lt;br /&gt;同样，如果需要分析一个字符串，获得一个数字或Boolean值，就可以使用所有预定义值类型都支持的Parse方法：&lt;br /&gt;&lt;br /&gt;string s = "100";&lt;br /&gt;&lt;br /&gt;int i = int.Parse(s);&lt;br /&gt;&lt;br /&gt;Console.WriteLine(i + 50);   // Add 50 to prove it is really an int&lt;br /&gt;&lt;br /&gt;注意，如果不能转换字符串(例如要把字符串Hello转换为一个整数)，Parse方法就会注册一个错误，抛出一个异常。第11章将介绍异常。&lt;br /&gt;&lt;br /&gt;5.2.2  装箱和取消装箱&lt;br /&gt;第2章介绍了所有类型，包括简单的预定义类型，例如int和char，以及复合类型，例如从Object类型中派生的类和结构。下面可以像处理对象那样处理字面值：&lt;br /&gt;&lt;br /&gt;string s = 10.ToString();&lt;br /&gt;&lt;br /&gt;但是，C#数据类型可以分为在堆栈上分配内存的值类型和在堆上分配内存的引用类型。如果int不过是堆栈上一个4字节的值，该如何在它上面调用方法？&lt;br /&gt;&lt;br /&gt;C#的实现方式是通过一个有点魔术性的方式，即装箱(boxing)。装箱和取消装箱(unboxing)可以把值类型转换为引用类型，或把引用类型转换为值类型。这已经在数据类型转换一节中介绍过了，即把值转换为object类型。装箱用于描述把一个值类型转换为引用类型。运行库会为堆上的对象创建一个临时的引用类型“box”。&lt;br /&gt;&lt;br /&gt;该转换是隐式进行的，如上面的例子所述。还可以手工进行转换：&lt;br /&gt;&lt;br /&gt;int i = 20;&lt;br /&gt;&lt;br /&gt;object o = i;&lt;br /&gt;&lt;br /&gt;取消装箱用于把引用类型转换为值类型，其中引用类型的值转换为值类型。这里使用术语"“cast”，是因为这种数据类型转换是显式进行的。其语法类似于前面的显式类型转换：&lt;br /&gt;&lt;br /&gt;int i = 20;&lt;br /&gt;&lt;br /&gt;object o = i;     // Box the int&lt;br /&gt;&lt;br /&gt;int j = (int)o;    // Unbox it back into an int&lt;br /&gt;&lt;br /&gt;只能把以前装箱的变量再转换为值类型。当o不是装箱后的int型时，如果执行最后一行，就会抛出一个异常。&lt;br /&gt;&lt;br /&gt;这里有一个警告。在把引用类型转换为值类型时，必须非常小心，确保得到的值变量有足够的空间存储值类型的值中的所有字节。例如，C#的int只有32位，所以把long值类型(64位)转换为int时，会产生一个InvalidCastException异常：&lt;br /&gt;&lt;br /&gt;long a = 333333423;&lt;br /&gt;&lt;br /&gt;object b = (object)a;&lt;br /&gt;&lt;br /&gt;int c = (int)b;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-8260519141229711563?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/8260519141229711563/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/03/c_12.html#comment-form' title='1 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/8260519141229711563'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/8260519141229711563'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/03/c_12.html' title='c#教程（二十二） 类型的安全性'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-1096785078592224745</id><published>2009-03-11T17:27:00.000-07:00</published><updated>2009-03-11T17:29:41.153-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、运算符'/><title type='text'>c#教程（二十一） 运算符(2)</title><content type='html'>5.1.4  is运算符&lt;br /&gt;is运算符可以检查对象是否与特定的类型兼容。例如，要检查变量是否与object类型兼容：&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;“兼容”是指对象是该类型，或者派生于该类型。&lt;br /&gt;&lt;br /&gt;int i = 10;&lt;br /&gt;&lt;br /&gt;if (i is object)&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("i is an object");&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;int和从object继承而来的其他C#数据类型一样，表达式i is object将得到true，并显示信息。&lt;br /&gt;&lt;br /&gt;5.1.5  as运算符&lt;br /&gt;as运算符用于执行引用类型的显式类型转换。如果要转换的类型与指定的类型兼容，转换就会成功进行；如果类型不兼容，as运算符就会返回值null。如下面的代码所示，如果object引用不指向string实例，把object引用转换为string就会返回null：&lt;br /&gt;&lt;br /&gt;object o1 = "Some String";&lt;br /&gt;&lt;br /&gt;object o2 = 5;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;string s1 = o1 as string;      //s1 = "Some String"&lt;br /&gt;&lt;br /&gt;string s2 = o2 as string;     //s1 = null&lt;br /&gt;&lt;br /&gt;as运算符允许在一步中进行安全的类型转换，不需要先使用is运算符测试类型，再执行   转换。&lt;br /&gt;&lt;br /&gt;5.1.6  sizeof运算符&lt;br /&gt;使用sizeof运算符可以确定堆栈中值类型需要的长度(单位是字节)：&lt;br /&gt;&lt;br /&gt;string s = "A string";&lt;br /&gt;&lt;br /&gt;unsafe&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   Console.WriteLine(sizeof(int));&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;其结果是显示数字4，因为int有4个字节。&lt;br /&gt;&lt;br /&gt;注意，只能在不安全代码中使用sizeof运算符。第7章将详细论述不安全代码。&lt;br /&gt;&lt;br /&gt;5.1.7  typeof运算符&lt;br /&gt;Typeof运算符返回一个表示特定类型的Type对象。例如，typeof(string)返回表示System.String类型的Type对象。在使用反射动态查找对象的信息时，这个运算符是很有效的。第10章将介绍反射。&lt;br /&gt;&lt;br /&gt;5.1.8  运算符的优先级&lt;br /&gt;表5-3显示了&lt;a href="http://futureangle.blogspot.com/2009/03/c1_10.html"&gt;C#&lt;/a&gt;运算符的优先级。表顶部的运算符有最高的优先级(即在包含多个运算符的表达式中，最先计算该运算符)：&lt;br /&gt;&lt;br /&gt;表  5-3&lt;br /&gt;&lt;br /&gt;组&lt;br /&gt; 运  算  符&lt;br /&gt; &lt;br /&gt;初级运算符&lt;br /&gt; () . [] x++ x––  new  typeof  sizeof  checked  unchecked&lt;br /&gt; &lt;br /&gt;一元运算符&lt;br /&gt; +  – !  ~  ++x  ––x和数据类型转换&lt;br /&gt; &lt;br /&gt;乘/除运算符&lt;br /&gt; *  /  %&lt;br /&gt; &lt;br /&gt;加/减运算符&lt;br /&gt; +  –&lt;br /&gt; &lt;br /&gt;移位运算符&lt;br /&gt; &lt;&lt;  &gt;&gt;&lt;br /&gt; &lt;br /&gt;关系运算符&lt;br /&gt; &lt;  &gt;  &lt;=  &gt;=  is as&lt;br /&gt; &lt;br /&gt;比较运算符&lt;br /&gt; = =  !=&lt;br /&gt; &lt;br /&gt;按位AND运算符&lt;br /&gt; &amp;&lt;br /&gt; &lt;br /&gt;按位XOR运算符&lt;br /&gt; |&lt;br /&gt; &lt;br /&gt;按位OR运算符&lt;br /&gt; ^&lt;br /&gt; &lt;br /&gt;布尔 AND运算符&lt;br /&gt; &amp;&amp;&lt;br /&gt; &lt;br /&gt;布尔OR运算符&lt;br /&gt; ||&lt;br /&gt; &lt;br /&gt;三元运算符&lt;br /&gt; ?:&lt;br /&gt; &lt;br /&gt;赋值运算符&lt;br /&gt; =  += –=  *=  /=  %=  &amp;=  |=  ^=  &lt;&lt;=  &gt;&gt;=  &gt;&gt;&gt;=&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;在复杂的表达式中，应避免利用运算符优先级来生成正确的结果。使用括号指定运算符的执行顺序，可以使代码更整洁，避免出现潜在的冲突。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-1096785078592224745?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/1096785078592224745/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/03/c-2.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/1096785078592224745'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/1096785078592224745'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/03/c-2.html' title='c#教程（二十一） 运算符(2)'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-943996066102099840</id><published>2009-03-10T18:06:00.000-07:00</published><updated>2009-03-10T18:09:06.981-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、运算符'/><title type='text'>c#教程（二十）运算符(1)</title><content type='html'>C和C++开发人员应很熟悉大多数C#运算符，这里为新程序员和VB开发人员介绍最重要的运算符，并介绍C#中的一些新变化。&lt;br /&gt;&lt;br /&gt;C#支持表5-1所示的运算符，其中有4个运算符(sizeof、*、–&gt;、&amp;)只能用于不安全的代码(这些代码绕过了C#类型安全性的检查)，这些不安全的代码见第7章的讨论。&lt;br /&gt;&lt;br /&gt;表  5-1&lt;br /&gt;&lt;br /&gt;类    别&lt;br /&gt; 运  算  符&lt;br /&gt; &lt;br /&gt;算术运算符&lt;br /&gt; + – * / %&lt;br /&gt; &lt;br /&gt;逻辑运算符&lt;br /&gt; &amp;  |  ^  ~  &amp;&amp;  ||  !&lt;br /&gt; &lt;br /&gt;字符串连接运算符&lt;br /&gt; +&lt;br /&gt; &lt;br /&gt;增量和减量运算符&lt;br /&gt; ++  – –&lt;br /&gt; &lt;br /&gt;移位运算符&lt;br /&gt; &lt;&lt;  &gt;&gt;&lt;br /&gt; &lt;br /&gt;比较运算符&lt;br /&gt; ==  !=  &lt; &gt;  &lt;=  &gt;=&lt;br /&gt; &lt;br /&gt;赋值运算符&lt;br /&gt; =  += –=  *=  /=  %=  &amp;=  |=  ^=  &lt;&lt;=  &gt;&gt;=&lt;br /&gt; &lt;br /&gt;成员访问运算符(用于对象和结构)&lt;br /&gt; .&lt;br /&gt; &lt;br /&gt;索引运算符(用于数组和索引器)&lt;br /&gt; []&lt;br /&gt; &lt;br /&gt;数据类型转换运算符&lt;br /&gt; ()&lt;br /&gt; &lt;br /&gt;条件运算符 (三元运算符)&lt;br /&gt; ?:&lt;br /&gt; &lt;br /&gt;对象创建运算符&lt;br /&gt; new&lt;br /&gt; &lt;br /&gt;类型信息运算符&lt;br /&gt; sizeof (只用于不安全的代码) is typeof as&lt;br /&gt; &lt;br /&gt;溢出异常控制运算符&lt;br /&gt; checked unchecked&lt;br /&gt; &lt;br /&gt;间接寻址运算符&lt;br /&gt; *  –&gt;  &amp; (只用于不安全代码) []&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;使用C#运算符的一个最大缺点是，与C风格的语言一样，赋值(=)和比较(==)运算使用不同的运算符。下述语句表示“x等于3”：&lt;br /&gt;&lt;br /&gt;x = 3;&lt;br /&gt;&lt;br /&gt;如果要比较x和另一个值，就需要使用两个等号(==)：&lt;br /&gt;&lt;br /&gt;if (x == 3)&lt;br /&gt;&lt;br /&gt;C#非常严格的类型安全规则防止出现常见的&lt;a href="http://futureangle.blogspot.com"&gt;C#&lt;/a&gt;错误，也就是在逻辑语句中使用赋值运算符代替比较运算符。在C#中，下述语句会产生一个编译错误：&lt;br /&gt;&lt;br /&gt;if (x = 3)&lt;br /&gt;&lt;br /&gt;习惯使用宏字符&amp;来连接字符串的VB程序员必须改变这个习惯。在C#中，使用加号+连接字符串，而&amp;表示两个不同整数值的按位AND运算。| 则在两个整数之间执行按位OR运算。VB程序员可能还没有使用过％(取模)运算符，它返回除运算的余数，例如，如果x等于7，则x％ 5会返回2。&lt;br /&gt;&lt;br /&gt;在C#中很少会用到指针，因此也很少会用到间接寻址运算符(–&gt;)。使用它们的惟一场合是在不安全代码块中，因为只有在此C#才允许使用指针。&lt;br /&gt;&lt;br /&gt;5.1.1  运算符的简化操作&lt;br /&gt;表5-2列出了C#中的全部简化赋值运算符。&lt;br /&gt;&lt;br /&gt;表  5-2&lt;br /&gt;&lt;br /&gt;运算符的简化操作&lt;br /&gt; 等  价  于&lt;br /&gt; &lt;br /&gt;x++, ++x&lt;br /&gt; x = x + 1&lt;br /&gt; &lt;br /&gt;x–,–x&lt;br /&gt; x = x – 1&lt;br /&gt; &lt;br /&gt;x+= y&lt;br /&gt; x = x + y&lt;br /&gt; &lt;br /&gt;x–= y&lt;br /&gt; x = x – y&lt;br /&gt; &lt;br /&gt;x *= y&lt;br /&gt; x = x * y&lt;br /&gt; &lt;br /&gt;x /= y&lt;br /&gt; x = x / y&lt;br /&gt; &lt;br /&gt;x %= y&lt;br /&gt; x = x % y&lt;br /&gt; &lt;br /&gt;x &gt;&gt;= y&lt;br /&gt; x = x &gt;&gt; y&lt;br /&gt; &lt;br /&gt;x &lt;&lt;= y&lt;br /&gt; x = x &lt;&lt; y&lt;br /&gt; &lt;br /&gt;x &amp;= y&lt;br /&gt; x = x &amp; y&lt;br /&gt; &lt;br /&gt;x |= y&lt;br /&gt; x = x | y&lt;br /&gt; &lt;br /&gt;x ^= y&lt;br /&gt; x = x ^ y&lt;br /&gt; &lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;为什么有两个例子来说明++增量和– –减量运算符。把运算符放在表达式的前面称为前置，把运算符放在表达式的后面称为后置。表达式x++和++x都等于x = x +1，但它们的执行方式有所不同。&lt;br /&gt;&lt;br /&gt;增量或减量运算符可以作用于整个表达式，也可以作用于表达式的内部。当x++和++x单独占一行时，它们的作用是相同的，对应于语句x = x + 1。但当它们用于表达式内部时，把运算符放在前面(++x)会在计算表达式之前递增x，换言之，递增了x后，在表达式中使用新值进行计算。而把运算符放在后面(x++)会在计算表达式之后递增x—— 使用原来的值计算表达式。下面的例子说明了它们的区别：&lt;br /&gt;&lt;br /&gt;int x = 5;&lt;br /&gt;&lt;br /&gt;if (++x == 6)&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("This will execute");&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;if (x++ == 7)&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("This won't");&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;第一个if条件得到true，因为在计算表达式之前，x从5递增为6。第二个if语句中的条件为false，因为在计算完整个表达式后，x才递增为7。&lt;br /&gt;&lt;br /&gt;前置运算符––x和后置运算符x––与此类似，但它们是递减，而不是递增。&lt;br /&gt;&lt;br /&gt;其他简化运算符，如+= 和–=需要两个操作数，用于执行算术、逻辑和按位运算，改变第一个操作数的值。例如，下面两行代码是等价的：&lt;br /&gt;&lt;br /&gt;x += 5;&lt;br /&gt;&lt;br /&gt;x = x + 5;&lt;br /&gt;&lt;br /&gt;5.1.2  三元运算符&lt;br /&gt;三元运算符?:是if...else结构的简化形式。其名称的出处是它带有三个操作数。它可以计算一个条件，如果条件为真，就返回一个值；如果条件为假，则返回另一个值。其语法如下：&lt;br /&gt;&lt;br /&gt;condition ? true_value : false_value&lt;br /&gt;&lt;br /&gt;其中condition是要计算的Boolean型表达式，true_value是condition为true时返回的值，false_value是condition为false时返回的值。&lt;br /&gt;&lt;br /&gt;恰当地使用三元运算符，可以使程序非常简洁。它特别适合于给被调用的函数提供两个参数中的一个。使用它可以把Boolean值转换为字符串值true或false。它也很适合于显示正确的单数形式或复数形式，例如：&lt;br /&gt;&lt;br /&gt;int x = 1;&lt;br /&gt;&lt;br /&gt;string s = x.ToString() + " ";&lt;br /&gt;&lt;br /&gt;s += (x == 1 ? "man" : "men");&lt;br /&gt;&lt;br /&gt;Console.WriteLine(s);&lt;br /&gt;&lt;br /&gt;如果x等于1，这段代码就显示 man，如果x等于其他数，就显示其正确的复数形式。但要注意，如果结果需要用在不同的语言中，就必须编写更复杂的例程，以考虑到不同语言的不同语法。&lt;br /&gt;&lt;br /&gt;5.1.3  checked和 unchecked运算符&lt;br /&gt;考虑下面的代码：&lt;br /&gt;&lt;br /&gt;byte b = 255;&lt;br /&gt;&lt;br /&gt;b++;&lt;br /&gt;&lt;br /&gt;Console.WriteLine(b.ToString());&lt;br /&gt;&lt;br /&gt;byte数据类型只能包含0~255的数，所以b值的增量会导致溢出。CLR如何处理这个溢出取决于许多方面，包括编译器选项，所以无论溢出有什么样的风险，都需要用某种方式确保得到我们希望的结果。&lt;br /&gt;&lt;br /&gt;为此，C#提供了checked和 unchecked运算符。如果把一个代码块标记为checked，CLR就会执行溢出检查，如果发生溢出，就抛出异常。如果改变代码，使之包含checked运算符：&lt;br /&gt;&lt;br /&gt;byte b = 255;&lt;br /&gt;&lt;br /&gt;checked&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   b++;&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;Console.WriteLine(b.ToString());&lt;br /&gt;&lt;br /&gt;运行这段代码，就会得到一个错误信息：&lt;br /&gt;&lt;br /&gt;Unhandled Exception: System.OverflowException: Arithmetic operation resulted in an overflow.&lt;br /&gt;&lt;br /&gt;   at Wrox.ProCSharp.Basics.OverflowTest.Main(String[] args)&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;用/checked选项进行编译，就可以检查程序中所有未标记代码中的溢出。&lt;br /&gt;&lt;br /&gt;如果要禁止溢出检查，可以把代码标记为unchecked：&lt;br /&gt;&lt;br /&gt;byte b = 255;&lt;br /&gt;&lt;br /&gt;unchecked&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   b++;&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;Console.WriteLine(b.ToString());&lt;br /&gt;&lt;br /&gt;在本例中，不会抛出异常，但会丢失数据——因为byte数据类型不能包含256，溢出的位会被丢掉，所以b变量得到的值是0。&lt;br /&gt;&lt;br /&gt;注意，unchecked是默认值。只有在需要把几个未检查的代码行放在一个明确标记为checked的大代码块中，才需要显式使用unchecked关键字。&lt;br /&gt;&lt;br /&gt;5.1.4  is运算符&lt;br /&gt;is运算符可以检查对象是否与特定的类型兼容。例如，要检查变量是否与object类型兼容：&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;“兼容”是指对象是该类型，或者派生于该类型。&lt;br /&gt;&lt;br /&gt;int i = 10;&lt;br /&gt;&lt;br /&gt;if (i is object)&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("i is an object");&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;int和从object继承而来的其他C#数据类型一样，表达式i is object将得到true，并显示信息。&lt;br /&gt;&lt;br /&gt;5.1.5  as运算符&lt;br /&gt;as运算符用于执行引用类型的显式类型转换。如果要转换的类型与指定的类型兼容，转换就会成功进行；如果类型不兼容，as运算符就会返回值null。如下面的代码所示，如果object引用不指向string实例，把object引用转换为string就会返回null：&lt;br /&gt;&lt;br /&gt;object o1 = "Some String";&lt;br /&gt;&lt;br /&gt;object o2 = 5;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;string s1 = o1 as string;      //s1 = "Some String"&lt;br /&gt;&lt;br /&gt;string s2 = o2 as string;     //s1 = null&lt;br /&gt;&lt;br /&gt;as运算符允许在一步中进行安全的类型转换，不需要先使用is运算符测试类型，再执行   转换。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-943996066102099840?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/943996066102099840/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/03/c1_10.html#comment-form' title='2 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/943996066102099840'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/943996066102099840'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/03/c1_10.html' title='c#教程（二十）运算符(1)'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-7368632708099563139</id><published>2009-03-09T17:44:00.000-07:00</published><updated>2009-03-09T17:45:31.618-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、接口'/><title type='text'>c#教程（十九）接口</title><content type='html'>熟悉COM的开发人员应注意，尽管在概念上C#接口类似于COM接口，但它们是不同的，底层的结构不同，例如，C#接口并不派生于IUnknown。C#接口根据.NET函数提供了一个契约。与COM接口不同，C#接口不代表任何类型的二进制标准。&lt;br /&gt;&lt;br /&gt;下面列出Microsoft预定义的一个接口的完整定义，System.IDisposable。IDisposable包含一个方法Dispose()，该方法由类执行，用于清理代码：&lt;br /&gt;&lt;br /&gt;public interface IDisposable &lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   void Dispose();&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;上面的代码说明，声明接口在语法上与声明抽象类完全相同，但不允许提供接口中任何成员的执行方式。一般情况下，接口中只能包含方法、属性、索引器和事件的声明。&lt;br /&gt;&lt;br /&gt;不能实例化接口，它只能包含其成员的签名。接口不能有构造函数(如何构建不能实例化的对象？)或字段(因为这隐含了某些内部的执行方式)。接口定义也不允许包含运算符重载，但这不是因为声明它们在原则上有什么问题，而是因为接口通常是公共契约，包含运算符重载会引起一些与其他.NET语言不兼容的问题，例如与VB.NET的不兼容，因为VB.NET不支持运算符重载。&lt;br /&gt;&lt;br /&gt;在接口定义中还不允许声明成员上的修饰符。接口成员总是公共的，不能声明为虚拟或静态。如果需要，就应由执行的类来声明，因此最好通过执行的类来声明访问修饰符，就像上面的代码那样。&lt;br /&gt;&lt;br /&gt;例如IDisposable。如果类希望声明为公共类型，以便执行方法Dispose()，该类就必须执行IDisposable。在C#中，这表示该类派生于IDisposable。&lt;br /&gt;&lt;br /&gt;class SomeClass : IDisposable&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   // this class MUST contain an implementation of the &lt;br /&gt;&lt;br /&gt;   // IDisposable.Dispose() method, otherwise&lt;br /&gt;&lt;br /&gt;   // you get a compilation error&lt;br /&gt;&lt;br /&gt;   public void Dispose()&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      // implementation of Dispose() method&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;   // rest of class&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;在这个例子中，如果SomeClass派生于IDisposable，但不包含与IDisposable中签名相同的Dispose()实现，就会得到一个编译错误，因为该类破坏了实现IDisposable的契约。当然，编译器允许类有一个不派生于IDisposable的Dispose()方法。问题是其他代码无法识别出SomeClass支持IDisposable特性。&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;IDisposable是一个相当简单的接口，它只定义了一个方法。大多数接口都包含许多成员。&lt;br /&gt;&lt;br /&gt;接口的另一个例子是C#中的foreach循环。实际上，foreach循环的内部工作方式是查询对象，看看它是否实现System.Collections.IEnumerable接口。如果是，C#编译器就插入IL代码，使用这个接口上的方法迭代集合中的成员，否则，foreach就会引发一个异常。第9章将详细介绍IEnumerable接口。但应注意，IEnumerable和IDisposable在某种程度上都是有点特殊的接口，因为它们都可以由C#编译器识别，在C#编译器生成的代码中会考虑它们。显然，自己定义的接口就没有这个特权。&lt;br /&gt;&lt;br /&gt;4.4.1  定义和实现接口&lt;br /&gt;下面开发一个遵循接口继承规范的小例子来说明如何定义和使用接口。这个例子建立在银行账户的基础上。假定编写代码，最终允许在银行账户之间进行计算机转账业务。许多公司可以实现银行账户，但它们都是彼此赞同表示银行账户的所有类都实现&lt;a href="http://futureangle.blogspot.com"&gt;接口&lt;/a&gt;IBankAccount。该接口包含一个用于存取款的方法和一个返回余额的属性。这个接口还允许外部代码识别由不同银行账户执行的各种银行账户类。我们的目的是允许银行账户彼此通信，以便在账户之间进行转账业务，但还没有介绍这个功能。&lt;br /&gt;&lt;br /&gt;为了使例子简单一些，我们把例子的所有代码都放在同一个源文件中，但实际上不同的银行账户类会编译到不同的程序集中，而这些程序集位于不同银行所属的不同机器上。第16章在讨论远程通信时，将介绍位于不同机器上的.NET程序集如何通信。但那些内容对于这里的例子来说过于复杂了。但是为了保留一定的真实性，我们为不同的公司定义不同的命名空间。&lt;br /&gt;&lt;br /&gt;首先，需要定义IBank接口：&lt;br /&gt;&lt;br /&gt;namespace Wrox.ProCSharp&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   public interface IBankAccount&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      void PayIn(decimal amount);&lt;br /&gt;&lt;br /&gt;      bool Withdraw(decimal amount);&lt;br /&gt;&lt;br /&gt;      decimal Balance&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         get;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;注意，接口的名称为IBankAccount。接口名称传统上以字母I开头，以便知道这是一个接口。&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;如第2章所述，在大多数情况下，.NET用法规则不鼓励采用所谓的Hungarian表示法，在名称的前面加一个字母，表示对象的类型，接口是Hungarian表示法推荐采用的几种名称之一。&lt;br /&gt;&lt;br /&gt;现在可以编写表示银行账户的类了。这些类不必彼此相关，它们可以是完全不同的类。但它们都表示银行账户，因为它们都实现IbankAccount接口。&lt;br /&gt;&lt;br /&gt;下面是第一个类，一个由Royal Bank of Venus运行的存款账户：&lt;br /&gt;&lt;br /&gt;namespace Wrox.ProCSharp.VenusBank&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   public class SaverAccount : IBankAccount&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      private decimal balance;&lt;br /&gt;&lt;br /&gt;      public void PayIn(decimal amount)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         balance += amount;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;      public bool Withdraw(decimal amount)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         if (balance &gt;= amount)&lt;br /&gt;&lt;br /&gt;         {&lt;br /&gt;&lt;br /&gt;            balance -= amount;&lt;br /&gt;&lt;br /&gt;            return true;&lt;br /&gt;&lt;br /&gt;         }&lt;br /&gt;&lt;br /&gt;         Console.WriteLine("Withdrawal attempt failed.");&lt;br /&gt;&lt;br /&gt;         return false;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;      public decimal Balance&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         get&lt;br /&gt;&lt;br /&gt;         {&lt;br /&gt;&lt;br /&gt;            return balance;&lt;br /&gt;&lt;br /&gt;         }&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;      public override string ToString()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         return String.Format("Venus Bank Saver: Balance = {0,6:C}", balance);&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;这个类的实现一目了然。其中包含一个私有字段balance，当存款或取款时就调整这个字段。如果因为账户中的金额不足而取款失败，就会显示一个错误消息。还要注意，因为我们要使代码尽可能简单，所以不实现额外的属性，例如账户持有人的姓名。在现实生活中，这是最基本的信息，但对于本例来说，这是不必要的。&lt;br /&gt;&lt;br /&gt;在这段代码中，惟一有趣的是类的声明：&lt;br /&gt;&lt;br /&gt;public class SaverAccount : IBankAccount&lt;br /&gt;&lt;br /&gt;SaverAccount派生于一个接口IbankAccount，我们没有明确指出任何其他基类(当然这表示SaverAccount直接派生于System.Object)。另外，从接口中派生完全独立于从类中派生。&lt;br /&gt;&lt;br /&gt;SaverAccount派生于IbankAccount，表示它获得了IbankAccount的所有成员，但接口并不实际实现其方法，所以SaverAccount必须提供这些方法的所有实现。如果没有提供实现，编译器就会产生错误。接口仅表示其成员的存在性，类负责确定这些成员是虚拟还是抽象的(但只有在类本身是抽象的，这些成员才能是抽象的)。在本例中，接口方法不必是虚拟的。&lt;br /&gt;&lt;br /&gt;为了说明不同的类如何实现相同的接口，下面假定Planetary Bank of Jupiter还实现一个类Gold Account来表示其银行账户：&lt;br /&gt;&lt;br /&gt;namespace Wrox.ProCSharp.JupiterBank&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   public class GoldAccount : IBankAccount&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      // etc&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;这里没有列出GoldAccount类的细节，因为在本例中它基本上与SaverAccount的实现相同。GoldAccount与VenusAccount没有关系，它们只是碰巧实现相同的接口而已。&lt;br /&gt;&lt;br /&gt;有了自己的类后，就可以测试它们了。首先需要一些using语句：&lt;br /&gt;&lt;br /&gt;using System;&lt;br /&gt;&lt;br /&gt;using Wrox.ProCSharp;&lt;br /&gt;&lt;br /&gt;using Wrox.ProCSharp.VenusBank;&lt;br /&gt;&lt;br /&gt;using Wrox.ProCSharp.JupiterBank;&lt;br /&gt;&lt;br /&gt;然后需要一个Main()方法：&lt;br /&gt;&lt;br /&gt;namespace Wrox.ProCSharp&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   class MainEntryPoint&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      static void Main()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         IBankAccount venusAccount = new SaverAccount();&lt;br /&gt;&lt;br /&gt;         IBankAccount jupiterAccount = new GoldAccount();&lt;br /&gt;&lt;br /&gt;         venusAccount.PayIn(200);&lt;br /&gt;&lt;br /&gt;         venusAccount.Withdraw(100);&lt;br /&gt;&lt;br /&gt;         Console.WriteLine(venusAccount.ToString());&lt;br /&gt;&lt;br /&gt;         jupiterAccount.PayIn(500);&lt;br /&gt;&lt;br /&gt;         jupiterAccount.Withdraw(600);&lt;br /&gt;&lt;br /&gt;         jupiterAccount.Withdraw(100);&lt;br /&gt;&lt;br /&gt;         Console.WriteLine(jupiterAccount.ToString());&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;这段代码(如果下载本例子，它在BankAccounts.cs文件中)的执行结果如下：&lt;br /&gt;&lt;br /&gt;C:&gt;BankAccounts&lt;br /&gt;&lt;br /&gt;Venus Bank Saver: Balance = £100.00&lt;br /&gt;&lt;br /&gt;Withdrawal attempt failed.&lt;br /&gt;&lt;br /&gt;Jupiter Bank Saver: Balance = £400.00&lt;br /&gt;&lt;br /&gt;在这段代码中要注意的一个要点是把引用变量声明为IBankAccount引用的方式。这表示它们可以指向实现这个接口的任何类的实例。但我们只能通过这些引用调用方法(这些方法是接口的一部分)—— 如果要调用由不是接口一部分的类执行的任何方法，就需要显式地把引用强制转换为合适的类型。在这段代码中，我们调用了ToString()(不由IBankAccount实现)，但没有进行任何显式转换，这只是因为ToString()是一个System.Object方法，所以C#编译器知道任何类都支持这个方法(换言之，从接口到System.Object的数据类型转换是隐式的)。第5章将介绍强制转换的语法。&lt;br /&gt;&lt;br /&gt;接口引用完全可以看做是类引用—— 但接口引用强大之处在于，它可以引用任何实现该接口的类。例如，我们可以构造接口数组，其中的每个元素都是不同的类：&lt;br /&gt;&lt;br /&gt;IBankAccount[] accounts = new IBankAccount[2];&lt;br /&gt;&lt;br /&gt;accounts[0] = new SaverAccount();&lt;br /&gt;&lt;br /&gt;accounts[1] = new GoldAccount();&lt;br /&gt;&lt;br /&gt;Note,however,that we'd get a compiler error if we tried something like this &lt;br /&gt;&lt;br /&gt;accounts[1] = new SomeOtherClass();     // SomeOtherClass does NOT implement&lt;br /&gt;&lt;br /&gt;                                                                // IBankAccount: WRONG!!&lt;br /&gt;&lt;br /&gt;这会导致一个如下所示的编译错误：&lt;br /&gt;&lt;br /&gt;Cannot implicitly convert type 'Wrox.ProCSharp.SomeOtherClass' to&lt;br /&gt;&lt;br /&gt; 'Wrox.ProCSharp.IBankAccount'&lt;br /&gt;&lt;br /&gt;4.4.2  派生的接口&lt;br /&gt;接口可以彼此继承，其方式与类的继承相同。下面通过定义一个新接口ITransferBankAccount来说明这个概念，该接口的功能与IBankAccount相同，只是又定义了一个方法，把资金直接转到另一个账户上。&lt;br /&gt;&lt;br /&gt;namespace Wrox.ProCSharp&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   public interface ITransferBankAccount : IBankAccount&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      bool TransferTo(IBankAccount destination, decimal amount);&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;因为ITransferBankAccount派生于IBankAccount，所以拥有IBankAccount的所有成员和它自己的成员。这表示执行(派生于)ITransferBankAccount的任何类都必须执行IBankAccount的所有方法和在ITransferBankAccount中定义的新方法TransferTo()。没有执行所有这些方法就会产生一个编译错误。&lt;br /&gt;&lt;br /&gt;注意，TransferTo()方法为目标账户使用了IBankAccount接口引用。这说明了接口的用途：在执行并调用这个方法时，不必知道转帐的对象类型，只需知道该对象执行IBankAccount即可。&lt;br /&gt;&lt;br /&gt;下面演示ITransferBankAccount；假定Planetary Bank of Jupiter还提供了一个当前账户。CurrentAccount类的大多数执行代码与SaverAccount 和 GoldAccount的执行代码相同(这仅是为了使例子更简单，一般是不会这样的)，所以在下面的代码中，我们仅突出显示了不同的地方：&lt;br /&gt;&lt;br /&gt;public class CurrentAccount : ITransferBankAccount&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   private decimal balance;&lt;br /&gt;&lt;br /&gt;   public void PayIn(decimal amount)&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      balance += amount;&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;   public bool Withdraw(decimal amount)&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      if (balance &gt;= amount)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         balance -= amount;&lt;br /&gt;&lt;br /&gt;         return true;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;      Console.WriteLine("Withdrawal attempt failed.");&lt;br /&gt;&lt;br /&gt;      return false;&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;   public decimal Balance&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      get&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         return balance;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;   public bool TransferTo(IBankAccount destination, decimal amount)&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      bool result;&lt;br /&gt;&lt;br /&gt;      if ((result = Withdraw(amount)) == true)&lt;br /&gt;&lt;br /&gt;         destination.PayIn(amount);&lt;br /&gt;&lt;br /&gt;      return result;&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;   public override string ToString()&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      return String.Format("Jupiter Bank Current Account: Balance = {0,6:C}",&lt;br /&gt;&lt;br /&gt;                                                            balance);&lt;br /&gt;&lt;br /&gt;   }   &lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;可以用下面的代码验证该类：&lt;br /&gt;&lt;br /&gt;static void Main()&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   IBankAccount venusAccount = new SaverAccount();&lt;br /&gt;&lt;br /&gt;   ITransferBankAccount jupiterAccount = new CurrentAccount();&lt;br /&gt;&lt;br /&gt;   venusAccount.PayIn(200);&lt;br /&gt;&lt;br /&gt;   jupiterAccount.PayIn(500);&lt;br /&gt;&lt;br /&gt;   jupiterAccount.TransferTo(venusAccount, 100);&lt;br /&gt;&lt;br /&gt;   Console.WriteLine(venusAccount.ToString());&lt;br /&gt;&lt;br /&gt;   Console.WriteLine(jupiterAccount.ToString());&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt; 这段代码(CurrentAccount.cs)的结果如下所示，其中显示转账后正确的资金数：&lt;br /&gt;&lt;br /&gt;C:&gt;CurrentAccount&lt;br /&gt;&lt;br /&gt;Venus Bank Saver: Balance = £300.00&lt;br /&gt;&lt;br /&gt;Jupiter Bank Current Account: Balance = £400.00&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-7368632708099563139?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/7368632708099563139/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/03/c_09.html#comment-form' title='3 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/7368632708099563139'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/7368632708099563139'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/03/c_09.html' title='c#教程（十九）接口'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>3</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-6951826109573582566</id><published>2009-03-08T18:24:00.000-07:00</published><updated>2009-03-08T18:25:33.481-07:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、修饰符'/><title type='text'>c#教程（十八）修饰符</title><content type='html'>表3-1所列&lt;a href="http://futureangle.blogspot.com"&gt;修饰符&lt;/a&gt;确定了是否允许其他代码访问某个方法。&lt;br /&gt;&lt;br /&gt;表  3-1&lt;br /&gt;&lt;br /&gt;修  饰  符&lt;br /&gt; 应  用  于&lt;br /&gt; 说    明&lt;br /&gt; &lt;br /&gt;public&lt;br /&gt; 所有的类型或成员&lt;br /&gt; 任何代码均可以访问该方法&lt;br /&gt; &lt;br /&gt;protected&lt;br /&gt; 类型和内嵌类型的所有成员&lt;br /&gt; 只有派生的类型能访问该方法&lt;br /&gt; &lt;br /&gt;internal&lt;br /&gt; 类型和内嵌类型的所有成员&lt;br /&gt; 只能在包含它的程序集中访问该方法&lt;br /&gt; &lt;br /&gt;private&lt;br /&gt; 所有的类型或成员&lt;br /&gt; 只能在它所属的类型中访问该方法&lt;br /&gt; &lt;br /&gt;protected internal&lt;br /&gt; 类型和内嵌类型的所有成员&lt;br /&gt; 只能在包含它的程序集和派生类型的代码中访问该方法&lt;br /&gt; &lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;注意，类型定义可以是公共或私有的，这取决于是否希望在包含类型的程序集中访问它：&lt;br /&gt;&lt;br /&gt;public class MyClass&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;//etc.&lt;br /&gt;&lt;br /&gt;不能把类型定义为protected、internal和protected internal，因为这些修饰符对于包含在命名空间中的类型来说是没有意义的。因此这些修饰符只能应用于成员。但是，可以用这些修饰符定义嵌套的类型(即包含在其他类型中的类型)，因此在这种情况下，类型也具有成员的状态。下面的代码是合法的：&lt;br /&gt;&lt;br /&gt;public class OuterClass&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   protected class InnerClass&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      //etc.&lt;br /&gt;&lt;br /&gt;    }&lt;br /&gt;&lt;br /&gt;    //etc.&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;如果有嵌套的类型，内部的类型总是可以访问外部类型的所有成员，所以在上面的代码中，InnerClass中的代码可以访问OuterClass的所有成员，甚至可以访问OuterClass的私有成员。&lt;br /&gt;&lt;br /&gt;4.3.2  其他修饰符&lt;br /&gt;表3-2所列修饰符可以应用于类型的成员，而且有不同的用途。在应用于类型时，其中的几个修饰符也是有意义的。&lt;br /&gt;&lt;br /&gt;表3-2&lt;br /&gt;&lt;br /&gt;修  饰  符&lt;br /&gt; 应  用  于&lt;br /&gt; 说    明&lt;br /&gt; &lt;br /&gt;new&lt;br /&gt; 函数成员&lt;br /&gt; 成员用相同的签名隐藏继承的成员&lt;br /&gt; &lt;br /&gt;static&lt;br /&gt; 所有的成员&lt;br /&gt; 成员不在类的具体实例上执行&lt;br /&gt; &lt;br /&gt;virtual&lt;br /&gt; 仅类和函数成员&lt;br /&gt; 成员可以由派生类重写&lt;br /&gt; &lt;br /&gt;abstract&lt;br /&gt; 仅函数成员&lt;br /&gt; 虚拟成员定义了成员的签名，但没有提供实现&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;                                                                           (续表)    &lt;br /&gt;&lt;br /&gt;修  饰  符&lt;br /&gt; 应  用  于&lt;br /&gt; 说    明&lt;br /&gt; &lt;br /&gt;override&lt;br /&gt; 仅函数成员&lt;br /&gt; 成员重写了继承的虚拟或抽象成员&lt;br /&gt; &lt;br /&gt;sealed&lt;br /&gt; 类&lt;br /&gt; 成员重写了继承的虚拟成员，但继承该类的任何类都不能重写该成员。该修饰符必须与override一起使用&lt;br /&gt; &lt;br /&gt;extern&lt;br /&gt; 仅静态[DllImport]方法&lt;br /&gt; 成员在外部用另一种语言实现&lt;br /&gt; &lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;在这些修饰符中，internal 和 protected internal是C#和.NET Framework新增的内容。internal与public类似，但访问仅限于同一个程序集中的其他代码，换言之，代码在同一个程序中同时编译。可以使用internal确保编写的其他类都可以访问某一成员，但同时在其他公司编写的其他代码中隐藏它们。protected internal合并了protected和internal，但这是一种OR合并，而不是AND合并。protected internal成员在同一个程序集的任何代码中都可见，在派生类中也可见，甚至在其他程序集中也可见。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-6951826109573582566?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/6951826109573582566/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/03/c_08.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/6951826109573582566'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/6951826109573582566'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/03/c_08.html' title='c#教程（十八）修饰符'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-9174811663049010001</id><published>2009-03-05T21:13:00.000-08:00</published><updated>2009-03-05T21:16:27.043-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、继承'/><title type='text'>c#教程（十七）实现的继承(2)</title><content type='html'>在开始为层次结构中的类(这个类继承了其他类，也可能有定制的构造函数)定义自己的构造函数时，会发生什么情况？ &lt;br /&gt;&lt;br /&gt;假定没有为类定义任何显式的构造函数，这样编译器就会为所有的类提供默认的构造函数，在后台会进行许多操作，编译器可以很好地解决层次结构中的所有问题，每个类中的每个字段都会初始化为默认值。但在添加了一个我们自己的构造函数后，就要通过派生类的层次结构高效地控制构造过程，因此必须确保构造过程顺利进行，不要出现不能按照层次结构进行构造的问题。&lt;br /&gt;&lt;br /&gt;为什么派生类会有某些特殊的问题？原因是在创建派生类的实例时，实际上会有多个构造函数在起作用。实例化类的构造函数本身不能初始化类，还必须调用基类中的构造函数。这就是为什么要通过层次结构进行构造的原因。&lt;br /&gt;&lt;br /&gt;为了说明为什么必须调用基类的构造函数，下面是手机公司MortimerPhones开发的一个例子。这个例子包含一个抽象类GenericCustomer，它表示顾客。还有一个(非抽象)类Nevermore60 Customer，它表示采用特定付费方式(称为Nevermore60付费方式)的顾客。所有的顾客都有一个名字，由一个私有字段表示。在Nevermore60付费方式中，顾客前几分钟的电话费比较高，需要一个字段highCostMinutesUsed，它详细说明了每个顾客该如何支付这些较高的电话费。抽象类GenericCustomer的定义如下所示：&lt;br /&gt;&lt;br /&gt;abstract class GenericCustomer&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   private string name;&lt;br /&gt;&lt;br /&gt;   // lots of other methods etc.&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;class Nevermore60Customer : GenericCustomer&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   private uint highCostMinutesUsed;&lt;br /&gt;&lt;br /&gt;   // other methods etc.&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;不要担心在这些类中执行的其他方法，因为这里仅考虑构造过程。如果下载了本章的示例代码，就会发现类的定义仅包含构造函数。&lt;br /&gt;&lt;br /&gt;下面看看使用new运算符实例化Nevermore60Customer时，会发生什么情况：&lt;br /&gt;&lt;br /&gt;   GenericCustomer customer = new Nevermore60Customer();&lt;br /&gt;&lt;br /&gt;显然，成员字段name和highCostMinutesUsed都必须在实例化customer时进行初始化。如果没有提供自己的构造函数，而是仅依赖于默认的构造函数，name就会初始化为null引用，highCostMinutesUsed初始化为0。下面详细讨论其过程。&lt;br /&gt;&lt;br /&gt;HighCostMinutesUsed字段没有问题：编译器提供的默认Nevermore60Customer构造函数会把它初始化为0。&lt;br /&gt;&lt;br /&gt;那么name呢？看看类定义，显然，Nevermore60Customer构造函数不能初始化这个值。字段name声明为private，这意味着派生的类不能访问它。默认的Nevermore60Customer构造函数甚至不知道存在这个字段。惟一知道这个字段的是GenericCustomer的其他成员，即如果对name进行初始化，就必须在GenericCustomer的某个构造函数中进行。无论类层次结构有多大，这种情况都会一直延续到最终的基类System.Object上。&lt;br /&gt;&lt;br /&gt;理解了上面的问题后，就可以明白实例化派生类时会发生什么样的情况了。假定默认的构造函数在整个层次结构中使用：编译器首先找到它试图实例化的类的构造函数，在本例中是Nevermore60Customer，这个默认Nevermore60Customer构造函数首先要做的是为其直接基类GenericCustomer运行默认构造函数，然后GenericCustomer构造函数为其直接基类System.Object运行默认构造函数，System. Object没有任何基类，所以它的构造函数就执行，并把控制返回给GenericCustomer构造函数。现在执行GenericCustomer构造函数，把name初始化为null，再把控制权返回给Nevermore60Customer构造函数，接着执行这个构造函数，把highCostMinutesUsed初始化为0，并退出。此时，Nevermore60Customer实例就已经成功地构造和初始化了。&lt;br /&gt;&lt;br /&gt;构造函数的调用顺序是先调用System.Object，再按照层次结构由上向下进行，直到到达编译器要实例化的类为止。还要注意在这个过程中，每个构造函数都初始化它自己的类中的字段。这是它的一般工作方式，在开始添加自己的构造函数时，也应尽可能遵循这个规则。&lt;br /&gt;&lt;br /&gt;注意构造函数的执行顺序。基类的构造函数总是最先调用。也就是说，派生类的构造函数可以在执行过程中调用基类方法、属性和其他成员，因为基类已经构造出来的，其字段也初始化了。如果派生类不喜欢初始化基类的方式，但要访问数据，就可以改变数据的初始值，但是，好的编程方式应尽可能避免这种情况，让基类构造函数来处理其字段。&lt;br /&gt;&lt;br /&gt;理解了构造过程后，就可以开始添加自己的构造函数了。&lt;br /&gt;&lt;br /&gt;1. 在层次结构中添加无参数的构造函数&lt;br /&gt;首先讨论最简单的情况，在层次结构中用一个无参数的构造函数来替换默认的&lt;a href="http://futureangle.blogspot.com"&gt;构造函数&lt;/a&gt;后，看看会发生什么情况。假定要把每个人的名字初始化为&lt;no name&gt;，而不是null引用，修改GenericCustomer中的代码，如下所示：&lt;br /&gt;&lt;br /&gt;   public abstract class GenericCustomer&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      private string name;&lt;br /&gt;&lt;br /&gt;      public GenericCustomer()&lt;br /&gt;&lt;br /&gt;         : base()  // we could omit this line without affecting the compiled code&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         name = "&lt;no name&gt;";&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;添加这段代码后，代码运行正常。Nevermore60Customer仍有自己的默认构造函数，所以上面描述的事件顺序仍不变，但编译器会使用定制的GenericCustomer构造函数，不是默认的构造函数，所以name字段按照需要应初始化为&lt;no name&gt;。&lt;br /&gt;&lt;br /&gt;注意，在定制的构造函数中，在执行GenericCustomer构造函数前，添加了一个对基类构造函数的显式调用，使用的语法与前面解释如何获得不同的重载构造函数以互相调用时使用的语法相同。惟一的区别是，这次使用的关键字是base，而不是this，表示这是一个基类的构造函数，而不是要调用的类的构造函数。在base关键字后面的圆括号中没有参数，这是非常重要的，因为没有给基类构造函数传送参数，所以编译器会调用无参数的构造函数。其结果是编译器会插入调用System.Object构造函数的代码，这正好与默认情况相同。&lt;br /&gt;&lt;br /&gt;实际上，可以把这行代码删除，只加上为本章中大多数构造函数编写的代码：&lt;br /&gt;&lt;br /&gt;      public GenericCustomer()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         name = "&lt;no name&gt;";&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;如果编译器没有在起始花括号的前面找到对另一个构造函数的任何引用，它就会假定我们要调用基类构造函数——这符合前面介绍的默认构造函数的工作方式。&lt;br /&gt;&lt;br /&gt;base 和 this关键字是调用另一个构造函数时允许使用的惟一关键字，其他关键字都会产生编译错误。还要注意只能指定一个其他的构造函数。&lt;br /&gt;&lt;br /&gt;到目前为止，这段代码运行正常。但是，要通过构造函数的层次结构把级数弄乱的最好方法是把构造函数声明为私有函数：&lt;br /&gt;&lt;br /&gt;      private GenericCustomer()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         name = "&lt;no name&gt;";&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;如果试图这样做，就会产生一个有趣的编译错误，如果不理解构造是如何按照层次结构由上而下的顺序工作的，这个错误会让人摸不着头脑。&lt;br /&gt;&lt;br /&gt;'Wrox.ProCSharp.GenericCustomer()' is inaccessible due to its protection level&lt;br /&gt;&lt;br /&gt;有趣的是，该错误没有发生在GenericCustomer类中，而是发生在派生类Nevermore60Customer中。编译器试图为Nevermore60Customer生成默认的构造函数，但又做不到，因为默认的构造函数应调用无参数的GenericCustomer构造函数。把该构造函数声明为private，它就不可能访问派生类了。如果为带有参数的GenericCustomer提供一个构造函数，但没有提供无参数的构造函数，也会发生类似的错误。在本例中，编译器不能为GenericCustomer生成默认构造函数，所以当编译器试图为派生类生成默认构造函数时，编译器会再次发现它不能做到这一点，因为对于派生类来说，没有无参数的基类构造函数可调用。这个问题的解决方法是为派生类添加自己的构造函数—— 实际上不需要在这些构造函数中做任何工作，这样，编译器就不会为这些派生类生成默认构造函数了。&lt;br /&gt;&lt;br /&gt;前面介绍了所有的理论知识，下面用一个例子来说明如何给类的层次结构添加构造函数。下一节为MortimerPhones样例添加带参数的构造函数。&lt;br /&gt;&lt;br /&gt;2. 在层次结构中添加带有参数的构造函数&lt;br /&gt;首先是带一个参数的MortimerPhones构造函数，它仅在顾客提供其姓名时才实例化顾客：&lt;br /&gt;&lt;br /&gt;   abstract class GenericCustomer&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      private string name;&lt;br /&gt;&lt;br /&gt;      public GenericCustomer(string name)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         this.name = name;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;到目前为止，代码运行一切正常，但刚才说过，在编译器试图为派生类创建一个默认构造函数时，会产生一个编译错误，因为编译器为Nevermore60Customer生成的默认构造函数会试图调用无参数的GenericCustomer构造函数，但GenericCustomer没有这样的构造函数。因此，需要为派生类提供一个构造函数，来避免这个错误：&lt;br /&gt;&lt;br /&gt;class Nevermore60Customer : GenericCustomer&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   private uint highCostMinutesUsed;&lt;br /&gt;&lt;br /&gt;   public Nevermore60Customer(string name)&lt;br /&gt;&lt;br /&gt;      :   base(name)&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;现在，Nevermore60Customer对象的实例只能在提供了包含顾客姓名的字符串后创建，这正是我们需要的。有趣的是Nevermore60Customer构造函数对这个字符串所做的处理。它本身不能初始化name字段，因为它不能访问基类中的私有字段，但可以把顾客姓名传送给基类，以便GenericCustomer构造函数处理。具体方法是，把先执行的基类构造函数指定为把顾客姓名当做参数的构造函数。除此之外，它不需要执行任何操作。&lt;br /&gt;&lt;br /&gt;下面讨论如果要处理不同的重载构造函数和一个类的层次结构，会发生什么情况。假定Nevermore60Customers通过朋友联系到MortimerPhones，即MortimerPhones公司中有一个人是朋友，因此可以获得折扣。这表示在构造一个Nevermore60Customer时，还需要传递联系人的姓名。在现实生活中，构造函数必须利用该姓名去完成更复杂的工作，例如处理折扣等，但这里只是把联系人的姓名存储到另一个字段中。&lt;br /&gt;&lt;br /&gt;此时，Nevermore60Customer定义如下所示：&lt;br /&gt;&lt;br /&gt;   class Nevermore60Customer : GenericCustomer&lt;br /&gt;&lt;br /&gt;   { &lt;br /&gt;&lt;br /&gt;      public Nevermore60Customer(string name, string referrerName)&lt;br /&gt;&lt;br /&gt;         : base(name) &lt;br /&gt;&lt;br /&gt;      { &lt;br /&gt;&lt;br /&gt;         this.referrerName = referrerName;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;      &lt;br /&gt;&lt;br /&gt;      private string referrerName;&lt;br /&gt;&lt;br /&gt;      private uint highCostMinutesUsed;&lt;br /&gt;&lt;br /&gt;该构造函数将姓名作为参数，并把它传递给GenericCustomer构造函数进行处理。ReferrerName是一个变量，我们需要声明它，这样构造函数才能在其主体中处理这个参数。&lt;br /&gt;&lt;br /&gt;但是，并不是所有的Nevermore60Customers都有联系人，所以还需要有一个不需此参数的构造函数(或者说是为它提供默认值的构造函数)。实际上，我们指定如果没有联系人，ReferrerName字段就设置为&lt;None&gt;。下面是这个带有一个参数的构造函数：&lt;br /&gt;&lt;br /&gt;      public Nevermore60Customer(string name)&lt;br /&gt;&lt;br /&gt;         : this(name, "&lt;None&gt;")&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;      } &lt;br /&gt;&lt;br /&gt;这样就正确建立了所有的构造函数。执行下面的代码时，检查事件链是很有益的：&lt;br /&gt;&lt;br /&gt;   GenericCustomer customer = new Nevermore60Customer("Arabel Jones");&lt;br /&gt;&lt;br /&gt;编译器认为它需要带有一个字符串参数的构造函数，所以它确认的构造函数就是刚才定义的那个构造函数，如下所示。&lt;br /&gt;&lt;br /&gt;      public Nevermore60Customer(string Name)&lt;br /&gt;&lt;br /&gt;         : this(Name, "&lt;None&gt;")&lt;br /&gt;&lt;br /&gt;在实例化customer时，就会调用这个构造函数。之后立即把控制权传送给对应的Nevermore60Customer构造函数，该构造函数带有2个参数，分别是Arabel Jones和&lt;None&gt;。在这个构造函数中，把控制权依次传送给GenericCustomer构造函数，该构造函数带有1个参数，即字符串Arabel Jones。然后这个构造函数把控制权传送给System.Object默认构造函数。现在执行这些构造函数，首先执行System.Object构造函数，接着执行Nevermore60 Customer构造函数，初始化name字段。然后带有两个参数的Nevermore60Customer构造函数得到控制权，把联系人的姓名初始化为&lt;None&gt;。最后，执行Nevermore60 Customer构造函数，该构造函数带有1个参数—— 这个构造函数什么也不做。&lt;br /&gt;&lt;br /&gt;这个过程非常简洁，设计也很合理。每个构造函数都处理变量的初始化。在这个过程中，正确地实例化了类，以备使用。如果在为类编写自己的构造函数时遵循这个规则，即便是最复杂的类，也可以顺利地初始化，不会出现任何问题。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-9174811663049010001?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/9174811663049010001/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/03/c2_05.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/9174811663049010001'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/9174811663049010001'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/03/c2_05.html' title='c#教程（十七）实现的继承(2)'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-2779487402676175278</id><published>2009-03-05T16:39:00.000-08:00</published><updated>2009-03-05T21:13:09.391-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、继承'/><title type='text'>c#教程（十六）实现的继承(1)</title><content type='html'>如果要声明一个类派生于另一个类，可以使用下面的语法：&lt;br /&gt;&lt;br /&gt;class MyDerivedClass : MyBaseClass&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   // functions and data members here&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;这个语法非常类似于C++和Java中的语法，但是，C++程序员习惯于使用公共和私有继承的概念，要注意C#不支持私有继承，因此基类名上没有public或private限定符。支持私有继承会大大增加语言的复杂性，实际上私有继承在C++中也很少使用。&lt;br /&gt;&lt;br /&gt;如果类(或结构)也派生于接口，则用逗号分隔开基类和接口：&lt;br /&gt;&lt;br /&gt;public class MyDerivedClass : MyBaseClass, IInterface1, IInterface2&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;//etc.&lt;br /&gt;&lt;br /&gt;对于结构，语法如下：&lt;br /&gt;&lt;br /&gt;public struct MyDerivedStruct : IInterface1, IInterface2&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;//etc.&lt;br /&gt;&lt;br /&gt;如果在类定义中没有指定基类，C#编译器就假定System.Object是基类。因此下面的两段代码生成相同的结果：&lt;br /&gt;&lt;br /&gt;class MyClass : Object   //derives from System.Object&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   //etc. &lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;和&lt;br /&gt;&lt;br /&gt;class MyClass   //derives from System.Object&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   //etc. &lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;为了简单一些，第二种形式比较常用。&lt;br /&gt;&lt;br /&gt;C#支持object关键字，它用作System.Object类的假名，所以也可以编写下面的代码：&lt;br /&gt;&lt;br /&gt;class MyClass : object   //derives from System.Object&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   //etc. &lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;如果要引用Object类，可以使用object关键字，智能编辑器(如VS.NET)会识别它，并对代码进行合适的编辑。&lt;br /&gt;&lt;br /&gt;4.2.1  虚方法&lt;br /&gt;把一个基类函数声明为virtual，该函数就可以在任何派生类中重写了：&lt;br /&gt;&lt;br /&gt;class MyBaseClass&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   public virtual string VirtualMethod()&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      return "This method is virtual and defined in MyBaseClass";&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;也可以把属性声明为virtual。对于虚属性或重写属性，语法与非虚属性是相同的，但要在定义中加上关键字virtual，其语法如下所示：&lt;br /&gt;&lt;br /&gt;public virtual string ForeName&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   get { return foreName; }&lt;br /&gt;&lt;br /&gt;   set { foreName = value; }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;private string foreName;&lt;br /&gt;&lt;br /&gt;为了简单起见，下面的讨论将主要集中于方法，但其规则也适用于属性。&lt;br /&gt;&lt;br /&gt;C#中虚函数的概念与标准OOP概念相同：可以在派生类中重写虚函数。在调用方法时，会调用对象类型的合适方法。在C#中，函数在默认情况下不是虚拟的，但(除了构造函数以外)可以显式地声明为虚拟。这遵循C++的方式，即从性能的角度来看，除非显式指定，否则函数就不是虚拟的，而在Java中，所有的函数都是虚拟的。但C#的语法与C++的语法不同，因为C#要求在派生类的函数重写另一个函数时，要使用override关键字显式声明：&lt;br /&gt;&lt;br /&gt;class MyDerivedClass : MyBaseClass&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   public override string VirtualMethod()&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      return "This method is an override defined in MyDerivedClass";&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;方法重写的语法避免了C++中很容易发生的潜在运行错误：当派生类的方法签名无意中与基类版本略有差别时，派生类方法就不能重写基类方法了。在C#中，这会出现一个编译错误，因为编译器会认为函数已标记为override，但没有重写它的基类方法。&lt;br /&gt;&lt;br /&gt;成员字段和静态函数都不能被声明为virtual，因为这个概念只对类中的实例函数成员有意义。&lt;br /&gt;&lt;br /&gt;4.2.2  隐藏方法&lt;br /&gt;如果签名相同的方法在基类和派生类中都进行了声明，但该方法没有声明为virtual 和 override，派生类方法就会隐藏基类方法。结果是调用哪个类的方法取决于用于引用实例的变量类型，而不是实例本身的类型。&lt;br /&gt;&lt;br /&gt;在大多数情况下，是要重写方法，而不是隐藏方法，因为隐藏方法会存在为给定类的实例调用错误方法的危险。但是，C#语法可以确保开发人员在编译时收到这个潜在错误的警告，使隐藏方法更加安全。这也是类库开发人员得到的版本方面的好处。&lt;br /&gt;&lt;br /&gt;假定有人编写了类HisBaseClass：&lt;br /&gt;&lt;br /&gt;class HisBaseClass&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   // various members&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;在将来的某一刻，要编写一个自己的派生类，给HisBaseClass添加某个功能，特别是要添加一个目前基类中没有的方法MyGroovyMethod()：&lt;br /&gt;&lt;br /&gt;class MyDerivedClass: HisBaseClass&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   public int MyGroovyMethod()&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      // some groovy implementation&lt;br /&gt;&lt;br /&gt;      return 0;&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;一年后，基类的编写者决定扩展基类的功能。为了保持一致，他也添加了一个名为MyGroovyMethod()的方法，该方法的名称和签名与前面添加的方法的名称和签名相同，但并不完成相同的工作。在使用基类的新方法编译代码时，在应该调用哪个方法上就会有潜在的冲突。这在C#中完全合法，但因为我们的MyGroovyMethod()与基类的MyGroovyMethod()不相关，运行这段代码的结果就可能不是我们希望的结果。C#已经为此设计了一种方式，可以很好地处理这种情况。&lt;br /&gt;&lt;br /&gt;首先，系统会发出警告。在C#中，应使用new关键字声明我们要隐藏一个方法，如下所示：&lt;br /&gt;&lt;br /&gt;class MyDerivedClass : HisBaseClass&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   public new int MyGroovyMethod()&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      // some groovy implementation&lt;br /&gt;&lt;br /&gt;      return 0;&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;但是，我们的MyGroovyMethod()没有声明为new，所以编译器会认为它隐藏了基类的方法，但没有显式声明，因此发出一个警告(这也适用于把MyGroovyMethod()声明为 virtual)。如果愿意，可以给我们的方法重命名，以响应该警告。这么做，是最好的情形，因为这会避免许多冲突。但是，如果觉得重命名方法是不可能的(例如，已经为其他公司把软件发布为一个库，所以无法修改方法的名称)，则所有的已有客户机代码仍能正确运行，选择我们的MyGroovyMethod()。这是因为访问这个方法的已有代码必须通过对MyDerivedClass(或进一步派生的类)的引用进行选择。&lt;br /&gt;&lt;br /&gt;已有的代码不能通过对HisBaseClass的引用访问这个方法，因为在对HisBaseClass的早期版本进行编译时，会产生一个编译错误。这个问题只会发生在将来编写的客户机代码上。C#会发出一个警告，告诉用户在将来的代码中可能会出问题——用户应注意这个警告，不要试图在将来的代码中通过对HisBaseClass的引用调用MyGroovyMethod()方法，但所有已有的代码仍会正常工作。这是比较微妙的，但很好地说明了C#如何处理类的不同版本。&lt;br /&gt;&lt;br /&gt;4.2.3  调用函数的基础版本&lt;br /&gt;C#有一种特殊的语法用于从派生类中调用方法的基础版本：base.&lt;MethodName&gt;()。例如，假定派生类中的一个方法要返回基类的方法返回的值的90%，就可以使用下面的语法：&lt;br /&gt;&lt;br /&gt;class CustomerAccount&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   public virtual decimal CalculatePrice()&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      // implementation&lt;br /&gt;&lt;br /&gt;     return 0.0M;&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}   &lt;br /&gt;&lt;br /&gt;class GoldAccount : CustomerAccount&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   public override decimal CalculatePrice()&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      return base.CalculatePrice() * 0.9M;&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;} &lt;br /&gt;&lt;br /&gt;这个语法类似于Java，但Java使用关键字super而不是base。C++没有类似的关键字，但需要对类名进行显式说明(CustomerAccount : CalculatePrice())。C++中对应于base的内容都比较模糊，因此C++允许多重继承。&lt;br /&gt;&lt;br /&gt;注意，可以使用base.&lt;MethodName&gt;()语法调用基类中的任何方法，不必是在同一个方法的重载中调用它。&lt;br /&gt;&lt;br /&gt;4.2.4  抽象类和抽象函数&lt;br /&gt;C#允许把类和函数声明为abstract，抽象类不能实例化，而抽象函数没有执行代码，必须在非抽象的派生类中重写。显然，抽象函数也是虚拟的(但也不需要提供virtual关键字，实际上，如果提供了该关键字，就会产生一个语法错误)。如果类包含抽象函数，该类将也是抽象的，也必须声明为抽象的：&lt;br /&gt;&lt;br /&gt;abstract class Building&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   public abstract decimal CalculateHeatingCost();   // abstract method&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;C++开发人员要注意C#中的一些语法区别。C#不支持采用=0语法来声明抽象函数。在C#中，这个语法有误导作用，因为可以在类声明的成员字段上使用=&lt;value&gt;，提供初始值：&lt;br /&gt;&lt;br /&gt;abstract class Building&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   private bool damaged = false;   // field&lt;br /&gt;&lt;br /&gt;   public abstract decimal CalculateHeatingCost();   // abstract method&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;C++开发人员还要注意术语上的细微差别：在C++中，抽象函数常常声明为纯虚函数，而在C#中，仅使用抽象这个术语。&lt;br /&gt;&lt;br /&gt;4.2.5  密封类和密封方法&lt;br /&gt;C#允许把类和方法声明为sealed，对于类来说，这表示不能继承该类，对于方法来说；这表示不能重写该方法。&lt;br /&gt;&lt;br /&gt;sealed class FinalClass&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   // etc&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;class DerivedClass : FinalClass       // wrong. Will give compilation error &lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   // etc&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;Java开发人员可以把C#中的sealed当作Java中的final。&lt;br /&gt;&lt;br /&gt;在把类或方法标记为sealed时，最可能的情形是：如果要对库、类或自己编写的其他类进行操作，则重写某些功能会导致错误。也可以因商业原因把类或方法标记为sealed，以防第三方以违反注册协议的方式扩展该类。但一般情况下，在把类或方法标记为sealed时要小心，因为这么做会严重限制它的使用。即使不希望能继承一个类或重写类的某个成员，仍有可能在将来的某个时刻，有人会遇到我们没有预料到的情形。.NET基类库大量使用了密封类，使希望从这些类中派生出自己的类的第三方开发人员无法访问这些类。例如string就是一个密封类。&lt;br /&gt;&lt;br /&gt;把方法声明为sealed也可以实现类似的目的，但很少这么做。&lt;br /&gt;&lt;br /&gt;class MyClass&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   public sealed override void FinalMethod()&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      // etc.&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;class DerivedClass : MyClass&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   public override void FinalMethod()      // wrong. Will give compilation error&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;在方法上使用sealed关键字是没有意义的，除非该方法本身是某个基类上另一个方法的重写形式。如果定义一个新方法，但不想让别人重写它，首先就不要把它声明为virtual。但如果要重写某个基类方法，sealed关键字就提供了一种方式，可以确保为方法提供的重写代码是最终的代码，其他人不能再重写它。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-2779487402676175278?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/2779487402676175278/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/03/c1_05.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/2779487402676175278'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/2779487402676175278'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/03/c1_05.html' title='c#教程（十六）实现的继承(1)'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-3324705803349700597</id><published>2009-03-04T17:39:00.000-08:00</published><updated>2009-03-04T17:40:14.399-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、继承'/><title type='text'>c#教程（十五）继承</title><content type='html'>面向对象编程的开发人员知道，有两种截然不同的继承类型：实现继承和接口继承。&lt;br /&gt;&lt;br /&gt;●       实现继承：表示一个类型派生于一个基类型，拥有该基类型的所有成员字段和函数。在实现的继承中。派生类型的每个函数采用基类型的实现，除非在派生类型的定义中指定重写该函数的实现。在需要给现有的类型添加功能，或许多相关的类型共享一组重要的公共功能时，这种类型的继承是非常有效的。例如第19章讨论的Windows Forms类(第19章也讨论了基类System.Windows.Forms.Control，该类提供了常用Windows控件的非常复杂的实现，第19章还讨论了许多其他的类，例如System.Windows.Forms. TextBox和System.Windows.Forms.ListBox，这两个类派生于Control，并重写了函数，或提供了新的函数，以实现特定类型的控件)。&lt;br /&gt;&lt;br /&gt;●       接口继承：表示一个类型只继承了函数的签名，没有继承任何实现。在需要指定该类型具有某些可用的特性时，最好使用这种类型的继承。例如，某些类型可以指定从接口System.IDisposable(详见第7章)中派生，从而提供一种清理资源的方法Dispose()。由于某种类型清理资源的方式可能与另一种类型的完全不同，所以定义通用的实现是没有意义的，此时就适合使用接口继承。接口继承常常被看做提供了一种契约：通过类型派生于接口，从而保证为客户提供某个功能。&lt;br /&gt;&lt;br /&gt;在传统上，像C++这样的语言在实现的继承方面功能非常强大，实际上，实现继承是C++编程模型的核心。另一方面，VB6不支持类的任何实现继承，但因其底层的COM基础体系，所以它支持接口继承。&lt;br /&gt;&lt;br /&gt;在C#中，既有实现继承，也有接口继承。它们没有强弱之分，因为这两种继承都完全内置于语言中，因此很容易为不同的情形选择最好的体系结构。&lt;br /&gt;&lt;br /&gt;4.1.2  多重继承&lt;br /&gt;一些语言如C++支持所谓的“多重继承”，即一个类派生于多个类。使用多重继承的优点是有争议的：一方面，毫无疑问，可以使用多重继承编写非常复杂、但很紧凑的代码，如C++ ALT库。另一方面，使用多重实现继承的代码常常很难理解和调试(这也可以从C++ ALT库中看出)。如前所述，使健壮代码的编写容易一些，是开发C#的重要设计目标。因此，C#不支持多重实现继承。而C#又允许类型派生于多个接口。这说明，C#类可以派生于另一个类和任意多个接口。因为System.Object是一个公共的基类，所以每个C#类(除了Object类之外)都有一个基类，还可以有任意多个基接口。&lt;br /&gt;&lt;br /&gt;4.1.3  结构和类&lt;br /&gt;第3章区分了结构(值类型)和类(引用类型)。使用结构的一个限制是结构不支持继承，但每个结构都自动派生于System.ValueType。实际上还应更仔细一些：不能建立结构的类型层次，但结构可以实现接口。换言之，结构并不支持实现继承，但支持接口继承。事实上，定义结构和类可以总结为：&lt;br /&gt;&lt;br /&gt;●       结构总是派生于System.ValueType，它们还可以派生于任意多个接口。&lt;br /&gt;&lt;br /&gt;●       类总是派生于用户选择的另一个类，它们还可以派生于任意多个接口。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-3324705803349700597?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/3324705803349700597/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/03/c_04.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/3324705803349700597'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/3324705803349700597'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/03/c_04.html' title='c#教程（十五）继承'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-2624809268681594239</id><published>2009-03-04T17:35:00.000-08:00</published><updated>2009-03-04T17:38:44.705-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、Object'/><title type='text'>c#教程（十四） Object类</title><content type='html'>所有的.NET类都派生于System.Object。在C#中，如果在定义类时没有指定基类，编译器就会自动假定这个类派生于Object。本章没有使用继承，所以前面介绍的每个类都派生于System.Object(如前所述，对于结构，这个派生是间接的：结构总是派生于System.ValueType，System.ValueType派生于System.Object)。&lt;br /&gt;&lt;br /&gt;其重要性在于，除了自己定义的方法和属性外，还可以访问为Object定义的许多公共或受保护的成员方法。这些方法可以用于自己定义的所有其他类中。&lt;br /&gt;&lt;br /&gt;3.4.1  System.Object方法&lt;br /&gt;在Object中定义的方法如表3-2所示。&lt;br /&gt;&lt;br /&gt;表  3-2&lt;br /&gt;&lt;br /&gt;方    法&lt;br /&gt; 访问修饰符&lt;br /&gt; 作    用&lt;br /&gt; &lt;br /&gt;string ToString()&lt;br /&gt; public virtual&lt;br /&gt; 返回对象的字符串表示&lt;br /&gt; &lt;br /&gt;int GetHashTable()&lt;br /&gt; public virtual&lt;br /&gt; 在实现字典(散列表)时使用&lt;br /&gt; &lt;br /&gt;bool Equals(object obj)&lt;br /&gt; public virtual&lt;br /&gt; 对对象的实例进行相等比较&lt;br /&gt; &lt;br /&gt;bool Equals(object objA, &lt;br /&gt;&lt;br /&gt;object objB)&lt;br /&gt; public static&lt;br /&gt; 对对象的实例进行相等比较&lt;br /&gt; &lt;br /&gt;bool ReferenceEquals(object objA, object objB)&lt;br /&gt; public static&lt;br /&gt; 比较两个引用是否指向同一个对象&lt;br /&gt; &lt;br /&gt;Type GetType()&lt;br /&gt; public&lt;br /&gt; 返回对象类型的详细信息&lt;br /&gt; &lt;br /&gt;object MemberwiseClone()&lt;br /&gt; protected&lt;br /&gt; 进行对象的浅表复制&lt;br /&gt; &lt;br /&gt;void Finalize()&lt;br /&gt; protected virtual&lt;br /&gt; 该方法是析构函数的.NET版本&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;我们还没有完整地介绍&lt;a href="http://futureangle.blogspot.com"&gt;&lt;strong&gt;C#&lt;/strong&gt;&lt;/a&gt;语言，所以用户还不能理解使用这些方法的方式。下面将简要总结每个方法的作用，但ToString()方法要详细论述。&lt;br /&gt;&lt;br /&gt;●       ToString()方法：是快速获取对象的字符串表示的一种便捷方式。当只需要快速获取对象的内容，用于调试时就可以使用这个方法。在数据的格式化方面，它提供的选择非常少：例如，日期在原则上可以表示为许多不同的格式，但DateTime.ToString()没有在这方面提供任何选择。如果需要更专业的字符串表示，例如考虑用户的格式化配置或文化(区域)，就应实现IFormattable接口(详见第8章)。&lt;br /&gt;&lt;br /&gt;●       GetHashTable()方法：如果对象放在名为映射(也称为散列表或字典)的数据结构中，就可以使用这个方法。处理这些结构的类使用该方法确定把对象放在结构的什么地方。如果希望把类用作字典的一个键，就需要重写GetHashTable()方法。对该方法重载的执行方式有一些相当严格的限制，这些将在第9章介绍字典时讨论。&lt;br /&gt;&lt;br /&gt;●       Equals()(两个版本)和ReferenceEquals()方法：如果把3个用于比较对象相等性的不同方法组合起来，就说明.NET Framework在比较相等性方面有相当复杂的模式。这3个方法和比较运算符==在使用方式上有微妙的区别。而且，在重写带一个参数的虚拟Equals()方法时也有一些限制，如果选择这么做，就是因为System.Collections命名空间中的一些基类要调用该方法，并希望它以特定的方式执行。第5章在介绍运算符时将探讨这些方法的使用。&lt;br /&gt;&lt;br /&gt;●       Finalize()方法：第7章将介绍这个方法，它最接近C++风格的析构函数，在引用对象被回收调用，以清理资源时。Finalize()方法的Object执行代码实际上什么也没有做，因而被垃圾收集器忽略。如果对象拥有对未托管资源的引用，则在该对象被删除时，就需要删除这些引用，此时一般要重写Finalize()。垃圾收集器不能直接重写该方法，因为它只负责托管的资源，只能使用用户提供的Finalize()。&lt;br /&gt;&lt;br /&gt;●       GetType()方法：这个方法返回从System.Type派生的类的一个实例。这个对象可以提供对象所属类的更多信息，包括基本类型、方法、属性等。System.Type还提供了.NET反射技术的入口。这个主题详见第10章。&lt;br /&gt;&lt;br /&gt;●       MemberwiseClone()方法：这是System.Object中惟一没有在本书的其他地方详细论述的方法。不需要讨论这个方法，因为它在概念上相当简单，只是复制对象，返回一个对副本的引用(对于值类型，就是一个装箱的引用)。注意，得到的副本是一个简单复制，即它复制了类中的所有值类型。如果类包含内嵌的引用，就只复制引用，而不复制引用的对象。这个方法是受保护的，所以不能用于复制外部的对象。该方法不是虚拟的，所以不能重写它的实现。&lt;br /&gt;&lt;br /&gt;3.4.2  ToString()方法&lt;br /&gt;第2章已经提到了ToString()方法，它是快速获取对象的字符串表示的最便捷的方式。&lt;br /&gt;&lt;br /&gt;例如：&lt;br /&gt;&lt;br /&gt;int i = –50;&lt;br /&gt;&lt;br /&gt;string str = i.ToString();  // returns "–50"&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;下面是另一个例子：&lt;br /&gt;&lt;br /&gt;enum Colors {Red, Orange, Yellow};&lt;br /&gt;&lt;br /&gt;// later on in code...&lt;br /&gt;&lt;br /&gt;Colors favoriteColor = Colors.Orange;&lt;br /&gt;&lt;br /&gt;string str = favoriteColor.ToString();                 // returns "Orange"&lt;br /&gt;&lt;br /&gt;Object.ToString() 声明为虚类型，在这些例子中，该方法的实现都是为C#预定义数据类型重写过的代码，以返回这些类型的正确字符串表示。您可能没有想到，Colors枚举就是一个预定义数据类型，它实际上执行为一个派生于System.Enum的结构，而System.Enum有一个相当聪明的ToString()重写方法，来处理用户定义的所有枚举。&lt;br /&gt;&lt;br /&gt;如果不在自己定义的类中重写ToString()，该类将只继承System.Object执行方式—— 显示类的名称。如果希望ToString()返回一个字符串，其中包含类中对象的值，就需要重写它。下面用一个例子Money来说明这一点。在该例子中，定义一个非常简单的类Money，表示美元数。Money是decimal类的包装器，提供了一个ToString()方法。注意，这个方法必须声明为override，因为它将替代(重写)Object提供的ToString()方法。第4章将详细讨论重写。该例子的完整代码如下所示(注意它还说明了如何使用属性封装字段)：&lt;br /&gt;&lt;br /&gt;using System;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;namespace Wrox.ProCSharp.OOCSharp&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   class MainEntryPoint&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      static void Main(string[] args)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         Money cash1 = new Money();&lt;br /&gt;&lt;br /&gt;         cash1.Amount = 40M;&lt;br /&gt;&lt;br /&gt;         Console.WriteLine("cash1.ToString() returns: " + cash1.ToString());&lt;br /&gt;&lt;br /&gt;         Console.ReadLine();&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;   class Money&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      private decimal amount;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      public decimal Amount&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         get&lt;br /&gt;&lt;br /&gt;         {&lt;br /&gt;&lt;br /&gt;            return amount;&lt;br /&gt;&lt;br /&gt;         }&lt;br /&gt;&lt;br /&gt;         set&lt;br /&gt;&lt;br /&gt;         {&lt;br /&gt;&lt;br /&gt;            amount = value;&lt;br /&gt;&lt;br /&gt;         }&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;      public override string ToString()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         return "$" + Amount.ToString();&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;这个例子仅说明了C#的语法特性。C#已经有表示货币的预定义类型decimal。所以在现实生活中，不必编写这样的类来重复该功能，除非要给它添加其他方法。在许多情况下，由于格式化要求，也可以使用String.Format()方法(详见第8章)格式化货币字符串，而不是ToString()。&lt;br /&gt;&lt;br /&gt;在Main()方法中，先实例化一个Money对象，在这个实例化过程中调用了ToString()。对于Money对象，选择我们自己的重写方法，显示类的信息。运行这段代码，会得到如下结果：&lt;br /&gt;&lt;br /&gt;StringRepresentations&lt;br /&gt;&lt;br /&gt;cash1.ToString() returns: $40&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-2624809268681594239?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/2624809268681594239/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/03/c-object.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/2624809268681594239'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/2624809268681594239'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/03/c-object.html' title='c#教程（十四） Object类'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-5158676682845821627</id><published>2009-03-03T17:24:00.001-08:00</published><updated>2009-03-03T17:24:45.775-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、结构'/><title type='text'>c#教程（十三）结构</title><content type='html'>class Dimensions&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   public double Length;&lt;br /&gt;&lt;br /&gt;   public double Width;&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;上面的示例定义了类Dimensions，它只存储了一些项的长度和宽度，也许可以编写一个安排设备的程序，让人们试着重新安排计算机上的设备，并存储每个设备项的维数。看起来这样就会违背编程规则，使字段变为公共字段，但我们实际上并不需要类的全部功能。现在只有两个数字，把它们当作一对来处理，要比单个处理方便一些。既不需要很多方法，也不需要从类中继承，也不希望.NET运行库在堆中遇到麻烦和性能问题，只需存储两个double类型的数据即可。&lt;br /&gt;&lt;br /&gt;如本章前面所述，为此，只需修改代码，用关键字struct代替class，定义一个结构而不     是类： &lt;br /&gt;&lt;br /&gt;   struct Dimensions&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      public double Length;&lt;br /&gt;&lt;br /&gt;      public double Width;&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;为结构定义函数与为类定义函数完全相同。下面的代码演示了结构的构造函数和属性：&lt;br /&gt;&lt;br /&gt;   struct Dimensions&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      public double Length;&lt;br /&gt;&lt;br /&gt;      public double Width;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      Dimensions(double length, double width)&lt;br /&gt;&lt;br /&gt;      { Length= length; Width= width; }&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      public int Diagonal&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         {&lt;br /&gt;&lt;br /&gt;            get&lt;br /&gt;&lt;br /&gt;            {&lt;br /&gt;&lt;br /&gt;               return Math.Sqrt(Length* Length + Width* Width);&lt;br /&gt;&lt;br /&gt;             }&lt;br /&gt;&lt;br /&gt;         }&lt;br /&gt;&lt;br /&gt;       }&lt;br /&gt;&lt;br /&gt;   } &lt;br /&gt;&lt;br /&gt;在许多方面，可以把C#中的结构看作是缩小的类。它们基本上与类相同，但更适合于把一些数据组合起来的场合。它们与类的区别在于：&lt;br /&gt;&lt;br /&gt;●       结构是值类型不是引用类型。它们存储在堆栈中或存储为内联(inline)(如果它们是另一个对象的一部分，就会保存在堆中)，其生存期的限制与简单的数据类型一样。&lt;br /&gt;&lt;br /&gt;●       结构不支持继承。&lt;br /&gt;&lt;br /&gt;●       结构的构造函数的工作方式有一些区别。尤其是编译器总是提供一个无参数的默认构造函数，这是不允许替换的。&lt;br /&gt;&lt;br /&gt;●       使用结构，可以指定字段如何在内存中布局(第10章在介绍属性时将详细论述这个问题)。&lt;br /&gt;&lt;br /&gt;因为结构实际上是把数据项组合在一起，有时大多数甚至全部字段都声明为public。严格说来，这与编写.NET代码的规则相背—— 根据Microsoft，字段(除了const字段之外)应总是私有的，并由公共属性封装。但是，对于简单的结构，许多开发人员都认为公共字段是可接受的编程方式。&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;C++开发人员要注意，C#中的结构在实现方式上与类大不相同。这与C++的情形完全不同，在C++中，类和结构是相同的对象。&lt;br /&gt;&lt;br /&gt;下面将详细说明类和结构之间的区别。&lt;br /&gt;&lt;br /&gt;3.3.1  结构是值类型&lt;br /&gt;虽然结构是值类型，但在语法上常常可以把它们当作类来处理。例如，在上面的Dimensions类的定义中，可以编写下面的代码：&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;   Dimensions point = new Dimensions();&lt;br /&gt;&lt;br /&gt;   point.Length = 3;&lt;br /&gt;&lt;br /&gt;   point.Width = 6;&lt;br /&gt;&lt;br /&gt;注意，因为结构是值类型，所以new运算符与类和其他引用类型的工作方式不同。new运算符并不分配堆中的内存，而是调用相应的构造函数，根据传送给它的参数，初始化所有的字段。对于结构，可以编写下述代码：&lt;br /&gt;&lt;br /&gt;   Dimensions point;&lt;br /&gt;&lt;br /&gt;   point.Length = 3;&lt;br /&gt;&lt;br /&gt;   point.Width = 6;&lt;br /&gt;&lt;br /&gt;如果Dimensions是一个类，就会产生一个编译错误，因为Point应包含一个未初始化的引用——不指向任何地方的一个地址，所以不能给其字段设置值。但对于结构，变量声明实际上是为整个结构分配堆栈中的空间，所以就可以赋值了。但要注意下面的代码会产生一个编译错误，编译器会抱怨用户使用了未初始化的变量：&lt;br /&gt;&lt;br /&gt;   Dimensions point;&lt;br /&gt;&lt;br /&gt;   Double D = point.Length;&lt;br /&gt;&lt;br /&gt;结构遵循其他数据类型都遵循的规则：在使用前所有的元素都必须进行初始化。在结构上调用new运算符，或者给所有的字段分别赋值，结构就可以完全初始化了。当然，如果结构定义为类的成员字段，在初始化包含对象时，该结构会自动初始化为0。&lt;br /&gt;&lt;br /&gt;结构是值类型，所以会影响性能，但根据使用结构的方式，这种影响可能是正面的，也可能是负面的。正面的影响是为结构分配内存时，速度非常快，因为它们将内联或者保存在堆栈中。在结构出了作用域被删除时，速度也很快。另一方面，只要把结构作为参数来传递或者把一个结构赋给另一个结构(例如A=B，其中A和B是结构)，结构的所有内容就被复制，而对于类，则只复制引用。这样，就会有性能损失，根据结构的大小，性能损失也不同。注意，结构主要用于小的数据结构。但当把结构作为参数传递给方法时，就应把它作为ref参数传递，以避免性能损失——此时只传递了结构在内存中的地址，这样传递速度就与在类中的传递速度一样快了。另一方面，如果这样做，就必须注意被调用的方法可以改变结构的值。&lt;br /&gt;&lt;br /&gt;3.3.2  结构和继承&lt;br /&gt;结构不是为继承设计的。不能从一个结构中继承，惟一的例外是结构(和C#中的其他类型一样)派生于类System.Object。因此，结构也可以访问System.Object的方法。在结构中，甚至可以重写System.Object中的方法—— 例如重写ToString()方法。结构的继承链是：每个结构派生于System.ValueType，System.ValueType派生于System.Object。ValueType并没有给Object添加任何新成员，但提供了Object的一些更适合结构的执行代码。注意，不能为结构提供其他基类：每个结构都派生于ValueType。&lt;br /&gt;&lt;br /&gt;3.3.3  结构的构造函数&lt;br /&gt;为结构定义构造函数的方式与为类定义构造函数的方式相同，但不允许定义无参数的构造函数。这看起来似乎没有意义，其原因隐藏在.NET运行库的执行方式中。下述情况非常少见：.NET运行库不能调用用户提供的定制0参数构造函数，因此Microsoft采用一种非常简单的方式，禁止在C#中的结构内使用0参数的构造函数。&lt;br /&gt;&lt;br /&gt;前面说过，默认构造函数把所有的字段都初始化为0，且总是隐式地给出，即使提供了其他带参数的构造函数，也是如此。也不能提供字段的初始值，以此绕过默认构造函数。下面的代码会产生编译错误：&lt;br /&gt;&lt;br /&gt;   struct Dimensions&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      public double Length = 1;       // error. Initial values not allowed&lt;br /&gt;&lt;br /&gt;      public double Width = 2;        // error. Initial values not allowed&lt;br /&gt;&lt;br /&gt;当然，如果Dimensions声明为一个类，这段代码就不会有编译错误。&lt;br /&gt;&lt;br /&gt;另外，可以像类那样为结构提供Close()或 Dispose()方法。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-5158676682845821627?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/5158676682845821627/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/03/c.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/5158676682845821627'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/5158676682845821627'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/03/c.html' title='c#教程（十三）结构'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-6446418700661781934</id><published>2009-03-03T17:22:00.000-08:00</published><updated>2009-03-03T17:23:55.817-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、类、'/><title type='text'>c#教程（十二）类成员(2)</title><content type='html'>(1) 只读和只写属性&lt;br /&gt;&lt;br /&gt;在属性定义中省略set访问器，就可以创建只读属性。因此，在上面的例子中把ForeName变成只读属性：&lt;br /&gt;&lt;br /&gt;private string foreName;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;public string ForeName&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   get&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      return foreName;&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;同样，在属性定义中省略get访问器，就可以创建只写属性。但是，这是不好的编程方式，因为这可能会使客户机代码的作者感到迷惑。一般情况下，如果要这么做，最好使用一个方法替代。&lt;br /&gt;&lt;br /&gt;(2) 属性的访问修饰符&lt;br /&gt;&lt;br /&gt;C#不允许给属性的get 和 set 访问器设置不同的访问修饰符。如果有一个封装了私有字段的属性需要读取的公共访问权限，但要限制对派生类的写入访问，就应把底层的字段设置为protected，而不是private，但这通常是不好的编码方法。此时，最常见的解决方法是声明一个公共只读属性和一个受保护或私有的Set()函数：&lt;br /&gt;&lt;br /&gt;public string ForeName&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   get&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      return foreName;&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;protected void SetForeName(string value)&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   if (value.Length &gt; 20)&lt;br /&gt;&lt;br /&gt;      // code here to take error recovery action &lt;br /&gt;&lt;br /&gt;      // (eg. throw an exception)&lt;br /&gt;&lt;br /&gt;   else&lt;br /&gt;&lt;br /&gt;      foreName = value;&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;(3) 内联&lt;br /&gt;&lt;br /&gt;一些开发人员可能会担心，在上一节中，我们列举了标准C#编码方式导致了使用非常小的函数的许多情形，例如通过属性访问字段，而不是直接访问字段。这些额外的函数调用是否会增加系统开销，导致性能下降？其实，不需要担心这种编程方式会在C#中带来性能损失。C#代码会编译为IL，然后在运行期间进行正常的JIT编译，获得内部可执行代码，并在适当的时候内联代码(即用内联代码来替代函数调用)。如果某个方法或属性的执行代码仅是调用另一个方法，或返回一个字段，则该方法或属性肯定是内联的。但要注意，在何处内联代码的决定完全由CLR做出。我们无法使用像C++中inline这样的关键字来控制哪些方法是内联的。&lt;br /&gt;&lt;br /&gt;3. 构造函数&lt;br /&gt;在C#中声明基本构造函数的语法与在Java 和C++中相同。下面声明一个与包含的类同名的方法，但该方法没有返回类型：&lt;br /&gt;&lt;br /&gt;public class MyClass&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   public MyClass()&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;   // rest of class definition&lt;br /&gt;&lt;br /&gt;与Java 和 C++相同，没有必要给类提供构造函数，在我们的例子中没有提供这样的构造函数。一般情况下，如果没有显式地提供任何构造函数，编译器会在后台创建一个默认的构造函数。这是一个非常基本的构造函数，它只能把所有的成员字段初始化为标准的默认值(例如，引用类型为空引用，数字数据类型为0，bool为false)。这通常就足够了，否则就需要编写自己的构造函数。&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;对于C++开发人员来说，C#中的基本字段在默认情况下初始化为0，而C++中的基本字段不进行初始化，不需要像C++那样在C#中编写构造函数。&lt;br /&gt;&lt;br /&gt;构造函数的重载遵循与其他方法相同的规则。换言之，可以为构造函数提供任意多的重载，只要它们的签名有明显的区别即可：&lt;br /&gt;&lt;br /&gt;   public MyClass()   // zero-parameter constructor&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      // construction code &lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;   public MyClass(int number)   // another overload&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      // construction code &lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;但注意，如果提供了带参数的构造函数，编译器就不会自动提供默认的构造函数，只有在没有显式定义任何构造函数时，编译器才会自动提供默认的构造函数。在下面的例子中，因为明确定义了一个带一个参数的构造函数，所以编译器会假定这是可以使用的惟一构造函数，不会隐式地提供其他构造函数：&lt;br /&gt;&lt;br /&gt;public class MyNumber&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   private int number;&lt;br /&gt;&lt;br /&gt;   public MyNumber(int number)   &lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      this.number = number; &lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;上面的代码还说明，一般使用this关键字区分成员字段和同名的参数。如果试图使用无参数的构造函数实例化MyNumber对象，就会得到一个编译错误：&lt;br /&gt;&lt;br /&gt;MyNumber numb = new MyNumber();   // causes compilation error&lt;br /&gt;&lt;br /&gt;注意，可以把构造函数定义为private或protected，这样不相关的类就看不到它们：&lt;br /&gt;&lt;br /&gt;public class MyNumber&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   private int number;&lt;br /&gt;&lt;br /&gt;   private MyNumber(int number)   // another overload&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      this.number = number; &lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;在这个例子中，我们并没有为MyNumber定义任何公共或受保护的构造函数。这就使MyNumber不能使用new运算符在外部代码中实例化(但可以在MyNumber上编写一个公共静态属性或方法，以进行实例化)。这在下面两种情况下是有用的：&lt;br /&gt;&lt;br /&gt;●       类仅用作某些静态成员或属性的容器，因此永远不会实例化。&lt;br /&gt;&lt;br /&gt;●       希望类仅通过调用某个静态成员函数来实例化(这就是所谓对象实例化的类代理方法)&lt;br /&gt;&lt;br /&gt;(1) 静态构造函数&lt;br /&gt;&lt;br /&gt;C#的一个新特征是也可以给类编写无参数的静态构造函数。这种构造函数只执行一次，而前面的构造函数是实例构造函数，只要创建类的对象，它都会执行。静态构造函数在C++和VB6中没有对应的函数。&lt;br /&gt;&lt;br /&gt;class MyClass&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   static MyClass()&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      // initialization code&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;   // rest of class definition&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;编写静态构造函数的一个原因是，类有一些静态字段或属性，需要在第一次使用类之前，从外部源中初始化这些静态字段和属性。&lt;br /&gt;&lt;br /&gt;.NET运行库没有确保静态构造函数什么时候执行，所以不要把代码放在某个特定的时刻(例如，加载程序集时)执行的静态构造函数中。也不能预计不同类的静态构造函数按照什么顺序执行。但是，可以确保静态构造函数至多运行一次，即在代码引用类之前执行。在C#中，静态构造函数通常在第一次调用类的成员之前执行。&lt;br /&gt;&lt;br /&gt;注意，静态构造函数没有定义访问修饰符，其他C#代码从来不调用它，但在加载类时，总是由.NET运行库调用它，所以像public 和 private这样的访问修饰符就没有意义了。同样，静态构造函数不能带有任何参数，一个类也只能有一个静态构造函数。很显然，静态构造函数只能访问类的静态成员，不能访问实例成员。&lt;br /&gt;&lt;br /&gt;注意，无参数的实例构造函数可以在类中与静态构造函数安全共存。尽管参数列表是相同的，但这并不矛盾，因为静态构造函数是在加载类时执行，而实例构造函数是在创建实例时执行，所以构造函数的执行不会有冲突。&lt;br /&gt;&lt;br /&gt;如果多个类都有静态构造函数，先执行哪个静态构造函数是不确定的。此时应根据其他静态构造函数的执行情况，在静态构造函数中添加代码。另一方面，如果静态字段有默认值，它们就在调用静态构造函数之前指定。&lt;br /&gt;&lt;br /&gt;下面用一个例子来说明静态构造函数的使用。假定这个例子叫StaticConstructor，基于包含用户设置的程序(假定存储在某个配置文件中)。为了简单一些，假定只有一个用户设置——BackColor，表示要在应用程序中使用的背景色。因为这里不想编写从外部数据源中读取数据的代码，所以假定该设置在工作日的背景色是红色，在周末的背景色是绿色。程序仅在控制台窗口中显示设置—— 但这足以说明静态构造函数是如何工作的。&lt;br /&gt;&lt;br /&gt;namespace Wrox.ProCSharp.StaticConstructorSample&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   public class UserPreferences&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      public static readonly Color BackColor;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      static UserPreferences()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         DateTime now = DateTime.Now;&lt;br /&gt;&lt;br /&gt;         if (now.DayOfWeek == DayOfWeek.Saturday &lt;br /&gt;&lt;br /&gt;            || now.DayOfWeek == DayOfWeek.Sunday)&lt;br /&gt;&lt;br /&gt;            BackColor = Color.Green;&lt;br /&gt;&lt;br /&gt;         else&lt;br /&gt;&lt;br /&gt;            BackColor = Color.Red;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      private UserPreferences()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;这段代码说明了颜色设置如何存储在静态变量中，该静态变量在静态构造函数中进行初始化。把这个字段声明为只读类型，表示其值只能在构造函数中设置。本章后面将详细介绍只读字段。这段代码使用了Microsoft在Framework类库中支持的两个有用的结构System.DateTime和System.Drawing.Color。DateTime结构实现了静态属性Now和实例属性DayOfWeek，Now属性返回当前的时间，DayOfWeek属性可以计算出某个日期是星期几。Color(详见第20章)用于存储颜色，它实现了各种静态属性，例如本例使用的Red和Green，返回常用的颜色。为了使用Color结构，需要在编译时引用System.Drawing.dll程序集，且必须为System.Drawing命名空间添加一个using语句：&lt;br /&gt;&lt;br /&gt;using System; &lt;br /&gt;&lt;br /&gt;using System.Drawing;&lt;br /&gt;&lt;br /&gt;用下面的代码测试静态构造函数：&lt;br /&gt;&lt;br /&gt;   class MainEntryPoint&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      static void Main(string[] args)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         Console.WriteLine("User-preferences: BackColor is: " + &lt;br /&gt;&lt;br /&gt;                            UserPreferences.BackColor.ToString());&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;编译并运行这段代码，会得到如下结果：&lt;br /&gt;&lt;br /&gt;C:&gt;StaticConstructor&lt;br /&gt;&lt;br /&gt;User-preferences: BackColor is: Color [Red]&lt;br /&gt;&lt;br /&gt;(2) 从其他构造函数中调用构造函数&lt;br /&gt;&lt;br /&gt;有时可能需要在一个类中有几个构造函数，以容纳某些可选参数，这些构造函数都包含一些共同的代码。例如，下面的情况：&lt;br /&gt;&lt;br /&gt;class Car&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   private string description;&lt;br /&gt;&lt;br /&gt;   private uint nWheels;&lt;br /&gt;&lt;br /&gt;   public Car(string model, uint nWheels)&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      this.description = description;&lt;br /&gt;&lt;br /&gt;      this.nWheels = nWheels;&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;   public Car(string model)&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      this.description = description;&lt;br /&gt;&lt;br /&gt;      this.nWheels = 4;&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;// etc.&lt;br /&gt;&lt;br /&gt;这两个构造函数初始化了相同的字段，显然，最好把所有的代码放在一个地方。&lt;a href="http://futureangle.blogspot.com"&gt;C#&lt;/a&gt;有一个特殊的语法，称为构造函数初始化器，可以实现此目的：&lt;br /&gt;&lt;br /&gt;class Car&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   private string description;&lt;br /&gt;&lt;br /&gt;   private uint nWheels;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;   public Car(string model, uint nWheels)&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      this.description = description;&lt;br /&gt;&lt;br /&gt;      this.nWheels = nWheels;&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;   public Car(string model) : this(model, 4)&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;   // etc   &lt;br /&gt;&lt;br /&gt;这里，this关键字仅调用参数最匹配的那个构造函数。注意，构造函数初始化器在构造函数之前执行。现在假定运行下面的代码：&lt;br /&gt;&lt;br /&gt;Car myCar = new Car("Proton Persona");&lt;br /&gt;&lt;br /&gt;在本例中，在带有一个参数的构造函数执行之前，先执行带2个参数的构造函数(但在本例中，因为带有一个参数的构造函数没有代码，所以没有区别)。&lt;br /&gt;&lt;br /&gt;C#构造函数初始化符可以包含对同一个类的另一个构造函数的调用(使用前面介绍的语法)，也可以包含对直接基类的构造函数的调用(使用相同的语法，但应使用base关键字代替this)。初始化符中不能有多于一个的调用。&lt;br /&gt;&lt;br /&gt;在C#中，构造函数初始化符的语法类似于C++中的构造函数初始化列表，但C++开发人员要注意，除了语法类似之外，C#初始化符所包含的代码遵循完全不同的规则。可以使用C++初始化列表指定成员变量的初始值，或调用基类构造函数，而C#初始化符中的代码只能调用另一个构造函数。这就要求C#类在构造时遵循严格的顺序，但C++就没有这个要求。这个问题详见第4章，那时就会看到，C#强制遵循的顺序只不过是良好的编程习惯而已。&lt;br /&gt;&lt;br /&gt;3.2.3  只读字段&lt;br /&gt;常量的概念就是一个包含不能修改的值的变量，常量是C#与大多数编程语言共有的。但是，常量不必满足所有的要求。有时可能需要一些变量，其值不应改变，但在运行之前是未知的。C#为这种情形提供了另一个类型的变量：readonly字段。&lt;br /&gt;&lt;br /&gt;readonly关键字比const灵活得多，允许把一个字段设置为常量，但可以执行一些运算，以确定它的初始值。其规则是可以在构造函数中给readonly字段赋值，但不能在其他地方赋值，readonly字段还可以是一个实例字段，而不是静态字段，类的每个实例可以有不同的值，与const字段不同，如果要把readonly字段设置为静态，就必须显式声明。&lt;br /&gt;&lt;br /&gt;如果有一个编辑文档的MDI程序，因为要注册，需要限制可以同时打开的文档数。现在假定要销售该软件的不同版本，而且顾客可以升级它们的版本，以便同时打开更多的文档。显然，不能在源代码中对最大文档数进行硬编码。而是需要一个字段表示这个最大文档数。这个字段必须是只读的——每次安装程序时，从注册表键或其他文件存储中读取。代码如下所示：&lt;br /&gt;&lt;br /&gt;   public class DocumentEditor&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      public static readonly uint MaxDocuments;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      static DocumentEditor()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         MaxDocuments = DosomethingToFindOutMaxNumber();&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;在本例中，字段是静态的，因为每次运行程序的实例时，只需存储最大文档数一次。这就是在静态构造函数中初始化它的原因。如果有readonly字段的一个实例，就要在实例构造函数中初始化它。例如，假定编辑的每个文档都有一个创建日期，但不允许用户修改它(因为这会覆盖过去的日期)。注意，该字段也是公共的，我们不需要把readonly字段设置为私有，因为按照定义，它们不能在外部修改(这个规则也适用于常量)。&lt;br /&gt;&lt;br /&gt;日期用基类System.DateTime的一个实例来表示，前面已经简要介绍过这个基类。在这段代码中，使用带有3个参数的System.DateTime构造函数(年份、月份和月份中的日，您可以从MSDN文档中找到这个构造函数和其他DateTime构造函数的更多信息)。&lt;br /&gt;&lt;br /&gt;   public class Document&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      public readonly DateTime CreationDate;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      public Document()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         // read in creation date from file. Assume result is 1 Jan 2002&lt;br /&gt;&lt;br /&gt;         // but in general this can be different for different instances&lt;br /&gt;&lt;br /&gt;         // of the class  &lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;         CreationDate = new DateTime(2002, 1, 1);&lt;br /&gt;&lt;br /&gt;         }&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;在上面的代码中，CreationDate和MaxDocuments的处理方式与其他字段相同，但因为它们是readonly，所以不能在构造函数外部赋值：&lt;br /&gt;&lt;br /&gt;void SomeMethod()&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   MaxDocuments = 10;       // compilation error here. MaxDocuments is readonly&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;还要注意，在构造函数中不必给readonly 字段赋值，如果没有赋值，它的值就是其数据类型的默认值，或者在声明时给它初始化的值。这适用于静态和实例readonly字段。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-6446418700661781934?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/6446418700661781934/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/03/c2.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/6446418700661781934'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/6446418700661781934'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/03/c2.html' title='c#教程（十二）类成员(2)'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-4074897483110213988</id><published>2009-03-02T16:26:00.000-08:00</published><updated>2009-03-02T16:30:50.565-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、类'/><title type='text'>c#教程（十一）类成员(1)</title><content type='html'>类和结构实际上都是创建对象的模板，每个对象都包含数据，并提供了处理和访问数据的方法。类定义了每个类对象(称为实例)可以包含什么数据和功能。例如，如果一个类表示一个顾客，就可以定义字段CustomerID、FirstName、LastName和Address，以包含该顾客的信息。还可以定义处理存储在这些字段中的数据的功能。接着，就可以实例化这个类的对象，以表示某个顾客，并为这个实例设置这些字段，使用其功能。&lt;br /&gt;&lt;br /&gt;class PhoneCustomer&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   public const string DayOfSendingBill ="Monday";&lt;br /&gt;&lt;br /&gt; public int CustomerID;&lt;br /&gt;&lt;br /&gt;   public string FirstName;&lt;br /&gt;&lt;br /&gt;   public string LastName;&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;结构在内存中的存储方式(类是存储在堆(heap)上的引用类型，而结构是存储在堆栈(stack)上的值类型)、访问方式和一些特征(如结构不支持继承)与类不同。较小的数据类型使用结构可提高性能。但在语法上，结构与类非常相似，主要的区别是使用关键字struct代替class来声明结构。例如，如果希望所有的PhoneCustomer实例都存储在堆栈上，而不是存储在托管堆上，就可以编写下面的语句：&lt;br /&gt;&lt;br /&gt;struct PhoneCustomerStruct&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   public const string DayOfSendingBill = "Monday";&lt;br /&gt;&lt;br /&gt;   public int CustomerID;&lt;br /&gt;&lt;br /&gt;   public string FirstName;&lt;br /&gt;&lt;br /&gt;   public string LastName;&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;对于类和结构，都使用关键字new来声明实例：这个关键字创建对象并对其进行初始化。在下面的例子中，类和结构的字段值都默认为0：&lt;br /&gt;&lt;br /&gt;PhoneCustomer myCustomer = new PhoneCustomer();     //works for a class&lt;br /&gt;&lt;br /&gt;PhoneCustomerStruct myCustomer2 = new PhoneCustomerStruct();   // works for a struct&lt;br /&gt;&lt;br /&gt;在大多数情况下，类要比结构常用得多。因此，我们先讨论类，然后指出类和结构的区别，以及选择使用结构而不使用类的特殊原因。但除非特别说明，否则就可以假定用于类的代码也适用于结构。&lt;br /&gt;&lt;br /&gt;3.2.1  数据成员&lt;br /&gt;数据成员包含了类的数据—— 字段、常量和事件。数据成员可以是静态数据(与整个类相关)或实例数据(类的每个实例都有它自己的数据副本)。通常，对于面向对象的语言，类成员总是实例成员，除非用static进行了显式的声明。&lt;br /&gt;&lt;br /&gt;字段是与类相关的变量。在前面的例子中已经使用了PhoneCustomer类中的字段：&lt;br /&gt;&lt;br /&gt;一旦实例化PhoneCustomer对象后，就可以使用语法Object.FieldName来访问这些字段：&lt;br /&gt;&lt;br /&gt;PhoneCustomer Customer1 = new PhoneCustomer();&lt;br /&gt;&lt;br /&gt;Customer1.FirstName = "Simon";&lt;br /&gt;&lt;br /&gt;常量与类的关联方式同变量与类的关联方式一样。使用const关键字来声明常量。如果它们声明为public，就可以在类的外部访问。&lt;br /&gt;&lt;br /&gt;class PhoneCustomer&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   public const string DayOfSendingBill = "Monday";&lt;br /&gt;&lt;br /&gt;   public int CustomerID;&lt;br /&gt;&lt;br /&gt;   public string FirstName;&lt;br /&gt;&lt;br /&gt;   public string LastName;&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;事件是类的成员，它可以让对象将某些特定行为(例如改变类的字段或属性，或者进行了某种形式的用户交互操作)发生的时间告知调用程序。客户可以包含一些称为事件处理程序的代码来响应该事件。第6章将详细介绍事件。&lt;br /&gt;&lt;br /&gt;3.2.2  函数成员&lt;br /&gt;函数成员提供了操作类中数据的某些功能，包括方法、属性、构造函数和终结器(finalizor)、运算符以及索引器。&lt;br /&gt;&lt;br /&gt;方法是与某个特定类相关的函数，它们可以是实例方法，也可以是静态方法。实例方法处理类的特定实例，静态方法提供了更一般的功能，不需要实例化一个类 (例如前面的Console.WriteLine()方法)。下一节介绍方法。&lt;br /&gt;&lt;br /&gt;属性是与在客户机上用与访问类的公共字段类似的方式访问的函数。C#为读写类上的属性提供了专用语法，所以不必使用那些名称中嵌有Get或Set的偷工减料的方法。因为属性的这种语法不同于一般函数的语法，在客户代码中，虚拟的对象被当做实际的东西。&lt;br /&gt;&lt;br /&gt;构造函数是在实例化对象时自动调用的函数。它们必须与所属的类同名，且不能有返回类型。构造函数可用于在实例化对象时设置字段的值。&lt;br /&gt;&lt;br /&gt;终结器类似于构造函数，但是在CLR检测到不再需要某个对象时调用。它们的名称与类相同，但前面有一个~符号。C++程序员应注意，终结器在C#中比在C++中用得较少一些，因为CLR会自动进行垃圾收集，另外，不可能预测什么时候调用析构函数。第7章将介绍终结器。&lt;br /&gt;&lt;br /&gt;运算符执行的最简单的操作就是+和–。在对两个整数进行相加操作时，严格地说，就是对整数使用+运算符。C#还允许指定把已有的运算符应用于自己的类(运算符重载)。第5章将详细论述运算符。&lt;br /&gt;&lt;br /&gt;索引器允许对象以数组或集合的方式进行索引。第5章介绍索引器。&lt;br /&gt;&lt;br /&gt;1. 方法&lt;br /&gt;在VB、C和C++中，可以定义与某个特定类完全不相关的全局函数，但在C#中不能这样做。在C#中，每个函数都必须与一个类或结构相关。&lt;br /&gt;&lt;br /&gt;注意，正式的C#术语实际上并没有区分函数和方法。在这个术语中，“函数”不仅包含方法，而且也包含类或结构的一些非数据成员。它包括索引器、运算符、构造函数和析构函数等，甚至还有属性。这些都不是数据成员，字段、常量和事件才是数据成员。本章将详细讨论方法。&lt;br /&gt;&lt;br /&gt;(1) 方法的声明&lt;br /&gt;&lt;br /&gt;在C#中，定义方法的语法与C风格的语言相同，与C++和Java中的语法也相同。与C++的主要语法区别是，在C#中，每个方法都单独声明为public或private，不能使用public：块把几个方法定义组合起来。另外，所有的C#方法都在类定义中声明和定义。在C#中，不能像在C++中那样把方法的实现代码分隔开来。&lt;br /&gt;&lt;br /&gt;在C#中，方法的定义包括方法的修饰符(例如方法的可访问性)、返回值的类型，然后是方法名、输入参数的列表(用圆括号括起来)和方法体(用花括号括起来)。&lt;br /&gt;&lt;br /&gt;[modifiers] return_type MethodName([parameters])&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   // Method body&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;每个参数都包括参数类型名及在方法体中的引用名称。但如果方法有返回值，return语句就必须与返回值一起使用，指定出口点，例如：&lt;br /&gt;&lt;br /&gt;public bool IsSquare(Rectangle rect)&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   return (rect.Height == rect.Width);&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;这段代码使用一个表示矩形的.NET基类System.Drawing.Rectangle。&lt;br /&gt;&lt;br /&gt;如果方法没有返回值，就把返回类型指定为void，因为不能省略返回类型。如果方法不带参数，仍需要在方法名的后面写上一对空的圆括号()(就像本章前面的Main()方法)。此时还包含一个可选的return语句—— 当到达右花括号时，方法会自动返回。注意方法可以包含任意多个return语句：&lt;br /&gt;&lt;br /&gt;public bool IsPositive(int value)&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   if (value &lt; 0)&lt;br /&gt;&lt;br /&gt;      return false;&lt;br /&gt;&lt;br /&gt;   return true;&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;(2) 调用方法&lt;br /&gt;&lt;br /&gt;C#中调用方法的语法与C++和Java中的一样，C#和VB的惟一区别是在C#中调用方法时，必须使用圆括号，这要比VB 6中有时需要括号，有时不需要括号的规则简单一些。&lt;br /&gt;&lt;br /&gt;下面的例子MathTest说明了类的定义和实例化、方法的定义和调用的语法。除了包含Main()方法的类之外，它还定义了类MathTest，该类包含两个方法和一个字段。&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;using System;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;namespace Wrox.ProCSharp. MathTestSample&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   class MainEntryPoint&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      static void Main()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         // Try calling some static functions&lt;br /&gt;&lt;br /&gt;         Console.WriteLine("Pi is " + MathTest.GetPi());&lt;br /&gt;&lt;br /&gt;         int x = MathTest.GetSquareOf(5);&lt;br /&gt;&lt;br /&gt;         Console.WriteLine("Square of 5 is " + x);&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;         // Instantiate at MathTest object&lt;br /&gt;&lt;br /&gt;         MathTest math = new MathTest();         // this is C#'s way of&lt;br /&gt;&lt;br /&gt;                                             // instantiating a reference type&lt;br /&gt;&lt;br /&gt;         &lt;br /&gt;&lt;br /&gt;         // Call non-static methods&lt;br /&gt;&lt;br /&gt;         math.value = 30;&lt;br /&gt;&lt;br /&gt;         Console.WriteLine(&lt;br /&gt;&lt;br /&gt;            "Value field of math variable contains " + math.value);&lt;br /&gt;&lt;br /&gt;         Console.WriteLine("Square of 30 is " + math.GetSquare());&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;   // Define a class named MathTest on which we will call a method&lt;br /&gt;&lt;br /&gt;   class MathTest&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      public int value;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      public int GetSquare()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         return value*value;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      public static int GetSquareOf(int x)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         return x*x;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      public static double GetPi()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         return 3.14159;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;运行mathTest示例，会得到如下结果：&lt;br /&gt;&lt;br /&gt;csc MathTest.cs&lt;br /&gt;&lt;br /&gt;Microsoft (R) Visual C# .NET Compiler version 7.10.3052.4&lt;br /&gt;&lt;br /&gt;for Microsoft (R) .NET Framework version 1.1.4322&lt;br /&gt;&lt;br /&gt;Copyright (C) Microsoft Corporation 2001-2002. All rights reserved.&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;MathTest&lt;br /&gt;&lt;br /&gt;Pi is 3.14159&lt;br /&gt;&lt;br /&gt;Square of 5 is 25&lt;br /&gt;&lt;br /&gt;Value field of math variable contains 30&lt;br /&gt;&lt;br /&gt;Square of 30 is 900&lt;br /&gt;&lt;br /&gt;从代码中可以看出，MathTest类包含一个字段和一个方法，该字段包含一个数字，该方法计算数字的平方。这个类还包含两个静态方法，返回pi的值，并把数字的平方作为参数传入。&lt;br /&gt;这个类有一些功能并不是&lt;a href="http://futureangle.blogspot.com"&gt;C#&lt;/a&gt;程序设计的好例子。例如，GetPi()通常作为const字段来执行，而好的设计应使用目前还没有介绍的概念。&lt;br /&gt;&lt;br /&gt;C++和Java开发人员应很熟悉这个语法的大多数内容。如果您有VB的编程经验，就会仅把MathTest类看作一个执行字段和方法的VB类模块。&lt;br /&gt;&lt;br /&gt;(3) 给方法传递参数&lt;br /&gt;&lt;br /&gt;参数可以通过引用或值传递给方法。在变量通过引用传递给方法时，被调用的方法得到的就是这个变量，所以在方法内部对变量进行的任何改变在方法退出后仍旧发挥作用。而如果变量是通过值传送给方法的，被调用的方法得到的是变量的初始副本，也就是说，在方法退出后，对变量进行的修改会丢失。对于复杂的数据类型，按引用传递的效率更高，因为在按值传递时，必须复制大量的数据。&lt;br /&gt;&lt;br /&gt;在C#中，所有的参数都是通过值来传递的，除非特别说明。这与C++是相同的，但与VB不同。但是，在理解引用类型的传递过程时需要注意。因为引用类型的对象只包含对象的引用，它们只给方法传递这个引用，而不是对象本身，所以对底层对象的修改会保留下来。相反，值类型的对象包含的是实际数据，所以传递给方法的是数据本身的副本。例如，int通过值传递给方法，方法对该int的值所作的任何改变都没有改变原int对象的值。但如果数组或其他引用类型(如类)传递给方法后，方法会使用该引用改变这个数组中的值，而新值会反射到原来的数组对象上。&lt;br /&gt;&lt;br /&gt;下面的例子ParameterTest.cs说明了这一点：&lt;br /&gt;&lt;br /&gt;using System;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;namespace Wrox.ProCSharp. ParameterTestSample&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   class ParameterTest&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      static void SomeFunction(int[] ints, int i)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         ints[0] = 100;&lt;br /&gt;&lt;br /&gt;         i = 100;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   &lt;br /&gt;&lt;br /&gt;      public static int Main()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         int i = 0;&lt;br /&gt;&lt;br /&gt;         int[] ints = { 0, 1, 2, 4, 8 };&lt;br /&gt;&lt;br /&gt;         // Display the original values&lt;br /&gt;&lt;br /&gt;         Console.WriteLine("i = " + i);&lt;br /&gt;&lt;br /&gt;         Console.WriteLine("ints[0] = " + ints[0]);&lt;br /&gt;&lt;br /&gt;         Console.WriteLine("Calling SomeFunction...");&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;         // After this method returns, ints will be changed,&lt;br /&gt;&lt;br /&gt;         // but i will not&lt;br /&gt;&lt;br /&gt;         SomeFunction(ints, i);&lt;br /&gt;&lt;br /&gt;         Console.WriteLine("i = " + i);&lt;br /&gt;&lt;br /&gt;         Console.WriteLine("ints[0] = " + ints[0]);&lt;br /&gt;&lt;br /&gt;         return 0;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;结果如下：&lt;br /&gt;&lt;br /&gt;csc ParameterTest.cs&lt;br /&gt;&lt;br /&gt;Microsoft (R) Visual C# .NET Compiler version 7.10.3052.4&lt;br /&gt;&lt;br /&gt;for Microsoft (R) .NET Framework version 1.1.4322&lt;br /&gt;&lt;br /&gt;Copyright (C) Microsoft Corporation 2001-2002. All rights reserved.&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;ParameterTest&lt;br /&gt;&lt;br /&gt;i = 0&lt;br /&gt;&lt;br /&gt;ints[0] = 0&lt;br /&gt;&lt;br /&gt;Calling SomeFunction...&lt;br /&gt;&lt;br /&gt;i = 0&lt;br /&gt;&lt;br /&gt;ints[0] = 100&lt;br /&gt;&lt;br /&gt;注意，i的值保持不变，而在ints中改变的值在原来的数组中也改变了。&lt;br /&gt;&lt;br /&gt;注意字符串是不同的，因为字符串是不能改变的(如果改变字符串的值，就会创建一个全新的字符串)，所以字符串无法显示一般引用类型的行为方式。在方法调用中，对字符串所作的任何改变都不会影响原来的字符串。这一点将在第8章详细讨论。&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;(4) ref参数&lt;br /&gt;&lt;br /&gt;通过值传送变量是默认的情况，也可以迫使值参数通过引用传送给方法。为此，要使用ref关键字。如果把一个参数传递给方法，且这个方法的输入参数前带有ref关键字，则该方法对变量所作的任何改变都会影响原来对象的值：&lt;br /&gt;&lt;br /&gt;static void SomeFunction(int[] ints, ref int i)&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   ints[0] = 100;&lt;br /&gt;&lt;br /&gt;   i = 100;       //the change to i will persist after SomeFunction() exits&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;在调用该方法时，还需要添加ref关键字：&lt;br /&gt;&lt;br /&gt;SomeFunction(ints, ref i);&lt;br /&gt;&lt;br /&gt;在C#中添加ref关键字等同于在C++中使用&amp;语法指定按引用传递参数。但是，C#在调用方法时要求使用ref关键字，使操作更明确(因此有助于防止错误)。&lt;br /&gt;&lt;br /&gt;最后，C#仍要求对传递给方法的参数进行初始化，理解这一点也是非常重要的。在传递给方法之前，无论是按值传递，还是按引用传递，任何变量都必须初始化。&lt;br /&gt;&lt;br /&gt;(5) out关键字&lt;br /&gt;&lt;br /&gt;在C风格的语言中，函数能从一个例程中输出多个值，这种情况很常见。这是使用输出参数实现的——方法是把输出值赋给通过引用传递给方法的变量。通常，变量通过引用传送的初值是不重要的，这些值会被函数重写，函数甚至从来没有使用过它们。&lt;br /&gt;&lt;br /&gt;如果可以在C#中使用这种相同的约定，就会非常方便。但C#要求变量在被引用前必须用一个初值进行初始化。在把输入变量传递给函数前，可以用没有意义的值初始化它们，函数将使用真实、有意义的值初始化它们，这样做是没有必要的，有时甚至会引起混乱。但有一种方法能够简化C#编译器所坚持的输入参数的初始化。&lt;br /&gt;&lt;br /&gt;编译器使用out关键字来初始化。当在方法的输入参数前面加上out关键字时，传递给该方法的变量可以不被初始值初始化。该变量通过引用被传送，所以在从被调用的方法中返回时，方法对该变量进行的任何改变都会被保留下来。在调用该方法时，还需要使用out关键字，这正如在定义该方法时一样：&lt;br /&gt;&lt;br /&gt;static void SomeFunction(out int i)&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   i = 100;&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;public static int Main()&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   int i; // note how i is declared but not initialized&lt;br /&gt;&lt;br /&gt;   SomeFunction(out i);&lt;br /&gt;&lt;br /&gt;   Console.WriteLine(i);&lt;br /&gt;&lt;br /&gt;   return 0;&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;out关键字是C#中的新增内容，在VB和C++中没有对应的关键字，该关键字的引入使C#更安全，更不容易出错。如果在函数体中没有给out参数分配一个值，那么该方法就不能编译。&lt;br /&gt;&lt;br /&gt;(6) 方法的重载&lt;br /&gt;&lt;br /&gt;C#支持方法的重载——方法的几个有不同签名(名称、参数个数、参数类型)的版本，但不支持C++或VB中的默认参数。为了重载方法，只需声明同名但参数个数或类型不同的方法即可：&lt;br /&gt;&lt;br /&gt;class ResultDisplayer&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   void DisplayResult(string result)&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      // implementation&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;   void DisplayResult(int result)&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      // implementation&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;因为C#不直接支持可选参数，所以需要使用方法重载来达到此目的：&lt;br /&gt;&lt;br /&gt;class MyClass&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   int DoSomething(int x)   // want 2nd parameter with default value 10&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      DoSomething(x, 10);&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;   int DoSomething(int x, int y)&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      // implementation&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;在任何语言中，对于方法重载来说，如果调用了错误的重载方法，就有可能出现运行错误。第4章将讨论如何使代码避免这些错误。现在，知道C#在重载方法的参数方面有一些小区别即可：&lt;br /&gt;&lt;br /&gt;●       两个方法不能仅在返回类型上有区别。&lt;br /&gt;&lt;br /&gt;●       两个方法不能仅根据参数是声明为ref还是out来区分。&lt;br /&gt;&lt;br /&gt;2. 属性&lt;br /&gt;属性(property)不太常见，px 的概念是：它是一个方法或一对方法，从在客户机代码来看，它们是一个字段。例如Windows窗体的Height属性。假定有下面的代码：&lt;br /&gt;&lt;br /&gt;// mainForm is of type System.Windows.Form&lt;br /&gt;&lt;br /&gt;mainForm.Height = 400;&lt;br /&gt;&lt;br /&gt;执行这段代码，窗口的高度设置为400，因此窗口会在屏幕上重新设置大小。在语法上，上面的代码类似于设置一个字段，但实际上是调用了包含代码的属性访问器(accessor)重新设置了窗体的大小。&lt;br /&gt;&lt;br /&gt;在C#中定义属性，可以使用下面的语法：&lt;br /&gt;&lt;br /&gt;public string SomeProperty&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   get&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      return "This is the property value";&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;   set&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      // do whatever needs to be done to set the property&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;get访问器不带参数，且必须返回为属性声明的类型。也不应为set访问器指定任何显式参数，但编译器假定它带一个参数，其类型也与属性相同，并表示为value。例如，下面的代码包含一个属性ForeName，它设置了一个字段foreName，该字段有一个长度限制。&lt;br /&gt;&lt;br /&gt;private string foreName;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;public string ForeName&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   get&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      return foreName;&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;   set&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      if (value.Length &gt; 20)&lt;br /&gt;&lt;br /&gt;         // code here to take error recovery action &lt;br /&gt;&lt;br /&gt;         // (eg. throw an exception)&lt;br /&gt;&lt;br /&gt;      else&lt;br /&gt;&lt;br /&gt;         foreName = value;&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;注意这里的命名模式。我们采用C#的区分大小写模式，使用相同的名称，但公共属性采用Pascal大小写命名规则，而私有属性采用camel大小写命名规则。这是标准的命名方式。一些开发人员喜欢使用前面有下划线的字段名_foreName，这会为识别字段提供极大的便利。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-4074897483110213988?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/4074897483110213988/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/03/c1.html#comment-form' title='2 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/4074897483110213988'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/4074897483110213988'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/03/c1.html' title='c#教程（十一）类成员(1)'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-9176918266442866395</id><published>2009-03-01T18:52:00.000-08:00</published><updated>2009-03-01T18:54:50.718-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、预处理器指令'/><title type='text'>c#教程（十）C#预处理器指令</title><content type='html'>预处理器指令的开头都有符号#。&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;C++开发人员应知道在C和C++中，预处理器指令是非常重要的，但是，在C#中，并没有那么多的预处理器指令，它们的使用也不太频繁。C#提供了其他机制来实现许多C++指令的功能，例如定制特性。还要注意，C#并没有一个像C++那样的独立预处理器，所谓的预处理器指令实际上是由编译器处理的。尽管如此，C#仍保留了一些预处理器指令，因为这些命令对预处理器有一定的影响。&lt;br /&gt;&lt;br /&gt;下面简要介绍预处理器指令的功能。&lt;br /&gt;&lt;br /&gt;2.13.1  #define和 #undef&lt;br /&gt;#define的用法如下所示：&lt;br /&gt;&lt;br /&gt;#define DEBUG&lt;br /&gt;&lt;br /&gt;它告诉编译器存在给定名称的符号，在本例中是DEBUG。这有点类似于声明一个变量，但这个变量并没有真正的值，只是存在而已。这个符号不是实际代码的一部分，而只在编译器编译代码时存在。在C#代码中它没有任何意义。&lt;br /&gt;&lt;br /&gt;#undef正好相反—— 删除符号的定义：&lt;br /&gt;&lt;br /&gt;#undef DEBUG&lt;br /&gt;&lt;br /&gt;如果符号不存在，#undef就没有任何作用。同样，如果符号已经存在，#define也不起作用。&lt;br /&gt;&lt;br /&gt;必须把#define和#undef命令放在C#源代码的开头，在声明要编译的任何对象的代码之前。&lt;br /&gt;&lt;br /&gt;#define本身并没有什么用，但当与其他预处理器指令(特别是#if)结合使用时，它的功能就非常强大了。&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;这里应注意一般的C#语法的一些变化。预处理器指令不用分号断开，一般是一行上只有一个命令。这是因为对于预处理器指令，C#不再要求命令用分号断开。如果它遇到一个预处理器指令，就会假定下一个命令在下一行上。&lt;br /&gt;&lt;br /&gt;2.13.2  #if, #elif, #else和 #endif&lt;br /&gt;这些指令告诉编译器是否要编译某个代码块。考虑下面的方法：&lt;br /&gt;&lt;br /&gt;   int DoSomeWork(double x)&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      // do something&lt;br /&gt;&lt;br /&gt;      #if DEBUG&lt;br /&gt;&lt;br /&gt;         Console.WriteLine("x is " + x);&lt;br /&gt;&lt;br /&gt;      #endif&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;这段代码会像往常那样编译，但Console.WriteLine命令被包含在#if子句内。这行代码只有在前面的#define命令定义了符号DEBUG后才执行。当编译器遇到#if语句后，将先检查相关的符号是否存在，如果符号存在，就只编译#if块中的代码。否则，编译器会忽略所有的代码，直到遇到匹配的#endif指令为止。一般是在调试时定义符号DEBUG，把不同的调试相关代码放在#if子句中。在完成了调试后，就把#define语句注释掉，所有的调试代码会奇迹般地消失，可执行文件也会变小，最终用户不会被这些调试信息弄糊涂(显然，要做更多的测试，确保代码在没有定义DEBUG的情况下也能工作)。这项技术在C和C++编程中非常普通，称为条件编译(conditional compilation)。&lt;br /&gt;&lt;br /&gt;#elif (=else if)和#else指令可以用在#if块中，其含义非常直观，它也可以嵌套#if块：&lt;br /&gt;&lt;br /&gt;#define ENTERPRISE&lt;br /&gt;&lt;br /&gt;#define W2K&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;// further on in the file&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;#if ENTERPRISE&lt;br /&gt;&lt;br /&gt;   // do something&lt;br /&gt;&lt;br /&gt;   #if W2K&lt;br /&gt;&lt;br /&gt;      // some code that is only relevant to enterprise&lt;br /&gt;&lt;br /&gt;      // edition running on W2K&lt;br /&gt;&lt;br /&gt;   #endif&lt;br /&gt;&lt;br /&gt;#elif PROFESSIONAL&lt;br /&gt;&lt;br /&gt;   // do something else&lt;br /&gt;&lt;br /&gt;#else&lt;br /&gt;&lt;br /&gt;   // code for the leaner version&lt;br /&gt;&lt;br /&gt;#endif&lt;br /&gt;&lt;br /&gt;注意：&lt;br /&gt;&lt;br /&gt;与C++中的情况不同，使用#if不是条件编译代码的惟一方式，&lt;a href="http://futureangle.blogspot.com"&gt;C#&lt;/a&gt;还提供了另一种利用Conditional特性的机制，详见第10章。&lt;br /&gt;&lt;br /&gt;#if和 #elif还支持一组逻辑运算符!、==、!=和 ||。如果符号存在，就被认为是true，否则为false，例如：&lt;br /&gt;&lt;br /&gt;#if W2K &amp;&amp; (ENTERPRISE==false)   // if W2K is defined but ENTERPRISE isn't&lt;br /&gt;&lt;br /&gt;2.13.3  #warning和 # error&lt;br /&gt;另外两个非常有用的预处理器指令是#warning和#error，当编译器遇到它们时，会分别产生一个警告或错误。如果编译器遇到#warning指令，会给用户显示#warning指令后面的文本，之后编译继续进行。如果编译器遇到#error指令，就会给用户显示后面的文本，作为一个编译错误信息，然后会立即退出编译，不会生成IL代码。&lt;br /&gt;&lt;br /&gt;使用这两个指令可以检查#define语句是不是做错了什么事，使用#warning语句可以让自己想起做过什么事：&lt;br /&gt;&lt;br /&gt;#if DEBUG &amp;&amp; RELEASE&lt;br /&gt;&lt;br /&gt;   #error "You've defined DEBUG and RELEASE simultaneously! "&lt;br /&gt;&lt;br /&gt;#endif&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;#warning "Don't forget to remove this line before the boss tests the code! "&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("*I hate this job*");&lt;br /&gt;&lt;br /&gt;2.13.4  #region和#endregion&lt;br /&gt;#region和 #endregion指令用于把一段代码标记为有给定名称的一个块，如下所示。&lt;br /&gt;&lt;br /&gt;#region Member Field Declarations&lt;br /&gt;&lt;br /&gt;   int x;&lt;br /&gt;&lt;br /&gt;   double d;&lt;br /&gt;&lt;br /&gt;   Currency balance;&lt;br /&gt;&lt;br /&gt;#endregion&lt;br /&gt;&lt;br /&gt;这看起来似乎没有什么用，它不影响编译过程。这些指令的优点是它们可以被某些编辑器所识别，包括Visual Studio .NET编辑器。这些编辑器可以使用这些指令使代码在屏幕上更好地布局。第12章介绍Visual Studio .NET时会详细介绍它们。&lt;br /&gt;&lt;br /&gt;2.13.5  #line&lt;br /&gt;#line指令可以用于改变编译器在警告和错误信息中显示的文件名和行号信息。这个指令用得并不多。如果编写代码时，在把代码发送给编译器前，要使用某些软件包改变键入的代码，就可以使用这个指令，因为这意味着编译器报告的行号或文件名与文件中的行号或编辑的文件名不匹配。#line指令可以用于恢复这种匹配。也可以使用语法#line default把行号恢复为默认的行号：&lt;br /&gt;&lt;br /&gt;#line 164 "Core.cs"   // we happen to know this is line 164 in the file&lt;br /&gt;&lt;br /&gt;                   // Core.cs, before the intermediate&lt;br /&gt;&lt;br /&gt;                   // package mangles it.&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;// later on&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;#line default      // restores default line numbering&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-9176918266442866395?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/9176918266442866395/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/03/cc.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/9176918266442866395'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/9176918266442866395'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/03/cc.html' title='c#教程（十）C#预处理器指令'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-7585305260115466490</id><published>2009-03-01T18:50:00.000-08:00</published><updated>2009-03-01T18:52:26.869-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、控制台、I/O'/><title type='text'>c#教程（九）控制台I/O</title><content type='html'>要从控制台窗口读取一行文本，可以使用Console.ReadLine()方法，它会从控制台窗口读取一个输入流(在用户按下回车键时停止)，并返回输入的字符串。写入控制台也有两个对应的方法，前面已经使用过它们：&lt;br /&gt;&lt;br /&gt;●       Console. Write()方法将指定的值写入控制台窗口。&lt;br /&gt;&lt;br /&gt;●       Console.WriteLine()方法类似，但在输出结果的最后添加一个换行符。&lt;br /&gt;&lt;br /&gt;所有预定义类型(包括object)的这些函数都有各种形式(重载)，所以在大多数情况下，在显示值之前不必把它们转换为字符串。&lt;br /&gt;&lt;br /&gt;例如，下面的代码允许用户输入一行文本，并显示该文本：&lt;br /&gt;&lt;br /&gt;string s = Console.ReadLine();&lt;br /&gt;&lt;br /&gt;Console.WriteLine(s);&lt;br /&gt;&lt;br /&gt;Console.WriteLine()还允许用与C的printf函数类似的方式显示格式化的结果。要以这种方式使用WriteLine()，应传入许多参数。第一个参数是花括号中包含标记的字符串，在这个花括号中，要把后续的参数插入到文本中。每个标记都包含一个基于0的索引，表示列表中参数的序号。例如，"{0}"表示列表中的第一个参数，所以下面的代码：&lt;br /&gt;&lt;br /&gt;int i = 10;&lt;br /&gt;&lt;br /&gt;int j = 20;&lt;br /&gt;&lt;br /&gt;Console.WriteLine("{0} plus {1} equals {2}", i, j, i + j);&lt;br /&gt;&lt;br /&gt;会显示：&lt;br /&gt;&lt;br /&gt;10 plus 20 equals 30&lt;br /&gt;&lt;br /&gt;也可以为值指定宽度，调整文本在该宽度中的位置，正值表示右对齐，负值表示左对齐。为此可以使用格式{n,w}，其中n是参数索引，w是宽度值。&lt;br /&gt;&lt;br /&gt;int i = 940;&lt;br /&gt;&lt;br /&gt;int j = 73;&lt;br /&gt;&lt;br /&gt;Console.WriteLine(" {0,4}\n+{1,4}\n––––\n {2,4}", i, j, i + j);&lt;br /&gt;&lt;br /&gt;结果如下：&lt;br /&gt;&lt;br /&gt;  940&lt;br /&gt;&lt;br /&gt;+  73&lt;br /&gt;&lt;br /&gt; ––––&lt;br /&gt;&lt;br /&gt; 1013&lt;br /&gt;&lt;br /&gt;最后，还可以添加一个格式字符串，和一个可选的精度值。这里没有列出可用格式字符串的完整列表，因为如第8章所述，我们可以定义自己的格式字符串。但用于预定义类型的主要格式字符串如表2-14所示。&lt;br /&gt;&lt;br /&gt;表  2-14&lt;br /&gt;&lt;br /&gt;字  符  串&lt;br /&gt; 说    明&lt;br /&gt; &lt;br /&gt;C&lt;br /&gt; 本地货币格式&lt;br /&gt; &lt;br /&gt;D&lt;br /&gt; 十进制格式，把整数转换为以10为基数的数，如果给定一个精度说明符，就加上前导0&lt;br /&gt; &lt;br /&gt;E&lt;br /&gt; 科学计数法(指数)格式。精度说明符设置小数位数(默认情况下为6)。格式字符串的大小写("e" 或 "E")确定指数符号的大小写&lt;br /&gt; &lt;br /&gt;F&lt;br /&gt; 固定点格式，精度说明符设置小数位数，可以为0&lt;br /&gt; &lt;br /&gt;G&lt;br /&gt; 普通格式，使用E 或 F格式取决于哪种格式最简单&lt;br /&gt; &lt;br /&gt;N&lt;br /&gt; 数字格式，用逗号表示千分符，例如32 767.44&lt;br /&gt; &lt;br /&gt;P&lt;br /&gt; 百分数格式&lt;br /&gt; &lt;br /&gt;X&lt;br /&gt; 16进制格式，精度说明符用于加上前导0&lt;br /&gt; &lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;注意格式字符串都不需要考虑大小写，除e/E之外。&lt;br /&gt;&lt;br /&gt;如果要使用格式字符串，应把它放在给出参数个数和字段宽度的标记后面，并用一个冒号把它们分隔开。例如，要把decimal值格式化为货币格式，且使用计算机上的地区设置，其精度为两位小数，则使用C2：&lt;br /&gt;&lt;br /&gt;decimal i = 940.23m;&lt;br /&gt;&lt;br /&gt;decimal j = 73.7m;&lt;br /&gt;&lt;br /&gt;Console.WriteLine(" {0,9:C2}\n+{1,9:C2}\n ––––––\n {2,9:C2}", i, j, i + j);&lt;br /&gt;&lt;br /&gt;在美国，其结果是：&lt;br /&gt;&lt;br /&gt;   $940.23&lt;br /&gt;&lt;br /&gt;+   $73.70&lt;br /&gt;&lt;br /&gt;–––––––&lt;br /&gt;&lt;br /&gt; $1,013.93&lt;br /&gt;&lt;br /&gt;最后一个技巧是，可以使用占位符来代替这些格式字符串，例如：&lt;br /&gt;&lt;br /&gt;double d = 0.234;&lt;br /&gt;&lt;br /&gt;Console.WriteLine("{0:#.00}", d);&lt;br /&gt;&lt;br /&gt;其结果为0.23，因为如果在符号(#)的位置上没有字符，就会忽略该符号(#)，如果0的位置上有一个字符，就用这个字符代替0，否则就显示0。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-7585305260115466490?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/7585305260115466490/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/03/cio.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/7585305260115466490'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/7585305260115466490'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/03/cio.html' title='c#教程（九）控制台I/O'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-1525573509910859385</id><published>2009-02-26T17:17:00.000-08:00</published><updated>2009-02-26T17:19:08.521-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、Main'/><title type='text'>c#教程（九）Main()方法</title><content type='html'>C#程序是从方法Main()开始执行的。这个方法必须是类或结构的静态方法，并且其返回类型必须是int或void。&lt;br /&gt;&lt;br /&gt;虽然显式指定public修饰符是很常见的，因为按照定义，必须在程序外部调用该方法，但我们给该方法指定什么访问级别并不重要，即使把该方法标记为private，它也可以运行。&lt;br /&gt;&lt;br /&gt;2.9.1  多个Main()方法&lt;br /&gt;在编译C#控制台或Windows应用程序时，默认情况下，编译器会在与上述名称列表相匹配的任何类中查找Main方法，并使这个类方法成为程序的入口。如果有多个Main方法，编译器就会返回一个错误，例如，考虑下面的代码MainExample.cs：&lt;br /&gt;&lt;br /&gt;using System;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;namespace Wrox.ProCSharp.Basics&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   class Client&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      public static int Main()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         MathExample.Main();&lt;br /&gt;&lt;br /&gt;         return 0;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;   class MathExample&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      static int Add(int x, int y)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         return x + y;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;      public static int Main()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         int i = Add(5,10);&lt;br /&gt;&lt;br /&gt;         Console.WriteLine(i);&lt;br /&gt;&lt;br /&gt;         return 0;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;上述代码中包含两个类，它们都有一个Main()方法。如果按照通常的方式编译这段代码，就会得到下述错误：&lt;br /&gt;&lt;br /&gt;csc MainExample.cs&lt;br /&gt;&lt;br /&gt;Microsoft (R) Visual C# .NET Compiler version 7.10.3052.4&lt;br /&gt;&lt;br /&gt;for Microsoft (R) .NET Framework version 1.1.4322&lt;br /&gt;&lt;br /&gt;Copyright (C) Microsoft Corporation 2001-2002. All rights reserved.&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;MainExample.cs(7,25): error CS0017: Program 'MainExample.exe' has more than one entry point defined: 'Wrox.ProCSharp.Basics.Client.Main()'&lt;br /&gt;&lt;br /&gt;MainExample.cs(21,25): error CS0017: Program 'MainExample.exe' has more than one entry point defined: 'Wrox.ProCSharp.Basics.MathExample.Main()'&lt;br /&gt;&lt;br /&gt;但是，可以使用/main选项，其后跟Main()方法所属类的全名(包括命名空间)，显式地告诉编译器把哪个方法作为程序的入口点：&lt;br /&gt;&lt;br /&gt;csc MainExample.cs /main:Wrox.ProCSharp.Basics.MathExample&lt;br /&gt;&lt;br /&gt;2.9.2  给Main()方法传送参数&lt;br /&gt;到目前为止，我们的例子只介绍了不带参数的Main()方法。但在调用程序时，可以让CLR包含一个参数，将命令行参数转送给程序。这个参数是一个字符串数组，传统称为args(但&lt;a href="http://futureangle.blogspot.com/2009/02/adonet.html"&gt;C#&lt;/a&gt;可以接受任何名称)。在启动程序时，可以使用这个数组，访问通过命令行传送过来的选项。&lt;br /&gt;&lt;br /&gt;下面的例子ArgsExample.cs是在传送给Main方法的字符串数组中迭代，并把每个选项的值写入控制台窗口：&lt;br /&gt;&lt;br /&gt;using System;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;namespace Wrox.ProCSharp.Basics&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   class ArgsExample&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      public static int Main(string[] args)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         for (int i = 0; i &lt; args.Length; i++)&lt;br /&gt;&lt;br /&gt;         {&lt;br /&gt;&lt;br /&gt;            Console.WriteLine(args[i]);&lt;br /&gt;&lt;br /&gt;         }&lt;br /&gt;&lt;br /&gt;         return 0;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;通常使用命令行就可以编译这段代码。在运行编译好的可执行文件时，可以在程序名的后面加上参数，例如：&lt;br /&gt;&lt;br /&gt;ArgsExample /a /b /c&lt;br /&gt;&lt;br /&gt;/a&lt;br /&gt;&lt;br /&gt;/b&lt;br /&gt;&lt;br /&gt;/c&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-1525573509910859385?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/1525573509910859385/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/02/cmain.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/1525573509910859385'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/1525573509910859385'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/02/cmain.html' title='c#教程（九）Main()方法'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-4343787826692061667</id><published>2009-02-26T17:14:00.000-08:00</published><updated>2009-02-26T17:16:18.253-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、命名空间'/><title type='text'>c#教程（八）命名空间</title><content type='html'>如前所述，命名空间提供了一种组织相关类和其他类型的方式。与文件或组件不同，命名空间是一种逻辑组合，而不是物理组合。在C#文件中定义类时，可以把它包括在命名空间定义中。以后，在定义另一个类，在另一个文件中执行相关操作时，就可以在同一个命名空间中包含它，创建一个逻辑组合，告诉使用类的其他开发人员这两个类是如何相关的以及如何使用它们：&lt;br /&gt;&lt;br /&gt;namespace CustomerPhoneBookApp&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   using System;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;   public struct Subscriber&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      // Code for struct here...&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;把一个类型放在命名空间中，可以有效地给这个类型指定一个较长的名称，该名称包括类型的命名空间，后面是句点(.)和类的名称。在上面的例子中，Subscriber结构的全名是CustomerPhoneBookApp.Subscriber。这样，有相同短名的不同的类就可以在同一个程序中使用了。&lt;br /&gt;&lt;br /&gt;也可以在命名空间中嵌套其他命名空间，为类型创建层次结构：&lt;br /&gt;&lt;br /&gt;namespace Wrox&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   namespace ProCSharp&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      namespace Basics&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         class NamespaceExample&lt;br /&gt;&lt;br /&gt;         {&lt;br /&gt;&lt;br /&gt;            // Code for the class here...&lt;br /&gt;&lt;br /&gt;         }&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;每个命名空间名都由它所在命名空间的名称组成，这些名称用句点分隔开，首先是最外层的命名空间，最后是它自己的短名。所以ProfessionalCSharp命名空间的全名是Wrox.ProCSharp，NamespaceExample类的全名是Wrox.ProCSharp.Basics.NamespaceExample。&lt;br /&gt;&lt;br /&gt;使用这个语法也可以组织自己的命名空间定义中的命名空间，所以上面的代码也可以写为：&lt;br /&gt;&lt;br /&gt;namespace Wrox.ProCSharp.Basics&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   class NamespaceExample&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      // Code for the class here...&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;注意不允许在另一个嵌套的命名空间中声明多部分的命名空间。&lt;br /&gt;&lt;br /&gt;命名空间与程序集无关。同一个程序集中可以有不同的命名空间，也可以在不同的程序集中定义同一个命名空间中的类型。&lt;br /&gt;&lt;br /&gt;2.8.1  using语句&lt;br /&gt;显然，命名空间相当长，键入起来很繁琐，用这种方式指定某个特定的类也是不必要的。如本章开头所述，C#允许简写类的全名。为此，要在文件的顶部列出类的命名空间，前面加上using关键字。在文件的其他地方，就可以使用其类型名称来引用命名空间中的类型了：&lt;br /&gt;&lt;br /&gt;using System;&lt;br /&gt;&lt;br /&gt;using Wrox.ProCSharp;&lt;br /&gt;&lt;br /&gt;如前所述，所有的&lt;a href="http://futureangle.blogspot.com/2009/02/aspnetinherits.html"&gt;C#&lt;/a&gt;源代码都以语句using System;开头，这仅是因为Microsoft提供的许多有用的类都包含在System命名空间中。&lt;br /&gt;&lt;br /&gt;如果using指令引用的两个命名空间包含同名的类，就必须使用完整的名称(或者至少较长的名称)，确保编译器知道访问哪个类型，例如，类NamespaceExample同时存在于Wrox.ProCSharp.Basics和Wrox.ProCSharp.OOP命名空间中，如果要在命名空间Wrox.ProCSharp中创建一个类Test，并在该类中实例化一个NamespaceExample类，就需要指定使用哪个类：&lt;br /&gt;&lt;br /&gt;using Wrox.ProCSharp;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;class Test&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   public static int Main()&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      Basics.NamespaceExample nSEx = new Basics.NamespaceExample();&lt;br /&gt;&lt;br /&gt;      //do something with the nSEx variable&lt;br /&gt;&lt;br /&gt;      return 0;&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}   &lt;br /&gt;&lt;br /&gt;因为using语句在C#文件的开头，C和C++也把＃i nclude放在这里，所以从C++迁移到C#的程序员常把命名空间与C++风格的头文件相混淆。不要犯这种错误，using语句在这些文件之间并没有真正建立物理链接。C#也没有对应于C++头文件的部分。&lt;br /&gt;&lt;br /&gt;公司应花一定的时间开发一种命名空间模式，这样其开发人员才能快速定位他们所需要的功能，而且公司内部使用的类名也不会与外部的类库相冲突。本章后面将介绍建立命名空间模式的规则和其他命名约定。&lt;br /&gt;&lt;br /&gt;2.8.2  命名空间的别名&lt;br /&gt;using关键字的另一个用途是给类和命名空间指定别名。如果命名空间的名称非常长，又要在代码中使用多次，但不希望该命名空间的名称包含在using指令中(例如，避免类名冲突)，就可以给该命名空间指定一个别名，其语法如下：&lt;br /&gt;&lt;br /&gt;using alias = NamespaceName;&lt;br /&gt;&lt;br /&gt;下面的例子(前面例子的修订版本)给Wrox.ProCSharp.Basics命名空间指定别名Introduction，并使用这个别名实例化了一个NamespaceExample对象，这个对象是在该命名空间中定义的。它有一个方法GetNamespace()，该方法调用每个类都有的GetType()方法，以访问表示类的类型的Type对象。下面使用这个对象来返回类的命名空间名：&lt;br /&gt;&lt;br /&gt;using System;&lt;br /&gt;&lt;br /&gt;using Introduction = Wrox.ProCSharp.Basics;&lt;br /&gt;&lt;br /&gt;class Test&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   public static int Main()&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      Introduction.NamespaceExample NSEx =&lt;br /&gt;&lt;br /&gt;         new Introduction.NamespaceExample();&lt;br /&gt;&lt;br /&gt;      Console.WriteLine(NSEx.GetNamespace());&lt;br /&gt;&lt;br /&gt;      return 0;&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}   &lt;br /&gt;&lt;br /&gt;   &lt;br /&gt;&lt;br /&gt;namespace Wrox.ProCSharp.Basics&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   class NamespaceExample&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      public string GetNamespace()&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         return this.GetType().Namespace;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-4343787826692061667?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/4343787826692061667/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/02/c_26.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/4343787826692061667'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/4343787826692061667'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/02/c_26.html' title='c#教程（八）命名空间'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-8329919625051091499</id><published>2009-02-25T17:25:00.000-08:00</published><updated>2009-02-25T17:26:25.768-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、数组'/><title type='text'>c#教程（七）数组</title><content type='html'>在声明C#中的数组时，要在各个元素的变量类型后面，加上一组方括号(注意数组中的所有元素必须有相同的数据类型)。&lt;br /&gt;&lt;br /&gt;提示：&lt;br /&gt;&lt;br /&gt;VB用户注意，C#中的数组使用方括号，而不是圆括号。C++用户很熟悉方括号，但应仔细查看这里给出的代码，因为声明数组变量的C#语法与C++语法并不相同。&lt;br /&gt;&lt;br /&gt;例如，int表示一个整数，而int[]表示一个整型数组：&lt;br /&gt;&lt;br /&gt;int[] integers;&lt;br /&gt;&lt;br /&gt;要初始化特定大小的数组，可以使用new关键字，在类型名后面的方括号中给出大小：&lt;br /&gt;&lt;br /&gt;// Create a new array of 32 ints&lt;br /&gt;&lt;br /&gt;int[] integers = new int[32];&lt;br /&gt;&lt;br /&gt;所有的数组都是引用类型，并遵循引用的语义。因此，即使各个元素都是基本的值类型，integers数组也是引用类型。如果以后编写如下代码：&lt;br /&gt;&lt;br /&gt;int[] copy = integers;&lt;br /&gt;&lt;br /&gt;该代码也只是把变量copy指向同一个数组，而不是创建一个新数组。&lt;br /&gt;&lt;br /&gt;要访问数组中的单个元素，可以使用通常的语法，在数组名的后面，把元素的下标放在方括号中。所有的C#数组都使用基于0的下标方式，所以要用下标0引用第一个变量：&lt;br /&gt;&lt;br /&gt;integers[0] = 35;&lt;br /&gt;&lt;br /&gt;同样，用下标值31引用有32个元素的数组中的最后一个元素：&lt;br /&gt;&lt;br /&gt;integers[31] = 432;&lt;br /&gt;&lt;br /&gt;&lt;a href="http://futureangle.blogspot.com"&gt;C#&lt;/a&gt;的数组语法也非常灵活，实际上，C#可以在声明数组时不进行初始化，这样以后就可以在程序中动态地指定其大小。利用这项技术，可以创建一个空引用，以后再使用new关键字把这个引用指向请求动态分配的内存位置：&lt;br /&gt;&lt;br /&gt;int[] integers;&lt;br /&gt;&lt;br /&gt;integers = new int[32];&lt;br /&gt;&lt;br /&gt;可以使用下面的语法查看一个数组包含多少个元素：&lt;br /&gt;&lt;br /&gt;int numElements = integers.Length;   // integers is any reference to an array&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-8329919625051091499?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/8329919625051091499/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/02/c_5139.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/8329919625051091499'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/8329919625051091499'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/02/c_5139.html' title='c#教程（七）数组'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-6828705965173208487</id><published>2009-02-25T17:16:00.000-08:00</published><updated>2009-02-25T17:17:44.004-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、枚举'/><title type='text'>c#教程（六） 枚举</title><content type='html'>枚举是用户定义的整数类型。在声明一个枚举时，要指定该枚举可以包含的一组可接受的实例值。不仅如此，还可以给值指定易于记忆的名称。如果在代码的某个地方，要试图把一个不在可接受值范围内的值赋予枚举的一个实例，编译器就会报告一个错误。这个概念对于VB程序员来说是新的。C++支持枚举，但C#枚举要比C++枚举强大得多。&lt;br /&gt;&lt;br /&gt;从长远来看，创建枚举可以节省大量的时间，减少许多麻烦。使用枚举比使用无格式的整数至少有如下三个优势：&lt;br /&gt;&lt;br /&gt;●       如上所述，枚举可以使代码更易于维护，有助于确保给变量指定合法的、期望的值。&lt;br /&gt;&lt;br /&gt;●       枚举使代码更清晰，允许用描述性的名称表示整数值，而不是用含义模糊的数来表示。&lt;br /&gt;&lt;br /&gt;●       枚举使代码更易于键入。在给枚举类型的实例赋值时，VS.NET IDE会通过IntelliSense弹出一个包含可接受值的列表框，减少了按键次数，并能够让我们回忆起可能的值。&lt;br /&gt;&lt;br /&gt;定义如下的枚举：&lt;br /&gt;&lt;br /&gt;public enum TimeOfDay&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   Morning = 0,&lt;br /&gt;&lt;br /&gt;   Afternoon = 1,&lt;br /&gt;&lt;br /&gt;   Evening = 2&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;在本例中，在枚举中使用一个整数值，来表示一天的每个阶段。现在可以把这些值作为枚举的成员来访问。例如，TimeOfDay.Morning返回数字0。使用这个枚举一般是把合适的值传送给方法，在switch语句中迭代可能的值。&lt;br /&gt;&lt;br /&gt;class EnumExample&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   public static int Main()&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      WriteGreeting(TimeOfDay.Morning);&lt;br /&gt;&lt;br /&gt;      return 0;&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;   static void WriteGreeting(TimeOfDay timeOfDay)&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      switch(timeOfDay)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         case TimeOfDay.Morning:&lt;br /&gt;&lt;br /&gt;            Console.WriteLine("Good morning!");&lt;br /&gt;&lt;br /&gt;            break;&lt;br /&gt;&lt;br /&gt;         case TimeOfDay.Afternoon:&lt;br /&gt;&lt;br /&gt;            Console.WriteLine("Good afternoon!");&lt;br /&gt;&lt;br /&gt;            break;&lt;br /&gt;&lt;br /&gt;         case TimeOfDay.Evening:&lt;br /&gt;&lt;br /&gt;            Console.WriteLine("Good evening!");&lt;br /&gt;&lt;br /&gt;            break;&lt;br /&gt;&lt;br /&gt;         default:&lt;br /&gt;&lt;br /&gt;            Console.WriteLine("Hello!");&lt;br /&gt;&lt;br /&gt;            break;&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;在&lt;a href="http://futureangle.blogspot.com"&gt;C#&lt;/a&gt;中，枚举的真正强大之处是它们在后台会实例化为派生于基类System.Enum的结构。这表示可以对它们调用方法，执行有用的任务。注意因为.NET Framework的执行方式，在语法上把枚举当做结构是不会有性能损失的。实际上，一旦代码编译好，枚举就成为基本类型，与int和float类似。&lt;br /&gt;&lt;br /&gt;可以获取枚举的字符串表示，例如使用前面的TimeOfDay枚举：&lt;br /&gt;&lt;br /&gt;TimeOfDay time = TimeOfDay.Afternoon;&lt;br /&gt;&lt;br /&gt;Console.WriteLine(time.ToString());&lt;br /&gt;&lt;br /&gt;会得到字符串Afternoon。&lt;br /&gt;&lt;br /&gt;另外，还可以从字符串中获取枚举值：&lt;br /&gt;&lt;br /&gt;TimeOfDay time2 = (TimeOfDay) Enum.Parse(typeof(TimeOfDay), "afternoon", true);&lt;br /&gt;&lt;br /&gt;Console.WriteLine((int)time2);&lt;br /&gt;&lt;br /&gt;这段代码说明了如何从字符串获取枚举值，并转换为整数。要从字符串中转换，需要使用静态的Enum.Parse()方法，这个方法带3个参数，第一个参数是要使用的枚举类型。其语法是关键字typeof后跟放在括号中的枚举类名。typeof运算符将在第5章详细论述。第二个参数是要转换的字符串，第三个参数是一个bool，指定在进行转换时是否忽略大小写。最后，注意Enum.Parse()方法实际上返回一个对象引用—— 我们需要把这个字符串显式转换为需要的枚举类型(这是一个取消装箱操作的例子)。对于上面的代码，将返回1，作为一个对象，对应于TimeOfDay.Afternoon的枚举值。在显式转换为int时，会再次生成1。&lt;br /&gt;&lt;br /&gt;System.Enum上的其他方法可以返回枚举定义中的值的个数、列出值的名称等。详细信息参见MSDN文档。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6509530088537976629-6828705965173208487?l=futureangle.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://futureangle.blogspot.com/feeds/6828705965173208487/comments/default' title='帖子评论'/><link rel='replies' type='text/html' href='http://futureangle.blogspot.com/2009/02/c_25.html#comment-form' title='0 条评论'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/6828705965173208487'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6509530088537976629/posts/default/6828705965173208487'/><link rel='alternate' type='text/html' href='http://futureangle.blogspot.com/2009/02/c_25.html' title='c#教程（六） 枚举'/><author><name>black-angle</name><uri>http://www.blogger.com/profile/16836665724221582592</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='24' src='http://3.bp.blogspot.com/__frkXe_FZc8/SXcSbCKp7XI/AAAAAAAAAAY/XpR9wUsCtLs/S220/http_imgload.jpg'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6509530088537976629.post-4560634920649290165</id><published>2009-02-24T16:54:00.000-08:00</published><updated>2009-02-24T16:56:09.884-08:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='c#、教程、流控制'/><title type='text'>c#教程（五）流控制2</title><content type='html'>C#提供了4种不同的循环机制(for、while、do...while和foreach)，在满足某个条件之前，可以重复执行代码块。for、while和do...while循环与C++中的对应循环相同。首先看看for 循环。&lt;br /&gt;&lt;br /&gt;1. for循环&lt;br /&gt;C#的for循环提供的迭代循环机制是在执行下一次迭代前，测试是否满足某个条件，其语法如下：&lt;br /&gt;&lt;br /&gt;for (initializer; condition; iterator)&lt;br /&gt;&lt;br /&gt;   statement(s)&lt;br /&gt;&lt;br /&gt;其中：&lt;br /&gt;&lt;br /&gt;●       initializer是指在执行第一次迭代前要计算的表达式(通常初始化为一个局部变量，作为循环计数器)；&lt;br /&gt;&lt;br /&gt;●       condition是在每次迭代循环前要测试的表达式(它必须等于true，才能执行下一次迭代)；&lt;br /&gt;&lt;br /&gt;●       iterator是每次迭代完要计算的表达式(通常是递增循环计数器)。当condition等于false时，迭代停止。&lt;br /&gt;&lt;br /&gt;for循环是所谓的预测试循环，因为循环条件是在执行循环语句前计算的，如果循环条件为假，循环语句就根本不会执行。&lt;br /&gt;&lt;br /&gt;for循环非常适合用于一个语句或语句块重复执行预定的次数。下面的例子就是使用for循环的典型用法，这段代码输出从0~99的整数：&lt;br /&gt;&lt;br /&gt;for (int i = 0; i &lt; 100; i = i+1)   // this is equivalent to&lt;br /&gt;&lt;br /&gt;                           // For i = 0 To 99 in VB.&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   Console.WriteLine(i); &lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;这里声明了一个int类型的变量i，并把它初始化为0，用作循环计数器。接着测试它是否小于100。因为这个条件等于true，所以执行循环中的代码，显示值0。然后给该计数器加1，再次执行该过程。当i等于100时，循环停止。&lt;br /&gt;&lt;br /&gt;实际上，上述编写循环的方式并不常用。C#在给变量加1时有一种简化方式，即不使用i = i+1，而简写为i++：&lt;br /&gt;&lt;br /&gt;for (int i = 0; i &lt; 100; i++)&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;//etc.&lt;br /&gt;&lt;br /&gt;C#的for循环语法比VB中的For…Next循环的功能强大得多，因为迭代程序可以是任何语句。在VB中，只能对循环控制变量加减某个数字。在C#中，则可以做任何事，例如，让循环控制变量乘以2。&lt;br /&gt;&lt;br /&gt;嵌套的for循环非常常见，在每次迭代外部的循环时，内部循环都要彻底执行完毕。这种模式通常用于在矩形多维数组中遍历每个元素。最外部的循环遍历每一行，内部的循环遍历某行上的每个列。下面的代码可以用作NumberTable例子，显示数字行，它还使用另一个Console方法Console.Write()，该方法的作用与Console.WriteLine()相同，但不在输出中添加回车换行符：&lt;br /&gt;&lt;br /&gt;using System;&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;namespace Wrox.ProCSharp.Basics&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   class MainEntryPoint&lt;br /&gt;&lt;br /&gt;   {&lt;br /&gt;&lt;br /&gt;      static void Main(string[ ] args)&lt;br /&gt;&lt;br /&gt;      {&lt;br /&gt;&lt;br /&gt;         // This loop iterates through rows...&lt;br /&gt;&lt;br /&gt;         for (int i = 0; i &lt; 100; i+=10)&lt;br /&gt;&lt;br /&gt;         {&lt;br /&gt;&lt;br /&gt;            // This loop iterates through columns...&lt;br /&gt;&lt;br /&gt;            for (int j = i; j &lt; i + 10; j++)&lt;br /&gt;&lt;br /&gt;            {&lt;br /&gt;&lt;br /&gt;               Console.Write("  " + j);&lt;br /&gt;&lt;br /&gt;            }&lt;br /&gt;&lt;br /&gt;            Console.WriteLine();&lt;br /&gt;&lt;br /&gt;         }&lt;br /&gt;&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;尽管j是一个整数，但它会自动转换为字符串，以便进行连接。C++开发人员要注意，这比在C++中处理字符串容易得多，VB开发人员则已经习惯于此了。&lt;br /&gt;&lt;br /&gt;C程序员应注意上述例子中的一个特殊功能。在每次迭代后续的外部循环时，最内部循环的计数器变量都要重新声明。这种语法不仅在C#中可行，在C++中也是合法的。&lt;br /&gt;&lt;br /&gt;上述例子的结果是：&lt;br /&gt;&lt;br /&gt;csc NumberTable.cs&lt;br /&gt;&lt;br /&gt;Microsoft (R) Visual C# .NET Compiler version 7.10.3052.4&lt;br /&gt;&lt;br /&gt;for Microsoft (R) .NET Framework version 1.1.4322&lt;br /&gt;&lt;br /&gt;Copyright (C) Microsoft Corporation 2001-2002. All rights reserved.&lt;br /&gt;&lt;br /&gt; &lt;br /&gt;&lt;br /&gt;  0  1  2  3  4  5  6  7  8  9&lt;br /&gt;&lt;br /&gt;  10  11  12  13  14  15  16  17  18  19&lt;br /&gt;&lt;br /&gt;  20  21  22  23  24  25  26  27  28  29&lt;br /&gt;&lt;br /&gt;  30  31  32  33  34  35  36  37  38  39&lt;br /&gt;&lt;br /&gt;  40  41  42  43  44  45  46  47  48  49&lt;br /&gt;&lt;br /&gt;  50  51  52  53  54  55  56  57  58  59&lt;br /&gt;&lt;br /&gt;  60  61  62  63  64  65  66  67  68  69&lt;br /&gt;&lt;br /&gt;  70  71  72  73  74  75  76  77  78  79&lt;br /&gt;&lt;br /&gt;  80  81  82  83  84  85  86  87  88  89&lt;br /&gt;&lt;br /&gt;  90  91  92  93  94  95  96  97  98  99&lt;br /&gt;&lt;br /&gt;尽管在技术上，可以在for循环的测试条件中计算其他变量，而不计算计数器变量，但这不太常用。也可以在for循环中忽略一个表达式(甚或所有表达式)。但此时，要考虑使用while循环。&lt;br /&gt;&lt;br /&gt;2. while循环&lt;br /&gt;while循环与C++和Java中的while循环相同，与VB中的While...Wend循环相同。与for循环一样，while也是一个预测试的循环。其语法是类似的，但while循环只有一个表达式：&lt;br /&gt;&lt;br /&gt;while(condition)&lt;br /&gt;&lt;br /&gt;   statement(s);&lt;br /&gt;&lt;br /&gt;与for循环不同的是，while循环最常用于下述情况：在循环开始前，不知道重复执行一个语句或语句块的次数。通常，在某次迭代中，while循环体中的语句把布尔标记设置为false，结束循环，如下面的例子所示。&lt;br /&gt;&lt;br /&gt;bool condition = false;&lt;br /&gt;&lt;br /&gt;while (!condition)&lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   // This loop spins until the condition is true&lt;br /&gt;&lt;br /&gt;   DoSomeWork();&lt;br /&gt;&lt;br /&gt;   condition = CheckCondition();   // assume CheckCondition() returns a bool&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;所有的&lt;a href="http://futureangle.blogspot.com"&gt;C#&lt;/a&gt;循环机制，包括while循环，如果只重复执行一条语句，而不是一个语句块，都可以省略花括号。许多程序员都认为最好在任何情况下都加上花括号。&lt;br /&gt;&lt;br /&gt;3. do…while循环&lt;br /&gt;do...while循环是while循环的后测试版本。它与C++和Java中的do...while循环相同，与VB中的Loop...While循环相同，该循环的测试条件要在执行完循环体之后执行。因此do...while循环适合于至少执行一次循环体的情况：&lt;br /&gt;&lt;br /&gt;bool condition;&lt;br /&gt;&lt;br /&gt;do &lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   // this loop will at least execute once, even if Condition is false&lt;br /&gt;&lt;br /&gt;   MustBeCalledAtLeastOnce();&lt;br /&gt;&lt;br /&gt;   condition = CheckCondition();&lt;br /&gt;&lt;br /&gt;} while (condition);&lt;br /&gt;&lt;br /&gt;4. foreach循环&lt;br /&gt;foreach循环是我们讨论的最后一种C#循环机制。其他循环机制都是C和C++的最早期版本，而foreach语句是新增的循环机制(借用于VB)，也是非常受欢迎的一种循环。&lt;br /&gt;&lt;br /&gt;foreach循环可以迭代集合中的每个项目。现在不必考虑集合的概念，第9章将介绍集合。现在，知道集合是一种包含其他对象的对象即可。从技术上看，要使用集合对象，它必须支持Ienumerable接口。集合的例子有C#数组、System.Collection命名空间中的集合类，以及用户定义的集合类。从下面的代码中可以了解foreach循环的语法，其中假定arrayOfInts是一个整型数组：&lt;br /&gt;&lt;br /&gt;foreach (int temp in arrayOfInts) &lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   Console.WriteLine(temp);&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;其中，foreach循环一次迭代数组中的一个元素。对于每个元素，它把该元素的值放在int型的变量temp中，然后再执行一次循环迭代。&lt;br /&gt;&lt;br /&gt;注意，不能改变集合中各项(上面的temp)的值，所以下面的代码不会被编译：&lt;br /&gt;&lt;br /&gt;foreach (int temp in arrayOfInts) &lt;br /&gt;&lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;   temp++;&lt;br /&gt;&lt;br /&gt;   Console.WriteLine(temp);&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;如果需要迭代集合中的各项，并改变它们的值，就应使用for循环。&lt;br /&gt;&lt;br /&gt;2.5.3  跳转语句&lt;br /&gt;C#提供了许多可以立即跳转到程序中另一行代码的语句，在此，先介绍goto语句。&lt;br /&gt;&lt;br /&gt;1. goto语句&lt;br /&gt;goto语句可以直接跳转到程序中用标签指定的另一行(标签是一个标识符，后跟一个冒号)：&lt;br /&gt;&lt;br /&gt;goto Label1;&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("This won't be executed");&lt;br /&gt;&lt;br /&gt;Label1:&lt;br /&gt;&lt;br /&gt;   Console.WriteLine("Continuing execution from here");&lt;br /&gt;&lt;br /&gt;goto语句有两个限制。不能跳转到像for循环这样的代码块中，也不能跳出类的范围，不能退出try...catch块后面的finally块(第11章将介绍如何用try...catch...finally块处理异常)。&lt;br /&gt;&lt;br /&gt;goto语句的名声不太好，在大多数情况下不允许使用它。一般情况下，使用它肯定不是面向对象编程的好方式。但是有一个地方使用它是相当方便的——在switch语句的case子句之间跳转，这是因为C#的switch语句在故障处理方面非常严格。前面介绍了其语法。&lt;br /&gt;&lt;br /&gt;2. break语句&lt;br /&gt;前面简要提到过break语句——在switch语句中使用它退出某个case语句。实际上，
