转载《叩开C#之门》

来源:互联网 发布:淘宝店铺导航条装修 编辑:程序博客网 时间:2024/05/16 11:29

解决方案、项目、程序集、命名空间

《叩开C#之门》系列之一

前言:表弟想要学编程,我推荐他学习.Net和C#。这一推荐不打紧,我却承担上了指导的职责。我又出差在外,直接辅导是不行了,通过邮件也太麻烦。推荐了几本书,可惜他太菜了,总有无从下手的感觉。推及他人,在初学C#时,是否也有这样的感觉呢?所以,就有了这个系列文章。表弟是我把他带入计算机行业的,当初什么都不懂,我曾经打开计算机机箱,指点他哪里是硬盘、哪里是内存,是CPU,现在对于计算机硬件他早已可以做我师傅。希望学软件编程也能这样。

一、解决方案、项目、程序集、命名空间
初学者很容易把这些概念搞混淆。先说说项目(Project),通俗的说,一个项目可以就是你开发的一个软件。在.Net下,一个项目可以表现为多种类型,如控制台应用程序,Windows应用程序,类库(Class Library),Web应用程序,Web Service,Windows控件等等。如果经过编译,从扩展名来看,应用程序都会被编译为.exe文件,而其余的会被编译为.dll文件。既然是.exe文件,就表明它是可以被执行的,表现在程序中,这些应用程序都有一个主程序入口点,即方法Main()。而类库,Windows控件等,则没有这个入口点,所以也不能直接执行,而仅提供一些功能,给其他项目调用。

在Visual Studio.Net中,可以在“File”菜单中,选择“new”一个“Project”,来创建一个新的项目。例如创建控制台应用程序。注意在此时,Visual Studio除了建立了一个控制台项目之外,该项目同时还属于一个解决方案(Solution)。这个解决方案有什么用?如果你只需要开发一个Hello World的项目,解决方案自然毫无用处。但是,一个稍微复杂一点的软件,都需要很多模块来组成,为了体现彼此之间的层次关系,利于程序的复用,往往需要多个项目,每个项目实现不同的功能,最后将这些项目组合起来,就形成了一个完整的解决方案。形象地说,解决方案就是一个容器,在这个容器里,分成好多层,好多格,用来存放不同的项目。一个解决方案与项目是大于等于的关系。建立解决方案后,会建立一个扩展名为.sln的文件。

在解决方案里添加项目,不能再用“new”的方法,而是要在“File”菜单中,选择“Add Project”。添加的项目,可以是新项目,也可以是已经存在的项目。

程序集叫Assembly。学术的概念我不想提,通俗的角度来说,一个项目也就是一个程序集。从设计的角度来说,也可以看成是一个完整的模块(Module),或者称为是包(Package)。因此,一个程序集也可以体现为一个dll文件,或者exe文件。怎样划分程序集也是大有文章的,不过初学者暂时不用考虑它。

命名空间(namespace)是在C++里面就有的概念。引入它,主要是为了避免一个项目中,可能会存在的相同对象名的冲突。这个命名空间的定义,没有特殊的要求。不过基本上来说,为了保证其唯一性,最好是用uri的格式,例如BruceZhang.com。这个命名空间有点像我们姓名中的姓,然后每个对象的名字则是姓名中的名。如果有重复,再国外的命名中,还可以加上middle name。那么名都为勇的,由于姓氏不同也就分开了,或者叫张勇,或者叫赵勇。当然人的姓氏重复者居多,所以我们为命名空间取名时,尽可能的复杂一点。

有许多初学者,常常把一个项目就理解为一个命名空间。其实这两者没有绝对的联系,在项目里我们也可以定义很多不相同的命名空间。但为了用户便于使用,最好在一个项目中,其命名空间最好是一体的层次结构。在Visual Studio里,我们可以在项目中新建一个文件夹,默认情况下,该文件夹下对象的命名空间,应该是“项目的命名空间.文件夹名”。当然,我们也可以在namespace中修改它。

命名空间和程序集名,都可以在Visual Studio中设置。用鼠标右键单击项目名,就可以弹出如下对话框:
 

cs01.gif



在图中,Assembly Name就是程序集名,如果经过编译,则为该项目的文件名。而Default Namespace则为默认的命名空间。在开发软件时,我们要养成良好的习惯,在建立新项目后,就将这些属性设置好。一旦设置好了Default Namespace,则以后新建的对象,其命名空间即为该设定的值。至于程序集名,如果是dll文件,建议其名最好与Default Namespace一致。

实例演练:
(一)创建控制台应用程序“Hello World!”
1、打开Visual Studio.Net,选择“File”菜单的“new”,选择“Project”;
2、选择Visual C# Projects中的“Console Application”,如图所示:
 

cs02.gif


在Location中,定位你要保存的项目的路径,而名字则为“FirstExample”。该名字此时既是解决方案的名字,同时也是该项目的名字。
3、用鼠标右键单击项目名,在弹出的对话框中,将Assembly Name命名为HelloWorld,将Default Namespace命名为:BruceZhang.com.FirstExample。
4、此时Visual Studio中已经建立了一个文件,其名为Class1.cs(如果是Visual Studio 2005,则默认为Program.cs);修改该文件的文件名为HelloWorld.cs,同时修改文件中的namespace,和类名,如下:
namespace BruceZhang.com.FirstExample
{
 /// <summary>
 /// Summary description for Class1.
 /// </summary>
 class HelloWorld
 {
  /// <summary>
  /// The main entry point for the application.
  /// </summary>
  [STAThread]
  static void Main(string[] args)
  {
   //
   // TODO: Add code to start application here
   //
  }
 }
}
5、注意在HelloWorld.cs中,有一个Main()方法。这是因为我们建立的是控制台应用程序。在Main()方法中添加如下代码:
Console.WriteLine("Hello World!");
Console.Read();
这里的Console是一个能对控制台进行操作的类。
6、运行。
检查保存项目的路径文件夹FirstExample/bin/debug,已经存在了一个HelloWorld.exe文件。

