面向对象编程(Object-Oriented Programming)简称OOP技术,是开发计算机应用程序的一种新方法、新思想。过去的面向过程编程常常会导致所有的代码都包含在几个模块中,使程序难以阅读,难以维护。在做一些程序修改时常常牵一动百,使以后的开发和维护不易进行。
而OOP技术经常要使用许多代码模块,每个模块都只提供特定的功能,并且彼此独立,这样就增大了代码重用的几率,更加有利于软件的开发、维护和升级。模块化的设计结构经常可以简化任务。
在面向对象中,算法与数据结构被看做是一个整体,称作类,现实世界中任何类的对象都具有一定的属性和操作,也总能用数据结构与算法两者合一地来描述,所以程序可以被这样定义:
对象=(算法+数据结构)
程序=(对象+对象+……)
因此程序就是许多对象在计算机中相继表现自己,而对象则是一个个程序实体。
类是对象概念在面向对象编程语言中的反映,是相同对象的集合。类描述了一系列在概念上有相同含义的对象,为这些对象统一定义了编程语言上的属性和方法。
类是C#中功能最为强大的数据类型。类定义了数据类型的数据和行为。在C#中,类的声明格式如下:
类的属性集 类的修饰符 关键字class 类名 继承方式 基类名
{
}
除class关键字和类名外,剩余的都是可选项。下面以汽车为例声明一个类,代码如下:
public class Car
{
}
public是类的修饰符,下面介绍几个常用的修饰符。
(1)new:仅允许在嵌套类声明时使用,表明类中隐藏了由基类中继承而来的、与基类中同名的成员。
(2)public:表示不限制对该类的访问。
(3)protected:表示只能从其所在类和所在类的子类进行访问。
(4)internal:只有其所在类才能访问。
(5)private:只有.NET中的应用程序或库才能访问。
(6)abstract:抽象类,不允许建立类的实例。
(7)sealed:密封类,不允许被继承。
对象是类的实例,是OOP应用程序的一个组成部件。这个组成部件封装了部分应用程序,这部分应用程序可以是一个过程、一些数据或一些更抽象的实体。
对象包含变量成员和函数类型。它所包含的变量组成了存储在对象中的数据,其中包含的方法可以具有访问对象的功能。复杂的对象可以不包含任何数据,只包含方法,表示一个过程。
在C#中和.NET Framework中的所有类型都是对象。变量类型是一个类,变量也是一个对象。
用属性和字段可以访问对象中包含的数据。对象数据用来区分不同的对象,同一个类的不同对象可能在属性和字段中存储了不同的值。包含在对象中的不同数据统称为对象的状态。字段和属性都可以输入,通常把信息存储在字段和属性中。属性和字段是不同的,属性不能直接访问数据,字段可以直接访问数据。在属性中可以添加对数据访问的限制,如有一个int型属性,可以限制它只能存储1~5的数字,但如果用字段就可以存储任何int型的数值。
通常在访问状态时提供属性,而不是字段,因为属性可以更好地控制访问过程和读写权限。除此之外,属性的可访问性确定了什么代码可以访问这些成员,可以声明为公有、私有或者其他更为复杂的方式。
下面的代码实现了访问Car类的对象和对象数据状态:
public class Car
{
public int number;
public string color;
private string _brand;
public Car()
{
}
public string brand
{
get
{
return _brand;
}
set
{
_brand = value;
}
}
}
下面代码在一个方法中实例化类对象并设置和访问数据状态:
private void button2_Click(object sender, EventArgs e)
{
string pa;
Car c = new Car();
c.brand = "奔驰";
c.color = "黑色";
pa = c.brand;
}
构造函数和析构函数
构造函数和析构函数是类中比较特殊的两种成员函数,主要用来对对象进行初始化和回收对象资源。一般来说,对象的生命周期是从构造函数开始,以析构函数结束。如果一个类含有构造函数,在实例化该类的对象时就会调用构造函数;如果含有析构函数则会在销毁对象时调用它。构造函数的名字和类名相同。析构函数和构造函数的名字相同,但析构函数要在名字前加一个波浪号(~)。当推出含有该对象的成员时,析构函数将自动释放这个对象所占用的内存空间。
每个类都有构造函数,在访问一个类的方法、属性或任何其他东西之前,首先执行的语句应是与类相应的构造函数。即使没有声明它,编译器也会自动构造一系列的构造函数。构造函数不声明返回类型,而且一般都是public类型,如果是private表明该类不能被实例化,这通常只用于静态成员的类。
析构函数是以类名加“~”来命名的。.NET系统有垃圾回收功能,当某个类的实例被认为不再有效,并符合析构条件时,.NET的垃圾回收功能就会调用该类的析构函数。
下面的代码实现了Car类析构函数的声明:
public class Car
{
public int number;
public string color;
private string _brand;
public Car()//构造函数
{
}
public ~Car()//析构函数
{
}
public string brand
{
get
{
return _brand;
}
set
{
_brand = value;
}
}
}
定义方法的基本格式如下:
方法的访问属性 返回类型方法名(参数列表)
{
方法的具体实现;
}
类中的功能大多数放在方法中实现。下面的代码实现了UpdateDatabase方法的定义:
public void UpdateDatabase(DataSet ds)
{
//具体实现代码;
}
方法可以向调用方返回值。如果返回类型(方法名称前列出的类型)不是void,则方法可以使用return关键字来返回值。如果语句中return关键字的后面是与返回类型匹配的值,则该语句将该值返回给方法调用方。return关键字还可以停止方法的执行。 如果返回类型为void,则可使用没有值的return语句来停止方法的执行。如果没有return关键字,方法执行到代码块末尾时即会停止。具有非void返回类型的方法才能使用return关键字返回值。例如,下面的两个方法使用return关键字来返回整数,代码如下: public class VoidTest
{
public int AddNum(int num1 ,int num2)
{
return num1 + num2;
}
public int SquareNum(int num)
{
return num * num;
}
}
方法的参数
如果方法要更改数值,有时需要传递值给方法并从方法获得返回值。下面是传递参数时的4种情况。
(1)值参数:声明时不带修饰符的参数是值参数,一个值参数相当于一个局部变量,初始值来自该方法调用时提供的相应参数。在方法内对值参数的操作不会改变传给方法时变量的值,因为将值类型传递给方法时,传递的是副本而不是对象本身。由于它们是副本,因此对参数所做的任何更改都不会在调用方法内部反映出来。之所以叫做值类型,是因为传递的是对象的副本而不是对象本身,传递的是值,而不是同一个对象。
(2)引用参数:如果要传递原值并修改它,使用引用参数就非常方便。因为引用参数传递了一个变量给方法而不仅仅传递它的值,所以对参数的操作会影响原值。在传递时变量必须被初始化。
(3)输出参数:在传递参数前加out关键字即可将该传递参数设作一个输出参数。输出参数用来返回一个结果。它和引用参数的区别是不必先初始化变量。例如:
static void Method(out int i)
(4)参数数组:参数数组必须用params修饰词明确指定。在方法的参数列表中只允许出现一个参数数组,而且在方法同时具有固定参数和参数数组的情况下,参数数组必须放在整个参数列表的{zh1},并且参数数组只允许是一维数组。
方法重载是指调用同一方法名,但使用不同数据类型的参数或不同的次序。只要类中有两个以上的同名方法,且使用的参数类型或者个数不同,编译器就可以判断在哪种情况下调用哪种方法。下面的代码实现了MethodTest方法的重载:
public int MethodTest(int i, int j)
{
}
public int MethodTest(int i)
{
}
public string MethodTest(string s)
{
}
在C#中可使用类来达到数据封装的效果,这样就可以使数据与方法封装成单一元素,以便于通过方法存取数据。除此之外,还可以控制数据的存取方式。
面向对象程序设计中一般以类作为数据封装的基本单位。类将数据和操作数据的方法结合成一个单位。在设计类时,不希望直接存取类中的数据,而是希望通过方法来存取数据。如此就可以达到封装数据的目的,方便以后维护、升级,也可以在操作数据时多一层判断,提高安全性。
封装还可以解决数据存取权限问题,使用封装可以将数据隐藏起来,形成一个封闭的空间,用户可以设置哪些数据只能在这个空间中使用,哪些数据可以在空间外部使用。如果一个类中包含敏感数据,则有些用户可以访问,有些用户却不能访问。如果不对这些数据的访问加以限制,那么后果是很严重的。所以,在编写程序时,要对类的成员使用不同的访问修饰符,从而定义它们的访问级别。
继承是OOP最重要的特性之一。任何类都可以从另外一个类继承,即这个类拥有它所继承类的所有成员。在OOP中,被继承的类称为父类或基类。C# 提供了类的继承机制,但C# 只支持单继承,不支持多重继承,即在C# 中一次只允许继承一个类,不能同时继承多个类。
利用继承机制,用户可以通过增加、修改或替换类中方法对这个类进行扩充,以适应不同的应用要求。利用继承,程序开发人员可以在已有类的基础上构造新类。继承使得类支持分类的概念。在日常生活中很多东西比较有条理,那是因为它们有着很好的层次分类。如果不用层次分类,则要对每个对象定义其所有的性质。使用继承后,每个对象就可以只定义自己的特殊性质。每一层的对象只需定义本身的性质,其他性质可以从上一层继承下来。
在C# 中,接口允许多继承,可以通过继承多个接口来实现类似于C++中的多重继承。
在继承一个基类时,成员的可访问性是一个重要的问题。子类不能访问基类的私有成员,但是可以访问其公共成员。子类和外部代码都可以访问公共成员。这就是说,只使用这两个可访问性,就可以让一个成员被基类和子类访问,同时也可以被外部的代码访问。
为了解决这个问题,C# 还提供了第3种可访问性:protected。只有派生类才能访问protected成员,基类和外部代码都不能访问protected成员。
除了成员的保护级别外,用户还可以为成员定义其继承行为。基类的成员可以是虚拟的,成员可以由继承它的类重写。子类可以提供成员的其他执行代码。这种执行代码不会删除原来的代码,仍可以在类中访问原来的代码,但外部代码不能访问它们。如果没有提供其他执行方式,外部代码就访问基类中成员的执行代码。
虚拟成员不能是私有成员,因为成员不能同时由子类重写,也不能访问它。基类还可以定义为抽象类。抽象类不能直接实例化,要使用抽象类就必须继承这个类,然后再实例化。
继承使得派生与基类的类在方法上有一定的重叠,因此可以使用相同的语法处理从同一个基类实例化的对象。
多态使得子类的实例可以直接赋予基类的变量,不需要进行强制类型转换,直接就可以通过这个变量调用基类的方法。
在派生于同一个类的不同对象上执行任务时,多态是一种极为有效的技巧,使用的代码最少。可以把一组对象放到一个数组中,然后调用它们的方法,这些对象不必是相同类型的对象,在这种情况下多态的作用就体现出来了。当然如果它们都继承自某个类,可以把这些派生类都放到一个数组中。如果这些对象都有同名方法,可以调用每个对象的同名方法。
C# 不支持多重继承,但是客观世界出现多重继承的情况又比较多。为了避免传统的多重继承给程序带来的复杂性等问题,C# 提出了接口的概念。通过接口可以实现多重继承的功能。
实现接口的类或结构要与接口的定义严格一致。接口描述可属于任何类或结构的一组相关行为。接口可由方法、属性、事件、索引器或这4种成员类型的任何组合构成。接口不能包含字段。接口成员一定是公共的。
类和结构可以像类继承基类或结构一样从接口继承,而且可以继承多个接口。当类或结构继承接口时,它继承成员定义但不继承实现。若要实现接口成员,类中的对应成员必须是公共的、非静态的,并且与接口成员具有相同的名称和签名。类的属性和索引器可以为接口上定义的属性或索引器定义额外的访问器。例如,接口可以声明一个带有get访问器的属性,而实现该接口的类可以声明同时带有get和set访问器的同一属性。但是,如果属性或索引器使用显式实现,则访问器必须匹配。
接口可以继承其他接口。类可以通过其继承的基类或接口多次继承某个接口。在这种情况下,如果将该接口声明为新类的一部分,则类只能实现该接口一次。如果没有将继承的接口声明为新类的一部分,其实现将由声明它的基类提供。基类可以使用虚拟成员实现接口成员,在这种情况下,继承接口的类可通过重写虚拟成员来更改接口行为。
接口的声明采用下列格式:
修饰符 interface 接口名称 :继承的接口列表
{
接口内容;
}
其中,除interface 和接口名称,其他的都是可选项,下面是一个定义接口的例子,代码如下:
public interface iSomeInterface
{
void UpdateDataBase();
}
一个接口可以从一个或多个基接口继承。例如:
interface iOthrerInterface :iSomeInterface,iCom
{
int CompareTo(object obj);
}
域也叫成员变量,它表示存储位置,用来保存类的各种数据信息。域是C#中不可缺少的一部分。这个成员代表一个与一个对象或类相关的变量。一个域声明把一个或多个给定类型的域引入。域的声明非常简单,例如:
private Thread t = null;
域的修饰符可以是new、public、protected、internal、private、static、readonly。
一个声明了多个域的域声明和单个域的多个声明相等。例如:
private int a;
private int b;
private int c;
等同于:
private int a,b,c;
域又分为静态域和实例域。当域的声明中包含static修饰符时,域就是静态域。如果域的声明不包含static修饰符,则域就是实例域。实例域属于具体的对象,为特定的对象专有。当创建一个对象时,它的实例变量赋予对应于此对象的值,而且这些值在对象的生存期间与对象保持一致。
静态域属于类,是所有对象共同拥有的。C#中严格规定实例域只能通过对象来获取,静态域只能通过类来获得。
属性是一种用于访问对象或类的特性的成员。属性可以包括字符串的长度、字体的大小、窗体的标题、客户的名称等。属性是成员的自然扩展,二者都是关联类型的命名成员。访问成员和属性的语法是相同的。
与成员不同的是,属性不表示存储位置。属性有访问器,这些访问器指定在它们的值被读取或写入时需要执行的语句。因此属性提供了一种机制,它把读取和写入对象的某些特性与一些操作关联起来。它们提供灵活的机制来读取、编写或计算私有成员的值,可以像使用公共数据成员一样使用属性,它们是称为“访问器”的特殊方法。这使得数据在可被轻松访问的同时仍能提供方法的安全性和灵活性。
属性结合了字段和方法的多个方面。对于对象的用户,属性显示为字段,访问该属性需要xx相同的语法。对于类的实现者,属性是一个或两个代码块,表示一个get访问器和/或一个set访问器。当读取属性时,执行get访问器的代码块;当向属性分配一个新值时,执行set访问器的代码块。不具有set访问器的属性被视为只读属性。不具有get访问器的属性被视为只写属性。同时具有这两个访问器的属性是读写属性。
与成员不同,属性不作为变量来分类。因此,不能将属性作为ref参数或out参数传递。
属性具有多种用法。它们可在允许更改前验证数据;它们可透明地公开某个类上的数据,该类的数据实际上是从其他源(例如数据库)检索到的;当数据被更改时,它们可采取行动,例如,引发事件或更改其他字段的值。
属性在类模块内是通过以下方式声明的:指定字段的访问级别、属性的类型、属性的名称,然后声明get访问器和/或set访问器的代码模块。例如:
public class Date
{
private int Day = 7;
public int day
{
get
{
return Day;
}
set
{
if ((value > 0) && (value < 8))
{
Day = value;
}
}
}
}
get访问器体与方法体相似,必须返回属性类型的值。执行get访问器相当于读取字段的值。例如,当正在从get访问器返回私有变量并且启用了优化时,对get访问器方法的调用由编译器进行内联,因此不存在方法调用的系统开销。然而,由于在编译时编译器不确定在运行时实际调用哪个方法,无法内联虚拟get访问器。
set访问器类似于返回类型为void的方法。它使用称为value的隐式参数,此参数的类型是属性的类型。
事件是类在发生其关注的事情时用来提供通知的一种方式。例如,封装用户界面控件的类可以定义一个在用户单击该控件时发生的事件。控件类不关心单击按钮时发生了什么,但它需要告知派生类单击事件已发生,然后,派生类可选择如何响应。
C# 是一个xx面向对象组件的程序,所以允许编写自定义的事件。实际上,事件就是一个回呼(callback)的函数指针,在C#中是通过delegate达成的。
在.NET的事件模型中,主要的角色有事件发行者和事件订阅者。事件的发行者是触发事件的对象,而事件的订阅者指的是在某种事件发生时被通知的人。
当发生与某个对象相关的事件时,类和结构会使用事件将这一对象通知给用户。这种通知即称为“引发事件”。引发事件的对象称为事件的源或发送者。对象引发事件的原因很多,响应对象数据的更改、长时间运行的进程完成或服务中断。例如,一个对象在使用网络资源时如果丢失网络连接,则会引发一个事件。表示用户界面元素的对象通常会引发事件来响应用户操作,如按钮单击或菜单选择。
如果要在程序中使用事件,就必须先声明事件。下面来声明一个事件,代码如下:
public delegate void MrEventDelegate(object sender, System.EventArgs e);
在.NET Framework 中事件的签名中,通常{dy}个参数为引用事件源的对象,第二个参数为一个传送与事件相关的数据的类。但是,在C#中并不强制使用这种形式,只要事件签名返回 void,其他方面可以与任何有效的委托签名一样。
向类中添加事件需要使用 event 关键字,并提供委托类型和事件名称。例如:
public class EventSend
{
public event MrEventDelegate TestEvent;
private void RaiseTestEvent() { /* ... */ }
}
事件可标记为public、private、protected、internal 或protectedinternal。这些访问修饰符定义类的用户访问事件的方式。
若要引发事件,类可以调用委托,并传递所有与事件有关的参数。然后,委托调用已添加到该事件的所有处理程序。如果该事件没有任何处理程序,则该事件为空。因此在引发事件之前,事件源应确保该事件不为空以避免NullReferenceException。若要避免争用条件({zh1}一个处理程序会在空检查和事件调用之间被移除),在执行空检查和引发事件之前,事件源还应创建事件的一个副本。下面的代码实现了触发MrEventDelegate事件:
private void RaiseTestEvent()
{
MrEventDelegate temp = TestEvent;
if (temp != null)
{
temp(this, new System.EventArgs());
}
}
每个事件都可以分配多个处理程序来接收该事件。这种情况下,事件自动调用每个接收器;无论接收器有多少,引发事件只需调用一次该事件。
要接收某个事件的类,可以创建一个方法来接收该事件,然后向类事件自身添加该方法的一个委托。这个过程称为“订阅事件”。
首先,接收类必须具有与事件自身具有相同签名(如委托签名)的方法。然后,该方法(称为事件处理程序)可以采取适当的操作来响应该事件。例如:
public class EventReceiver
{
public void ReceiveTestEvent(object sender, System.EventArgs e)
{
System.Console.Write("Event received from ");
System.Console.WriteLine(sender.ToString());
}
}
每个事件可有多个处理程序。多个处理程序由源按顺序调用。如果一个处理程序引发异常,还未调用的处理程序,则没有机会接收事件。因此,建议事件处理程序迅速处理事件并避免引发异常。
若要订阅事件,接收器必须创建一个与事件具有相同类型的委托,并使用事件处理程序作为委托目标。然后,接收器必须使用加法赋值运算符(+=)将该委托添加到源对象的事件中。例如:
public void Subscribe(EventSend send)
{
TestEventDelegate temp = new TestEventDelegate(ReceiveTestEvent);
send.TestEvent += temp;
}
若要取消订阅事件,接收器可以使用减法赋值运算符(− =)从源对象的事件中移除事件处理程序的委托。例如:
public void UnSubscribe(EventSource send)
{
TestEventDelegate temp = new TestEventDelegate(ReceiveTestEvent);
send.TestEvent -= temp;
}
索引器允许类或结构的实例按照与数组相同的方式进行索引。索引器类似于属性,不同之处在于它们的访问器采用参数。它可以使得像数组那样对对象使用下标。它提供了通过索引方式方便地访问类的数据信息的方法。
声明类或结构上的索引器时,可以使用this关键字,例如:
public int this[int index] //声明索引器
{
// get and set 访问
}
索引器的修饰符有new、public、protected、internal、private、virtual、sealed、override、abstract和extern。当索引器声明包含extern修饰符时,称该索引器为外部索引器。因为外部索引器声明不提供任何实际的实现,所以它的每个访问器声明都由一个分号组成。
索引器的签名由其形参的数量和类型组成。它不包括索引器类型或形参名。如果在同一类中声明一个以上的索引器,则它们必须具有不同的签名。
索引器值不归类为变量,因此,不能将索引器值作为ref或out参数来传递。
下面用一个例子来说明如何声明和使用索引器。
在本示例中,定义了一个泛型类,并为其提供了简单的get和set访问器方法(作为分配和检索值的方法)。Program 类为存储字符串创建了此类的一个实例。代码如下:
class SampleCollection<T>
{
private T[] arr = new T[100];
public T this[int i]
{
get
{
return arr[i];
}
在本示例中,定义了一个泛型类,并为其提供了简单的get和set访问器方法(作为分配和检索值的方法)。Program 类为存储字符串创建了此类的一个实例。代码如下:
class SampleCollection<T>
{
private T[] arr = new T[100];
public T this[int i]
{
get
{
return arr[i];
}
set
{
arr[i] = value;
}
}
}
下面讲解如何使用上述代码实现的索引器,具体代码示例如下:
class Program
{
static void Main(string[] args)
{
SampleCollection<string> 锁 = new SampleCollection<string>();
s[0] = "索引器的使用";
System.Console.WriteLine(锁[0]);
}
}
C# 并不将索引类型限制为整数。例如,对索引器使用字符串可能是有用的。通过搜索集合内的字符串并返回相应的值,可以实现此类索引器的功能。由于访问器可被重载,字符串和整数版本可以共存。