类
类
前言:类中的数据和函数称为类的成员。成员的可访问性可以是public protected internal protected、private或internal。
1.数据成员:
数据成员是包含类的数据(字段)、常量和事件的成员。数据成员可以是静态数据。类成员总是实例成员,除非用static进行显示的声明。字段是与类相关的变量。我们写代码先来表示一个类:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace _1.类的结构的区别 { public class PhoneCustomer2 { public const string DayOfSendingBill = "Monday"; //发送账单的日期 public int CustomerID; //员工的ID号 public string FirstName; //姓 public string LastName; //名 } }
一旦实例实例化了PhoneCustomer对象,就可以使用语法Object.FieldName来访问这些字段,如下实例所示:
static void Main(string[] args) { //类的声明 PhoneCustomer2 phoneCustomer2 = new PhoneCustomer2(); //给字段进行赋值的操作 phoneCustomer2.FirstName = "Simon"; }
常量与类的关联方式与变量与类的关联方式相同。使用 const关键字来声明常量。如果把它声明为public,就可以在类的外部进行访问它。
public class PhoneCustomer2 { public const string DayOfSendingBill = "Monday"; //发送账单的日期 public int CustomerID; //员工的ID号 public string FirstName; //姓 public string LastName; //名 }
事件是类的成员,在发生某些行为(如改变类的字段或者属性,或者进行了某种形式的用户交互操作)时,它可以让对象通知调用方。客户包含所谓“事件处理程序”的代码来响应事件(事件在别的博客中进行详细的介绍)。
2.函数成员:
函数成员提供了操作类中数据的某些功能,包括方法、属性、构造函数和终结器、运算符以及索引器。
2.1:方法是某个类相关的函数,与数据成员一样,函数成员默认是实例成员。使用static修饰符可以把方法定义为静态方法。
2.2:属性是可以从客户端访问的函数组,其访问方式与访问类的公共字段类似。c#为读写类中的属性提供了专用的语法,所以不必使用那些名称中嵌有Get()或者Set()的方法。因为属性的这种方法不同于一般函数的语法,在客户端代码中,虚拟的对象被当作实际的东西。
2.3:构造函数是在实例化对象是自动调用的特殊的函数。他们必须与所属的类同名。且不能有返回类型、构造函数用于初始化字段的值。
2.4:终结器类似于构造函数,但是在CLR检测到不再需要某个对象时调用它。他们的名称与类相同。但是前面有一个“~”符号。不可能预测什么时候调用终结器(本章不做详细的解释)。
2.5:运算符执行的最简单的操作就是加法与减法。在两个整数相加时,严格的说,就是对整数使用“+”运算符。c#还允许指定把已有的运算符应用于自己的类(运算符的重载)。
2.6:索引器允许对象以数组或者集合的方式进行索引。
2.7:方法:
注意:正式的c#术语中区分函数与方法。“函数成员”不仅包含方法,也包含类或结构的一些非数据成员,如索引器、运算符、构造函数和析构函数,甚至还有属性。这些都不是数据成员。字段、常量、事件才是数据成员。
2.7.1:方法的声明:
在c#中,方法的定义包含任意方法的修饰符(如方法的可访问性)、返回值的类型,然后依次是方法名、输入参数的列表(用圆括号表示)和方法体(用花括号括起来)。实例如下:
[访问修饰符]返回类型 方法名([参数] )
{
方法的主体
}
每个参数都包括参数的类型名和在方法体中的引用的名称。但如果方法有返回值,return语句就必须与返回值一起使用。以指定出口点:
public bool IsSquare(Rectangle rect) { return (rect.Height == rect.Width); }
如果没有返回值的话,就把返回类型指定为void,因为不能省略返回类型。如果方法不带参数,仍需要在方法名的后面包含一对空的圆括号(),此时return语句是可选的--当到达花括号时,方法会自动返回,注意方法可以包含任意多条return语句。
/// <summary> /// 是否为正整数 /// </summary> /// <param name="value">传入的值</param> /// <returns><返回true或者是false/returns> public bool IsPositive(int value) { if (value < 0) { return false; } return true; }
2.7.2:方法的调用:
在下面的例子中,MathTest说明了类的定义和实例化、方法的定义和调用的语法。除了包含Main()方法的类之外,它还定义了MathTest,该类包含了几个方法和一个字段:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace _1.类的结构的区别 { /// <summary> /// 数学类 /// </summary> public class ClassMathTest { private int _value; //数值字段的定义 /// <summary> /// 得到相乘的结果 /// </summary> /// <returns>返回最后的结果</returns> public int GetSquare() { return _value * _value; } /// <summary> /// 得到结果 /// </summary> /// <param name="x">外部传入的值</param> /// <returns>得到结果</returns> public static int IntGetSquareOf(int x) { return x * x; } /// <summary> /// 得到数值 /// </summary> /// <returns>返回数值</returns> public static double GetPi() { return 3.14159; } } }
客户端程序的调用:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace _1.类的结构的区别 { class Program { static void Main(string[] args) { //类的实例化 ClassMathTest classMathTest = new ClassMathTest(); Console.WriteLine("调用静态的方法得到的值:{0}", ClassMathTest.IntGetSquareOf(5)); Console.WriteLine("调用静态的方法得到的值:{0}", ClassMathTest.GetPi()); //实例方法的调用 Console.WriteLine("调用实例方法得到的值:{0}", classMathTest.GetSquare()); Console.ReadKey(); } } }
结果如下:
从代码中可以看出,MathTest类包含一个字段和一个方法,该字段包含一个数字,该方法计算该数字的平方。这个类还包含两个静态的方法。一个返回pi的值,另一个计算作为参数传入的数字的平方。
这个类的功能并不是设计c#程序好例子,例如,GetPi()通常作为const字段来执行,而好的设计应使用目前还没有介绍的概念。
2.7.3:给方法传递参数:
参数可以通过值传递和引用传递的方式传递给方法。在变量通过引用传递给方法时,被调用的方法得到的就是这个变量,更准确的说,是指向内存中变量的指针。所以在方法内部对变量进行的任何的改变在方法退出后仍然有效。而如果变量通过值传递的方式传送给方法,被调用的方法得到的是变量的一个相同的副本。也就是说,在方法退出后,对变量进行修改会丢失。对于复杂的数据类型,按引用传递的效率更高,因为在按值传递的时候,必须赋值大量的数据。
在c#中,除非特别指定,所有的引用类型都通过引用传递,所有的值类型都通过值传递。但是,在理解引用类型的含义时需要注意。因为引用类型的变量值包含对象的引用。作为参数传递的正是这个引用,而不是对象的本身。所以底层的对象的修改会保留下来。相反,值类型的变量包含的是实际的数据,所以传递给方法的是数据本身的副本。例如:int通过值传递给方法,对应方法对该int的值所做的任何的改变都没有改变原int对象的值。但是如果把数组或者其他引用类型(如类)传递给方法,对应的方法就会使用该引用改变这个数组的值,而新值会反射在原始数组对象上。我们下面例子说明参数的值类型的引用类型的区别:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace _2.给方法传递参数 { public class ParameterTest { /// <summary> /// 静态方法的调用 /// </summary> /// <param name="ints">数组</param> /// <param name="i">数值</param> public static void SomeFunction(int[] ints, int i) { ints[0] = 100; i = 100; } } }
客户端程序的调用:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace _2.给方法传递参数 { class Program { static void Main(string[] args) { int i = 0; int[] ints = { 0, 1, 2, 3, 4, 8 }; Console.WriteLine("i={0}", i); Console.WriteLine("ints[0]={0}", ints[0]); //方法的调用 ParameterTest.SomeFunction(ints, i); Console.WriteLine("i={0}", i); Console.WriteLine("ints[0]={0}", ints[0]); Console.ReadKey(); } } }
截图如下:
这时候我们发现i的值保持不变,而在ints中改变的值在原始的数组中也改变了。注意字符串是不可变的(如果改变字符串的值,就会创建一个全新的字符串),所以字符串无法采用一般引用类型的行为方式。在方法的调用的时候,对字符串所做的任何的修改都不会影响到原始的字符串(在别的博客中做详细的介绍)。
2.7.4:ref参数
如前所述,通过值传送变量是默认的。也可以迫使值参数通过引用传递给方法。为此,要使用ref关键字。如果把一个参数传递给方法,且这个方法的输入参数中带有ref关键字,则该方法对变量所做的任何的改变都会影响到原始对象的值。
这样我们对代码稍作修改(添加ref关键字):
public static void SomeFunction(int[] ints, ref int i) { ints[0] = 100; i = 100; }
c#仍要求对传递给方法的参数进行初始化,理解这一点也非常重要。在传递方法之前,无论是按值传递,还是按引用传递,任何的变量都必须初始化。
2.7.5:out参数:
c#要求变量在被引用的前必须用一个初值进行初始化。尽管在吧输入变量传递给函数前,可以用没有意义的值初始化它们,因为函数将使用真实、有意义的值初始化他们,但是这样做是没有必要的。有时还会引起混乱。但是有一种方法能够简化c#编译器所坚持的输入参数的初始化。
我们可以使用out参数来初始化。在方法的时候输入参数前面加上out前缀的时候,传递给该方法的变量可以不进行初始化。该变量通过引用传递。所以在从被调用的方法中返回时,对应方法对该变量进行的任何的改变都会保留下来。在调用该方法的时候,还需要使用out关键字。与在定义该方法时一样:
public static void SomeFunction(out int i) { //在其方法的内部必须为其赋值 i = 100; }
方法的调用:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace _2.给方法传递参数 { class Program { static void Main(string[] args) { int j; ParameterTest.SomeFunction(out j); Console.WriteLine(j); Console.ReadKey(); } } }
3:属性:
属性的概念:它是一个方法或者一对方法。在c#中我们可以这样定义属性:
private string _name; public string Name { get { return _name; } set { _name = value; } }
Get访问器不带任何的参数,且必须返回属性声明的类型。也不应为Set访问器指定任何显示参数,但是编译器假定它带有一个参数,其类型也和属性相同。并表示为Value。例如上面的代码中包含一个属性Age,它设置了一个字段age,在这个例子中,age表示属性Age的后备变量。
在这里注意的是:我们采用c#的区分大小写模式,使用相同的名称,但共有属性采用Pascal大小写形式命名。如果存在一个等价的私有的字段,则它采用camel大小写形式命名。我们喜欢使用把下划线作为前缀的字段名,如 _age 这会为识别字段提供极大的便利。
3.1:自动属性的实现:
如果属性的set和get访问器中没有任何的逻辑。就可以使用自动实现的属性。这种属性会自动的实现后备成员变量。前面的Age实例的代码如下:
public string Name { get; set; }
不需要声明private int _age;,编译器会自动的创建它。
使用了自动实现的属性,就不能再属性设置中验证实行的有效性。所以在上面的例子中,不能检验是否设置了无效的年龄。但是必须有两个访问器。尝试把该属性设置为只读属性,就会报错。但是,每个访问器的访问的级别可以不同。因此,下面的代码是合法的:
public string Name { get; private set; }
4:构造函数:
声明基本的构造函数的语法就是生命一个与包含的类同名的方法,但是该方法没有返回类型:
public class Person { /// <summary> /// 构造函数的定义 /// </summary> public Person() { } }
一般的情况下,如果我们没有提供任何的构造函数的话,编译器会在后台创建一个默认的构造函数。这是一个非常基本的构造函数,它只能把所有的成员字段初始化为标准的默认值(例如,引用类型为空引用,数值数据类型为0,bool为false),这通常就足够了,否则就需要编写自己的构造函数了。构造函数的重载遵循与其他方法相同的规则。换言之,可以为构造函数提供任意多的重载,只是他们的签名有明显的区别:
/// <summary> /// 构造函数的定义 /// </summary> public Person() { } public Person(string name) { }
但是如果提供了带参数的构造函数,编译器就不会自动提供了默认的构造函数。只是在没有定义任何构造函数的时候,编译器才会自动提供默认的构造函数。在下面的例子中,因为定义了一个带单个单数的构造函数,编译器会假定这是可用的唯一构造函数,所以他不会隐式的提供其他的构造函数:
public class Person { private string _name; public string Name { get { return _name; } set { _name = value; } } public Person(string name) { this.Name = name; } }
上面的代码还说明,一般的使用this关键字区分成员字段和同名的参数。如果试图使用无参数的构造函数实例化Person,就会得到一个编译错误。
我们还可以构造函数定义为private和protected,这样不相关的子类就不能访问它了。
5:静态构造函数:
当然我们也可以给类编写无参数的静态的构造函数。这种构造函数只执行一次。而上面的构造函数是实例构造函数,只要创建类的对象,就会执行它。编写静态的构造函数的一个原因是:类有一些静态字段或者属性,需要在第一次使用类之前,从外部源中初始化这些静态字段和属性。
注意:静态构造函数没有访问修饰符,其他的c#代码从来不调用它,但是在加载类时,总是由.Net运行库调用它,所以像public或者private这样的修饰符就没有任何的意义。处于这样的原因,静态构造函数不能带有任何的参数。一个类也只能有一个静态构造函数。很显然,静态构造函数只能访问类的静态成员,不能访问类的实例成员。
无参数的实例构造函数与静态的构造函数可以在同一个类中出现。尽管参数列表相同。但是这并不矛盾,因为在加载类是执行静态构造函数,而在创建实例时执行实例构造函数,所以何时执行哪个构造函数不会有冲突。
下面用一个例子来说明静态构造函数的用法,为了简单起见,假定只有一个用户首选项——BackColor,它表示要早应用程序中使用的背景色。假定该首选项在工作日中的背景色是红色。在周末的时候是绿色。代码如下:
public class UsePreferences { //字段为只读类型,只能在构造函数中设置 public static readonly ConsoleColor BackColor; /// <summary> /// 静态的构造函数 /// </summary> static UsePreferences() { //表示时间上的一刻 DateTime now = DateTime.Now; //如果是星期六或者是星期日 if (now.DayOfWeek == DayOfWeek.Saturday || now.DayOfWeek == DayOfWeek.Sunday) { //则背景色为绿色 BackColor = ConsoleColor.Green; } else { //否则为红色 BackColor = ConsoleColor.Red; } } /// <summary> /// 私有的构造函数 /// </summary> private UsePreferences() { } }
客户端代码的调用如下:
Console.WriteLine("背景的颜色是:{0}", UsePreferences.BackColor.ToString()); Console.ReadKey();
6.从构造函数中调用构造函数(使用this关键字):
有时,在一个类中有几个构造函数,以容纳某些可选参数,这些构造函数包含一些共有的代码。代码如下:
private string _description; private uint _nWheels; public Car(string description, uint nWheels) { this._description = description; this._nWheels = nWheels; } public Car(string description) { this._description = description; this._nWheels = 4; }
这两个构造函数初始化了相同的字段,显然最好把所有的代码放在一个地方。c#有一个特殊的语法,称为构造函数初始化器,可以实现此目的(使用this关键字):
private string _description; private uint _nWheels; public Car(string description, uint nWheels) { this._description = description; this._nWheels = nWheels; } public Car(string description) : this(description, 0) { }
客户端代码的调用:
static void Main(string[] args) { Car aoDi = new Car("奥迪"); }