(二)为解决方案添加一个新项目
1、在“File”菜单中,选择“Add Project”,添加“New Project”。在对话框中选择“Class Library”,名字为Printer。至于保存路径,可以放在之前建立的FirstExample文件夹下:

cs03.gif


2、在Visual Studio右侧,可以看到现在有两个项目了。仍然修改新项目的名称和默认命名空间名,均为BruceZhang.com.Printer。
3、将默认建立的Class1.cs改名为MessagePrinter.cs,同时修改其代码为:
namespace BruceZhang.com.Printer
{
 /// <summary>
 /// Summary description for Class1.
 /// </summary>
 public class MessagePrinter
 {
  public MessagePrinter()
  {
   //
   // TODO: Add constructor logic here
   //
  }

  public static void Print(string msg)
  {
   Console.WriteLine(msg);
  }
 }
}
在MessagePrinter类中,我们注意到并没有Main()方法,因为它不是应用程序。新增加的Print()方法,能够接收一个字符串,然后在控制台中显示出来。
4、编译Printer项目。鼠标右键单击该项目名,在菜单中选择“Build”。成功编译后,找到文件夹Printer/bin/debug,可以发现有文件BruceZhang.com.Printer.dll,这就是最后形成的程序集文件。
5、关联这两个项目。我们希望是在FirstExample项目中用到Printer项目的Print()方法,前提是需要在FirstExample项目中添加对Printer项目的引用。右键单击FirstExample项目的“Reference”,选择“Add Reference”,在对话框中选择“Project”标签,找到该项目并选中,最后如图所示:

cs04.gif


 
6、现在就可以在FirstExample项目中使用MessagePrinter了。首先,在命名空间中添加对它的使用(Using),然后再Main()方法中调用它,最后代码如下:
using System;
using BruceZhang.com.Printer;

namespace BruceZhang.com.FirstExample
{
 /// <summary>
 /// Summary description for Class1.
 /// </summary>
 class HelloWorld
 {
  /// <summary>
  /// The main entry point for the application.
  /// </summary>
  [STAThread]
  static void Main(string[] args)
  {
   MessagePrinter.Print("Hello World!");
   Console.Read();
  }
 }
}
7、运行。结果与前一个例子一样。

在这个例子中,解决方案中就包含了两个项目,一个是控制台应用程序,一个是类库。类库提供一些基本的功能,如例子中的Print()方法。我们常常把一些共用的方法,放到类库中。这样其他的应用程序就可以去调用它。例如本例的控制台应用程序。如果新建的Windows应用程序,也需要这个功能,就可以直接引用MessagePrinter的Print()方法,而不必重复去实现。

 


 

C#与面向对象编程语言

《扣开C#之门》系列之二

二、C#与面向对象编程语言

C#是纯粹的面向对象编程语言,它真正体现了“一切皆为对象”的精神。在C#中,即使是最基本的数据类型,如int,double,bool类型,都属于System.Object类型。此外,使用C#编程,不会存在与游离于对象之外的属于过程的东西。因此,学习C#,就必须具有面向对象思想,不明白所谓的“面向对象思想”,就不可能掌握C#的精髓,而对于C#的理解,就只能仅限于语言特色的表面了。

很多讲解面向对象思想的书籍和文章喜欢将面向对象与面向过程相比较,然而对于学习程序设计的初学者而言,是完全没有必要的。一张空白的纸张,事实上比写满字的纸张更利于未来的发展。

那么什么是“面向对象思想”?其精要当然是“一切皆为对象”的意义。软件的开发与设计,是围绕着开发的目标进行的。比如开发一个电子商务网站,那么与电子商务有关的都是我们要操作和关心的,商品、订单、用户、目录、购物车等等,都是对象。如果要开发一个论坛,那么论坛中必备的要素,如论坛分类、主题、帖子、评论、用户,也全都是对象。从小处说,我们要求写一个算法用以计算阶乘,那么起始值、运算结果、以及整个算法都是对象。

那么对象具有什么特质?一般而言,一个对象应具有属性和行为。以现实世界为例,“人”作为一种特殊的动物,也是我们所称谓的“对象”。这个对象具有很多属性,例如姓名,身高、体重、民族、国籍、出生年月等等。而行为呢,可以是行走、吃饭、跑步、乃至于玩游戏、踢足球。如果从词义学的角度来看,属性更偏向于名词的范畴,而行为则近乎于动词。然而对象也有特殊的情况,或者只具有属性,或者只具有行为。例如身份证,可能只具有姓名,出生年月,籍贯,身份证号等属性,而没有行为。例如“飞”,这本身是一种动作,但同样可以认为是一种行为。

然而这种划分并非绝对,根据实际情况,同一种对象具有的属性和行为可能不一样。例如同样是“人”,如果是要开发一个户籍管理系统,我们就不必去考虑行走、吃饭、跑步等行为,但如果是一个足球游戏,那么对象“人”,就需要各种踢足球的动作作为该对象的行为了。再比如“身份证”对象,也可能为了使用方便,会为它添加“IsValidate”这样一个行为。

还要注意的是,很多时候属性其实本身也是一个对象,例如姓名,可以是string类型,它属于Sytem.Object对象类型,我们也可以单独为姓名建立一个类对象:
public class Name
{
 public string FirstName;
 public string MiddleName;
 public string LastName;
}

在C#里,有各种各样的对象类型,除了本身提供的基本类型外,还支持自定义各种类型,包括class,struct,enum,interface,delegate,event。以前面的例子来看,对象Person“人”,就可以被定义为class类型,国籍可以被定义为enum类型。没有行为的身份证,也可以被定义为struct类型。动作“飞”可以被定义为interface类型。至于delegate和event,则是一种特殊对象类型,例如我们可以为论坛管理系统,定义一系列事件,如BeforePost,AfterPost等等。关于这些内容,我会在后面的内容中详细讲解。

要掌握面向对象思想,就必须具备识别系统中对象的能力。在我们要开发的系统中,哪些需要定义为单独的对象,而且这个对象应该具备哪些属性和行为,都是初学者比较头疼的问题。要正确地识别对象,就需要首先分析该系统所应实现的功能。例如,我们要求开发一个电子商务系统,功能要求如下:
1、支持商品的查询功能;
2、用户如需购买商品,需要成为注册用户并登录;
3、如果当前库存无该商品,用户可以预订;
4、用户可以将商品放入购物车;
5、用户购买商品之前,需提交订单;
6、用户提交订单后,系统应将订购信息发送邮件给用户;
7、用户可以对商品添加评论。

有一个简单的方法,可以帮助我们识别对象,就是找出这些功能描述中的关键名词,如商品、用户、购物车、订单、评论。这些词语基本上可以涵盖这个电子商务系统的主要对象了。至于对象的属性,可以考察该系统对这个对象的要求和所需要的信息。例如商品,必然要求有商品名、价格、相关图片、所属类别、库存量等等。至于行为,我们还可以从功能描述中去寻找与这些对象名词相关联的动词。例如商品的查询,就代表商品具有Search行为。用户注册与登录,就代表该用户具有这两种行为:Register,SignIn。当然隐含的行为自然还包括SignOut。

真正要识别好对象,也许我们需要掌握UML的知识,知道怎么写用例。或者利用极限编程的方法,把一个个用户故事写出来,然后一一识别。不过这些知识对于初学者似乎太难了。我们现在知道能有这样一个方法就可以了。我更希望的是,读者在看完本节,能够建立起“一切皆为对象”的思想。最后,更希望大家能把识别对象看作是一种填字游戏,关键不在于答案是什么,乐趣全在游戏中了。

面向对象思想博大精深,不是三言两语就能说清楚的。但建立这样一种思想,要存乎于你的内心,这在与一种“妙悟”。也许在你写了几百个小程序之后还没能体会到面向对象的真谛,但是在霎那之间,你却突然明白了,顿有一种拨开云开见月明的喜悦。我不是故意卖弄玄虚,有时候开发软件还真需要那么一点灵感,有人说,编程是一门艺术,我同意。那么这种“妙悟”什么时候能来到,我无法确知答案,这需要你的体悟。不过这种体悟与谈禅无关,不是每天冥想就可以的,还是踏实点,多写写程序,慢慢你就会明白了。

 


 

对象的封装与C#的类

《叩开C#之门》系列之三

三、对象的封装与C#的类

面向对象思想有三个核心要素:封装、继承与多态。如能正确理解这三要素,那么基本上可以算是在编程中建立了面向对象思想。在第二节中我曾介绍,在C#中,所有数据类型的实例都是“对象”,不过最能体现对象特质的类型,还是“类”,同时它也是C#中最重要、最频繁使用的类型。接下来,我将通过介绍C#的类,来充分理解对象封装的概念。

所谓“对象”,形象地说,我们可以把它理解为一块积木。设计积木的人需要设计积木的外观与形状,还有内部的材质。堆积木的人对于内部的材质并不关心,他们只需要根据不同的外观与形状来决定堆放的位置。因此,对于开发者而言,要设计面向对象的程序,同时会是两个迥然不同的身份:设计者与使用者。

先谈谈使用者。使用者的身份,就是利用已经提供给你的所有对象,根据需求,设计出自己需要实现的程序。就如堆积木的过程。这恰恰是面向对象编程的优势所在,那就是“对象的重用”。已经设计好的对象,可以被不同的使用者调用,这些功能既然已经实现,对于使用者而言,当然就免去了自己去设计的过程。正如堆积木那样,既然有了现成设计好的积木,使用者所要做的工作就是把这些积木最后组合起来,堆成不同的形状。.Net Framework所提供的类库,就是这样的积木。

例如我们想把一个int类型转换成字符型,就没有必要自己去实现这种转换,直接调用.Net Framework提供的功能就可以了:
int i = 10;
string s = i.ToString();

再比如我们想弹出一个Windows消息框,同样可以直接使用.Net Framework现有的类库:
MessageBox.Show(“Message”);

在上述的例子中,i和MessageBox都是一个对象。

再谈谈设计者的身份。虽然.Net Framework的类库功能已经非常强大,但它不可能考虑到业务的方方面面,如果需要使用一个根本就不存在的对象,此时就需要自己来设计了。例如图书管理系统,可能就需要用户,图书等对象。这就需要开发者自己来设计这些对象。

既然最能体现“对象”思想的类型是“类”,我就来介绍一下C#中的类类型。C#中类的关键字是class。在一个class对象中,主要分为field(字段)、property(属性)和method(方法),前面两个对应的是对象的属性,而method则对应对象的行为。一个典型的class定义如下所示:
public class User
{
 private string m_name;
 private string m_password;
 private int m_tryCounter;
 public string Name
 {
  get {return m_name;}
  set {m_name = value;}
 }
 public string Password
 { 
  get {return m_password;}
  set {m_password = value;}
 }
 public void SignIn()
 {  
  if (m_tryCounter < 3)
  {
   if (IsValid())
   {  
    m_tryCounter = 0;
    Console.WriteLine("User {0} was signed in.", m_name);
   }
   else
   {
    m_tryCounter++;
    Console.WriteLine("User {0} is invalid. Can’t Sign in.", m_name);
   }
  }
  else
  {
   Console.WriteLine("You try to sign in more than 3 times. You are be denied.");
  }
 }
 public void SignOut()
 {
  m_tryCounter = 0;
  Console.WriteLine("User {0} was signed out.", m_name);
 } 
 private bool IsValid()
 {
  if (m_name.ToUpper() == "ADMIN" && m_password == "admin")
  {
   return true;
  }
  else
  {
   return false;
  }
 }
}

字符串m_name,m_password,m_tryCounter就是类User的字段,Name,Password是类User的属性,而SignIn、SignOut和IsValid则是类User的方法。

关于field,property和method,我会在之后的文章中介绍,这里主要介绍的是在这个类中出现的修饰符public、private等相关的知识。

前面说到对象好比是一个积木,设计者需要定义好这个积木的外观和形状,也要考虑积木内部的制作,例如选用的材质,以及是空心还是实心。如果将这个积木剖开来看,实际上该对象应分为内、外两层。由于使用者只关心外部的实现,因此设计者就需要考虑,哪些实现应暴露在外,哪些实现应隐藏于内。这就体现了对象的封装的思想。

封装对象,并非是将整个对象完全包裹起来,而是根据具体的需要,设置使用者访问的权限。在C#中,分别用修饰符public,internal,protected,private设定,分别修饰类的字段、属性和方法,甚至于类对象本身:
public:表明所有对象都可以访问;
protected internal:表明同一个程序集内的对象,或者该类对象以及其子类可以访问;
internal:表明只有同一个程序集的对象可以访问;
protected:表明只有该类对象及其子类对象可以访问;(关于继承,会在以后介绍)
private:表明只有对象本身在对象内部可以访问;

可以看出,public的开放性最大,其次是protected internal,private的开放性最小。internal和protected居中。那么,internal和protected哪一个开放范围更大呢?我认为,没有完全绝对的结论。它们的范围前者体现一个横向的概念,后者则体现纵向的概念。如果是internal,那么外部程序集对象自然不能访问,但只要是居于同一个程序集中,则所有对象都可以访问它;如果是protected,那么即使是外部程序集对象,只要它继承了该对象,就可以访问,而即使是同一程序集,如果对象不是该类对象的子类,也是无法访问的。打一个比方,在我们的传统文化中,是非常强调“宗族”观念的,一个宗族的族长,对于本族人而言,权力极大,甚至掌握了生杀大权。以一个州府的范围为例,internal就好比是知府大人,只要是该州府的百姓,都属于他的管辖范围,而不管他是哪一个宗族。protected则好比是宗族的族长,只要是这个宗族的成员,都要服从他,哪怕该成员属于其他州府。我以前看过《雍正王朝》,其中就有这样一个情节,身为皇子的胤祯,竟然无法挽救自己心爱女人的命运,因为这个女人违反了她们宗族的族规,最后眼看着她被活活烧死,却只能黯然神伤,梦里萦回。

以前面定义的User类而言,所有的字段m_name,m_password,m_tryCounter都是private的,因此User类的外部调用者无法调用它们,但请注意User类内部的方法比如SignIn或者属性Name,却完全可以调用。同样的,private方法IsValid,可以被SignIn方法调用,但对于外部调用者而言,则是无法调用的。而对于public属性Name,Password,public方法SignIn和SignOut,外部的调用者是可以访问的。在后面的演练中,我们能够看出这之间的区别。通过这样分层次的封装,就可以充分保证对象的重用性和安全性。

那么对于类类型而言,如何确定它们的访问权限呢?这要根据实际的需求来看了。假定这个User类是用于一个电子商务网站。那么电子商务系统在设计过程中,就需要调用到User类对象。显然,登录与退出功能是必须提供给外部使用者的,例如登录页面就会使用到User类。而IsValid()方法用于验证用户的合法性,虽然也非常必要,但该功能仅仅用于登录的时候核实用户身份,也就是说,IsValid方法只会被SignIn方法使用,但外部实用者却并不关心,因此,设置为private就是合理的。同样的道理,字段m_tryCounter也是如此。但如果需求发生改变,验证用户的功能不仅仅是登录的时候需要使用,在添加商品到购物车,下订单,付款的时候,都需要该功能,那么IsValid方法,就有必要修改为public方法了。

所以,在设计程序的时候,除了要考虑识别对象,还要充分考虑该对象的封装。类对象内的字段、属性和方法,包括类本身,哪些应该暴露在外,哪些应该被隐藏,都需要根据实际的需求,给与正确的设计。

演练:
(一)设计类User,并调用该类
1、打开Visual Studio.Net,选择“File”菜单的“new”,选择“Project”;
2、选择Visual C# Projects中的“Console Application”。在Location中,定位你要保存的项目的路径,而名字则为“SecondExample”。该名字此时既是解决方案的名字,同时也是该项目的名字。
3、用鼠标右键单击项目名,在弹出的对话框中,将Assembly Name命名为SecondExample,将Default Namespace命名为:BruceZhang.com. SecondExample。
4、用鼠标右键单击项目名,选择“Add”菜单项的“Add Class”:
 

cs05.gif


5、在弹出的对话框中,将文件的名字命名为User.cs,如图:
 

cs06.gif


6、点击“Open”按钮后,项目中就添加了一个新的文件User.cs。打开该文件,将public class User中的内容,修改为前面文中定义好的User类。
7、修改原有默认的Class1.cs(如果是Visual Studio 2005,则默认为Program.cs)文件名为App.cs,然后将文件内容修改为:
 class App
 {
  /// <summary>
  /// The main entry point for the application.
  /// </summary>
  [STAThread]
  static void Main(string[] args)
  {
   User user = new User();
   
   //用户名和密码均错误;
   user.Name = "Bruce";
   user.Password = "test";
   for (int i=0;i<=4;i++)
   {
    user.SignIn();
   }
   user.SignOut();

   //用户名正确,密码错误;
   user.Name = "admin";
   user.Password = "test";
   for (int i=0;i<=4;i++)
   {
    user.SignIn();
   }
   user.SignOut();

   //用户名和密码正确;
   user.Name = "admin";
   user.Password = "admin";
   for (int i=0;i<=4;i++)
   {
    user.SignIn();
   }
   user.SignOut();

   //注意此时是无法调用这样的字段和方法的;
   //user.m_name;
   //user.m_password;
   //user.IsValid();

   Console.ReadLine();
  }
 }
8、运行。

 


 

定义C#的类

《叩开C#之门》系列之四

四、定义C#的类

既然类类型是C#中最重要、最常见的类型,因此它是我要介绍的重点,实际上,C#中的许多特性都可以通过类类型来体现。

前面已经介绍,一个类对象中,主要包括字段、属性和方法。不过除此之外,在类类型中还可以定义嵌套类,也可以定义一个常量。

对于一个没有任何编程知识的初学者而言,也许还应介绍一下常量与变量。不过从它们的名字就可以非常清晰地辨明二者的区别,常量其值是不可改变的,而变量的值则可以修改,除非该变量是只读的(如设置为readonly)。

最好的常量的例子就是圆周率值,这个值当然是不变化的,如果保留小数点后七位,其值为3.1415926。然而如果我们要频繁使用该值,则输入这么多数字自然不是好的选择,何况一旦用户要求圆周率更加精确,需要保留更多的小数位,要修改起来就非常困难了。因此我们需要定义一个常量:
const PI = 3.1415926;

此时pi就代表了3.1415926,要使用圆周率,直接取pi的值即可:
square = PI * radius;

注意上面的表达式,其中PI是常量,在定义它时,使用了const关键字;而square和radius则为变量,定义如下:
double square, radius;

一旦定义了PI为常量,那么该类对象被建立之后,就不能修改了,而变量是可以修改的。如下的代码是错误的:
PI = 3.1415926535897;
square = PI * radius;

而下面的两行代码则是正确的:
radius = 2.5;
square = PI * radius;

类的字段其实也是变量。如系列三中的类User,就包含有字段m_name,m_password,m_tryCounter。它们的类型分别为string,string,int。字段仍然可以利用public,internal,protected,private来修饰它。不过,我建议如非特殊情况,不要将字段修饰为public。因为,根据”对象封装”的原则,应尽量避免将一个类型的字段以公有方式提供给外部。毕竟,对于字段而言,对象对它的控制非常弱,一旦公开在外,则调用者可以比较容易的对其进行操作,尤其是写操作,从而可能会导致错误。例如,我们为前面定义的User类增加一个age(年龄)字段,假如我将其定义为public字段,如下所示:
public int Age;

那么调用者可能会将Age的值设为负数:
user.Age = -5;

对于字段的定义而言,并不能判断这样一种不合常理的操作,因为我们对字段的写操作的控制无能为力。

大家可以看到,这里所谓的字段值,其实可以对应于前面所讲的对象的属性。例如姓名,年龄,就是一个用户的属性。如果字段不能设置为public,那么调用者又如何访问它们呢?答案就是使用C#类中的property(属性)。

所谓“属性”,很大程度可以看作是对“字段”的一种封装,它利用一种被称为“get/set访问器”分别控制对字段的读写操作,并暴露一个属性值,如Age属性:
private int m_age;
public int Age
{
 get {return m_age;}
 set
 {
  if (value < 0)
  {
   throw new ArgumentOutOfRangeException("Age must be greater than or equal to 0");
  }
  m_age = value;
 }
}

上面的代码中,throw语句的作用是抛出一个异常,我们暂时可以不去理会它,而是将注意力放到get和set访问器上。首先,我们定义了一个私有字段m_age,然后再定义一个公共属性Age。在该属性中,get返回私有字段的值m_age,而在set中,首先会判断value的值,如果小于0,那么这个值是非法的,就将抛出一个异常,停止往下执行,并告诉你对Age值的设置错误了。当然,我们也可以为value值设置更严格的要求,例如不允许value大于150。至少人的年龄现在没有超过150岁的吧。

也许会有人疑问value究竟是什么?它其实是C#中提供的关键字,代表的就是你赋给该属性的真正的值,例如:
user.Age = 30;

此时是对Age属性赋值,.Net会执行set访问器,而value值就是30。然后判断30是否小于0。显然不符合条件,不会抛出异常,继续执行,将value值30赋给字段m_age。为什么要赋给m_age呢?让我们再看看get访问器,它其实就是一个读操作,返回的值是什么?对了,就是字段m_age,如下所示:
user.Age = 30;     //set操作,将字段m_age设置为30;
Console.WriteLine(“User’s Age is {0}.”, user.Age);   //get操作,将m_age的值取出;

此时就会在控制台下显示:
User’s Age is 30.

此外,对于一些特殊的要求,我们在将字段封装为属性时,可以只设置它的get访问器或者set访问器,这样这个属性就是只读属性,或者只写属性了。这样显然更有利于对象的封装。毕竟对于公共字段而言,我们最能可以控制它为只读(设置为readonly),却无法设置为只写。

从上可以看到,实际上属性就是对字段进行一次封装。通过这个封装,使我们对字段m_age的读写都具有了控制功能,至少现在的Age属性能够控制赋值为负数的情况了。这就是属性的好处。

在C# 2.0中,除了可以对整个属性设置public等访问修饰符外,对内部的get/set访问器同样可以设置访问修饰符,当然它要受到一定的限制。由于有些限制和接口、重写有关,我暂时不会介绍,在这里,我仅介绍访问器和属性的访问修饰符冲突问题。
1、如果整个属性被设置为public,则其访问器没有限制;
2、如果整个属性被设置为protected internal,则访问器的访问修饰仅能设置为internal,protected或者private中的一种;
3、如果整个属性被设置为internal或者protected,那么访问器的访问修饰只能是private。
如下例:
public Class A
{
 private string m_text;
 private int m_count;
 public string Text
 {
  get {return m_text;}
  protected set { m_text = value;}
 }
 internal int Count
 {
  private get {return 5;}
  private set {m_count = value}
}
}

从程序的实质来看,其实属性就是一种特殊的方法,它等同于下面的代码:
public int GetAge()
{
 return m_age;
}
public void SetAge(int age)
{
 m_age = age;
}

从这个意义上来理解get/set访问器的访问级别修饰,就更容易理解了。实质上,所谓的访问器的访问级别修饰,不外乎就是对方法进行访问级别修饰罢了。当然,C#中提供的属性要比访问字段的get/set方法更加简便。一般而言,如要定义方法,应该是和一个对象的行为有关,例如系列三定义的User类中的SignIn()和SignOut()方法,它们代表的是对象User的行为:登录和退出。

定义一个类的方法,必须包括五个要素:方法修饰符,方法名,返回类型,参数,以及方法体,例如Add方法:
public int Add(int x, int y)
{
 return x + y;
}

public即为我们的方法修饰符,它代表了该方法能被访问的级别。当然,修饰的方法的关键字还包括static,virtual,abstract等,不过这些内容会在以后介绍。方法名自然是Add了,自然属于方法的名字。返回类型为int,代表该方法会返回一个结果,该结果类型为int类型。参数有两个,分别为x和y,它们的类型都是int。调用者可以通过参数传递值到方法体中,并对它们进行操作。方法体则是花括号中的内容。

假设Add方法是定义在类Calculator中,那么该方法的调用为:
Calculator cal = new Calculator();
int result = cal.Add(3,5);

通过对Add的调用,并传入3和5的参数,最后得到结果8,并返回。因此,此时变量result的值就为8。而第一行代码,则是利用new关键字对Calculator类进行实例化,获得一个对象cal。通过对象cal,才可以调用Calculator类的公共方法、属性或字段。

为什么要进行实例化呢?我们定义一个类类型,是为调用者所使用的,否则就失去其意义了。但我们定义的这样一个类类型,仅仅是代表了某种格式而已,例如User类说明它是一个class,它拥有了一些字段、属性和方法。通过这样的定义,我们在使用这些类型的对象时,.Net能够识别它。而如果真正要调用这些类型对象,就必须进行”实例化”,这个操作就会在运行期间,创建一个个对象,并被放在内存空间中供程序调用。就好比”人”就是一个类类型,而某一个具体的人,才是被实例化的、真正存在的对象。要使得一个类类型被实例化,就需要为该类型提供”构造器”。构造器是一种特殊的方法,它没有返回类型,且其方法名和类型名保持一致,如Calculator类的定义以及它的构造器:
public class Calculator
{
 public Calculator()
 {
 }
 public int Add(int x, int y)
 {
  return x + y;
 }
}

Calculator()方法就是一个”构造器”,这个构造器并没有参数,在C#中,也被称为默认的构造器,即使不定义该构造器,.Net也会为它默认创建。例如在Calculator类中,我们完全可以删去Calculator()构造器的定义。然而,一旦我们定义了有参数的构造器时,则该默认构造器将不存在,如果我们再需要不带参数创建实例的话,就需要显式创建该构造器了。例如之前的User类。如果姓名和密码是该类一个非常重要的属性,大部分情况下,如果要创建User对象时,都需要这两个属性的值时,我们就可以为User类专门创建一个构造器:
public class User
{
 public User(string name, string password)
 {
  m_name = name;
  m_password = password;
 } 
}

注意在这个构造器中,接收两个参数name和password,并将其值赋给User类的字段m_name,m_password。因此,当我们通过如下的方式创建User类的实例时,我们创建的对象就已经具有Name和Password的值了:
User specUser = new User("bruce zhang", "password");

然而此时如果利用下面的方式创建User的实例,就会出现错误:
User user = new User();
因为此时User类的默认构造器(即无参的构造器)已经不存在,如要支持上面的实例化方式,就需要在User类的定义中添加无参构造器。

是否需要为一个类定义有参的构造器,应根据具体的需要而定。以User类而言,由于该类的Name和Password属性是一个对象必备的,那么建立这样一个构造器是有必要的。因为如果不具备这样的构造器,那么如前构造的specUser就需要用下面三行代码来完成:
User specUser = new User();
specUser.Name = "bruce zhang";
specUser.Password = "password";

注意,在一个类的定义中,我们可以使用this关键字来代表其类对象本身,通过this,可以访问到这个类的所有常量、字段、属性和方法,不管它们是public还是private,或者其他访问级别。虽然这个this指代的是类对象本身,也就是说它代表的就是实例化所产生的对象,但this的含义也仅仅限于对象的内部,从对象封装的思想来看,它是被封装了的,在对象外部是无法看到的。

例如下面的定义:
public class Visitor
{
 public void Visit(Element e)
 {
  Console.WriteLine("I was visited.");
 }
}
public class Element
{
 public void Accept(Visitor v)
 {
  v.Visit(this);
 }
}

在Element类中,Accept方法则传入一个Visitor类型的参数值,在该方法中,调用参数v的方法Visit,而Visit方法传入的是Element类型的值,由于Accept方法本身就属于Element类,因此,我们可以把其自身传递到Visit方法中,也就是代码中的this。

分析如下的代码段:
Visitor v = new Visitor();
Element e = new Element();
e.Accept(v);

Element的实例e,执行了Accept()方法,该方法传入的参数是Visitor类的实例v。那么执行Accept方法,实质就是在其方法内部执行v.Visit()方法,并通过this关键字将对象e本身传入,所以最后的结果是,打印如下的字符串:
I was visited。

这这里顺便提一下命名的要求。所谓命名规范,在作为团队开发的时候,是非常重要的。以本文为例,如何定义类名、字段名、属性名和方法名,都是有讲究的。通常来说,类名、属性名和方法名都要求所有单词的首字母大写。如果是字段,那么除非是公共字段,一般而言,应将第一个单词的首字母小写。不过这也是变量命名的要求。由于在一个类中,可能会临时用到一些变量,而不是字段,为了区别一般变量和字段,C++的程序员喜欢在变量名前加上“_”符号,许多C#程序员也沿用了这个习惯。不过我更喜欢为这些字段名前加上“m_”。命名一定要统一,尤其是在一个团队中,不过类似于这些临时变量,或者非公有变量,对名字的限制要少一些,毕竟这些变量不会被类的调用者使用。此外,对于常量而言,最好定义为全部大写的名字,如前面的定义的常量PI。

C#专门有一套完整的命名规范,有兴趣的可以自己去查阅一下专门的资料。此外,不同的公司可能还有一些特定的命名规范,在这里就不再赘述了。

 


 

const、readonly和static

《叩开C#之门》系列之五

五、const、readonly和static

在第四节中,我介绍了常量的定义,其关键字就是const。在定义常量时,必须赋予其初始值。一旦赋予了初始值后,就不能修改其值。也就是所谓的常量值不能更改的含义。由于C#是一门纯粹的面向对象语言,并不存在一个常量或者变量游离于对象之外,因此,这些定义,必然都是在一个类型内完成的。

关于常量的使用,除了会用作一些算法的临时常量值以外,最重要的是定义一些全局的常量,被其他对象直接调用。而集中这些常量最好的类型是struct(结构)。关于struct我会在后面的章节详细讲解,在这里仅举一例说明常量的这种运用。例如,我们需要在.Net下使用FTP,那么一些特定的FTP代码就可以通过这种方式完成定义,如下所示:
public struct FtpCode
{
 public const string ConnectOk = "220";
 public const string RequiredPassword = "331";
 public const string LoginOk = "230";
 public const string PasvOk = "227";
 public const string CwdOk = "250";
 public const string PwdOk = "257";
 public const string TransferOk = "226";
 public const string ListOk = "150";
 public const string PortOK = "200";
 public const string NoFile = "550";
}

要使用这些常量,可以直接调用,例如FtpCode.ConnectOk。如果结构FtpCode仅用于本程序集内部,也可以把结构类型和内部的常量设置为internal。采用这种方式有三个好处:
1、集中管理全局常量,便于调用;
2、便于修改,一旦Ftp的特定代码发生变化,仅需要修改FtpCode中的常量值即可,其余代码均不受影响;
3、便于扩展。要增加新的Ftp代码,可以直接修改结构FtpCode,其余代码不受影响。

虽然说变量的值可以修改,但我们也可以定义只读的变量,方法就是在定义的时候加上关键字readonly。如下定义:
public readonly int number = 20;

变量number的值此时是只读的,不能再对其进行重新赋值的操作。在定义只读变量的时候,建议必须为变量赋予初值。如果不赋予初值,.Net会给与警告,同时根据其类型不同,赋予不同的初值。例如int类型赋初值为0,string类型赋初值为null。由于定义的只读变量其值不可修改,因此不赋初值的只读变量定义,没有任何意义,反而容易造成空引用对象的异常。

static的意义与const和readonly迥然不同。const仅用于常量定义,readonly仅用于变量定义,而static则和常量、变量无关,它是指所定义的值与类型有关,而与对象的状态无关。

前面我已介绍,所谓“对象”,可以称为一个类型的实例,以class类型为例,当定义了一个类类型之后,要创建该类型的对象,必须进行实例化,方可以调用其属性或者方法。例如User类型的Name、Password属性,SignIn和SignOut方法,就都是与对象相关的,要调用这些属性和方法,只能通过实例化对象来调用,如下所示:
User user = new User();
user.Name = "bruce zhang";
user.Password = "password";
user.SignIn();
user.SignOut();

然而,我们在定义类的成员时,也可以利用static关键字,定义一些与对象状态无关的类成员,例如下面的代码:
public class LogManager
{
 public static void Logging(string logFile,string log)
 {
  using (StreamWriter logWriter = new StreamWriter(logFile,true))
  {
   logWriter.WriteLine(log);
  }
 }
}
方法Logging为static方法(静态方法),它们与类LogManager的对象状态是无关的,因此调用这个方法时,并不需要创建LogManager的实例:
LogManager.Logging ("log.txt","test.");

所谓“与对象状态无关”,还需要从实例化谈起。在对一个类类型进行实例化操作的时候,实际上就是在内存中分配一段空间,用以创建该对象,并储存对象的一些值,如Name和Password等。对同一个类类型,如果没有特殊的限制,是可以同时创建多个对象的,这些对象被分配到不同的内存空间中,它们的类型虽然一样,却具有不同的对象状态,如内存地址、对象名、以及对象中各个成员的值等等。例如,我们可以同时创建两个User对象:
User user1 = new User();
User user2 = new User();

由于Name和Password属性是和对象紧密相关的,方法SignIn和SignOut的实现也调用了内部的Name和Password属性值,因此也和对象紧密相关,所以这些成员就不能被定义为静态成员。试想一下,如果把Name和Password属性均设置为静态属性,则设置其值时,只能采用如下形式:
User.Name = "bruce zhang";
User.Password = "password";

显然,此时设置的Name和Password就与实例user无关,也就是说无论创建了多少个User实例,Name和Password都不属于这些实例,这显然和User类的意义相悖。对于方法SignIn和SignOut,也是同样的道理。当然我们也可以更改方法的定义,使得该方法可以被定义为static,如下所示:
public class User
{
 public static void SignIn(string userName, string password)
 {
  //代码略
}
 public static void SignOut(string userName, string password)
 {
  //代码略
}
}

由于SignIn和SignOut方法需要调用的Name和Password值改为从方法参数中传入,此时这两个方法就与对象的状态没有任何关系。定义好的静态方法的调用方式略有不同:
User user = new User();
user.Name = "bruce zhang";
user.Password = "password";
User.SignIn(user.Name, user.Password);
User.SignIn(user.Name, user.Password);

两相比较,这样的修改反而导致了使用的不方便。因此,当一个方法与对象的状态有较紧密的联系时,最好不要定义为静态方法。

那么为什么在LogManager类中,我将Logging方法均定义为静态方法呢?这是因为该方法与对象状态没有太大的关系,如果将方法的参数logFile和log定义为LogManager类的属性,从实际运用上也不合理,同时也会导致使用的不方便。最重要的是,一旦要调用非静态方法,不可避免的就需要创建实例对象。这会导致不必要的内存空间浪费。毕竟LogManager类型对于调用者而言,仅在于其Logging方法,而和对象的状态没有太大的关系,因此并不需要为调用这个方法专门去创建一个实例。这一点是和User类型是完全不同的。

在一个类类型的定义中,既可以允许静态成员,也可以允许非静态成员。然而在一个静态方法中,是不允许直接调用同一类型的非静态方法的,如下所示:
public class Test
{
 private void Foo1()
 {
  //代码略;
 }
 public static void Foo2()
 {
  Foo1();  //错误;
 }
 public void Foo3()
 {
  Foo1();  //正确;
 }
}

在静态方法Foo2中,直接调用了同一类型Test下的私有非静态方法Foo1,将会发生错误;而非静态方法Foo3对Foo1的调用则正确。如要在静态方法Foo2中正确调用Foo1方法,必须创建Test类的实例,通过它来调用Foo1方法,修改如下:
 public static void Foo2()
 {
  Test test = new Test();
  testFoo1();  //正确;
 }

在Foo2方法中,创建了Test的实例,通过实例对象test来调用Foo1方法。需要注意的是虽然Foo1方法是private方法,但由于Foo2方法本身就在Test对象中,所以此时的私有方法Foo1是可以被调用的,因为对象的封装仅针对外部的调用者而言,对于类型内部,即使是private,也是可以被调用的。
 
对于类型的静态属性成员而言,具有和静态方法一样的限制。毕竟,从根本上说,类型的属性,其实就是两个get和set方法。

如果在类中定义了static的字段,有两种方式对其初始化。一是在定义时初始化字段,或者是在类型的构造器中为这些静态字段赋予初始值。例如:
class ExplicitConstructor
{
private static string message;
public ExplicitConstructor()
   {
message = "Hello World";
   }
   public static string Message
   {
     get { return message; }
   }  
}
class ImplicitConstructor
{
private static string message = "Hello World"; 
public static string Message
   {
     get { return message; }
   } 
}

在类ExplicitConstructor中,是利用构造器为静态字段message初始化值,而在类ImplicitConstructor中,则是直接在定义时初始化message静态字段。虽然这两种方式均可达至初始化的目的,但后者在性能上有明显的优势(有兴趣者,可以阅读我博客上的一篇文章http://wayfarer.cnblogs.com/archive/2004/12/20/78817.html)。因此,我建议当需要初始化静态字段时,应直接初始化。

如果对于静态字段未设置值,.Net会给出警告,并根据类型的不同赋予不同的初始值。此外,static还可以和readonly结合起来使用,定义一个只读的静态变量。但是static不能应用到常量的定义中。

在C# 1.x中,static并不能用来修饰类类型,也就是说,我们不能定义一个静态类。然而对于一个类类型,如果其成员均为静态成员,则此时实例化该类是没有意义的。此时,我们常常将构造器设置为private,同时将其类设置为sealed(sealed表明该类不可继承,关于sealed会在后面介绍)。这样就可以避免对类的实例化操作,如前面定义的LogManager,即可以修改定义:
public sealed class LogManager
{
 private LogManager()
 {}
 public static void Logging(string logFile,string log)
 {
  using (StreamWriter logWriter = new StreamWriter(logFile,true))
  {
   logWriter.WriteLine(log);
  }
 }
}

C# 2.0支持静态类的定义,方法是在类前面加上static关键字,如:
public static class LogManager{}

由于静态类不支持实例化操作,因此在静态类的定义中,不允许再添加sealed或abstract关键字,也不允许继承某个类或被某个类继承,而类的成员中,也只能是静态成员,且不能定义构造器。由于不存在类的继承关系,因此,静态类成员中,也不允许有protected或protected internal作为访问限制修饰符。

原创粉丝点击