C# 2012程序设计实践教程 (清华电脑学堂)
上QQ阅读APP看书,第一时间看更新

3.3 常见成员

类和结构(第4章进行介绍)都具有表示其数据和行为的成员,类的成员包括在类中声明的所有成员,以及在该类的继承层次结构中的所有类中声明的所有成员(构造函数和析构函数除外)。父类中的私有成员被继承,但是不能从派生类中访问。

具体来说,类或结构中可包含的成员有字段、常量、属性、方法、事件、运算符、索引器、构造函数、析构函数和嵌套类型等。这些成员的修饰符可以是public、protected、private、internal、sealed、abstract、virtual、override、readonly和const。本节将对字段、常量、属性和方法进行详细介绍。

3.3.1 字段

字段是在类或结构中声明的任意类型的变量。字段可以是内置数值类型,也可以是其他类的实例。例如,日历类可能具有一个包含当前日期的字段。在类或结构中可以拥有实例字段或者静态字段,或者同时拥有两者。实例字段特定于类型的实例,如果拥有T类和实例字段F,可以创建类型T的两个对象,并修改每个对象中F的值,这不影响另一对象中的该值。相比之下,静态字段属于类本身,在该类的所有实例中共享。对实例A所做的更改将立刻呈现在实例B和C上(如果它们访问该字段)。

字段通常存储这样的数据:该数据必须可供多个类方法访问,并且其存储期必须长于任何单个方法的生存期。例如,表示日历日期的类可能有三个整数字段:一个表示月份,一个表示日期,还有一个表示年份。不在单个方法范围外部使用的变量应在方法体自身范围内声明为局部变量。

在C#类中,声明一个字段的语法如下:

    修饰符 字段类型 字段名称

其中,修饰符可以是public、protected、private、internal、sealed、abstract、virtual、override、readonly和const。public、protected、private和internal又称为可访问修饰符。

【范例6】

创建公有的名称为Animal的类,并在该类中添加一个公有字段和两个私有字段,代码如下。

    public class Animal
    {
        public int animalId;                       //动物ID
        private string animalTypeName;              //动物类型名称
        private string animalName;                  //动物名称
    }

如果要在其他类中访问Animal类的字段,首先要创建Animal类的实例对象,然后在对象名称之后添加一个圆点(即“.”),再添加该字段的名称。如访问Animal类中的animalId字段,并为其赋值,代码如下。

    Animal animalObj = new Animal();
    animalObj.animalId = 1;

由于Animal类中的其他两个字段animalTypeName和animalName都是private类型的,因此,在其他类(除Animal类)中不能进行访问,也不能进行赋值。

【范例7】

声明字段时可以用赋值运算符为字段指定初始值。如指定animalTypeName字段的默认值为“猫咪”,animalName字段的默认值是“猫咪白白”,代码如下。

    public class Animal
    {
        public int animalId;                                  //动物ID
        private string animalTypeName = "小猫咪";          //动物类型名称
        private string animalName = "猫咪白白";            //动物名称
    }

3.3.2 常量

常量是在编译时已知并在程序的生存期内不发生更改的不可变值。在C#中声明常量需要通过const关键字,只有C#内置类型(System.Object除外)可以声明为const。

使用常量时,常量必须在声明时初始化;而且可以同时声明多个相同类型的常量;如果不会造成循环引用,用于初始化一个常量的表达式可以引用另一个常量。为了区分常量和字段,一般情况下,将常量的名称定义为大写。

【范例8】

开发者在计算圆的周长和面积时,需要使用到一个固定的常量——圆周率。因此,可以将圆周率声明为常量PI,然后在计算圆的周长和面积时直接调用。步骤如下。

(1)创建名称是Constants的类,在该类中声明常量PI,代码如下。

    public class Constants
    {
        public const double PI = 3.14159;       //定义圆周率
    }

(2)在Program.cs文件的Main()方法中添加代码,提示用户输入半径并获取用户输入的半径,然后计算圆的周长和面积并在控制台输出,代码如下。

    Console.Write("请输入圆的半径:");
    int radius = Convert.ToInt32(Console.ReadLine());  //获取用户输入的半径
    Console.WriteLine("圆的周长是:{0}", 2 * Constants.PI * radius);
    Console.WriteLine("圆的面积是:{0}", Constants.PI * radius * radius);

(3)运行Program.cs文件中的代码测试,在控制台中输入半径4并计算结果,如图3-1所示。

图3-1 常量的使用

用户自定义的类型(包括类、结构和数组)不能为const,这需要使用readonly修饰符创建在运行时初始化一次即不可更改的类、结构或数组。const和readonly两个修饰符的区别在于以下几个方面。

(1)const字段只能在该字段的声明中初始化。readonly字段可以在声明或构造函数中初始化。因此,根据所使用的构造函数,readonly字段可能具有不同的值。

(2)const字段是编译时常数,而readonly字段可用于运行时常数。

(3)const默认就是静态的,不需要使用static声明常量,而readonly如果设置成静态的就必须显式声明。

(4)const对于引用类型的常数,可能的值只能是string和null。readonly可以是任何类型。

3.3.3 属性

属性可以说是字段的延伸,对于对象来说,属性可以看作是字段,访问属性与访问字段的语法和结果一样;对于类来说,属性是一个或两个语句块,它的两个核心的代码块分别是get属性访问器和set属性访问器。设置属性的值时会执行set代码块,取属性的值时访问get代码块。get访问器和set访问器都是可以省略的,不具有set访问器的属性是只读属性,不具有get访问器的属性是只写属性,同时具有这两个访问器的属性是读写属性,其用法和特点如下。

(1)get访问器与方法相似。它必须返回属性类型的值作为属性的值,当引用属性时,若没有为属性赋值,则调用get访问器获取属性的值。

(2)get访问器必须以return或throw语句终止,并且控制权不能离开访问器。

(3)get访问器除了直接返回字段值,还可以通过计算返回字段值。

(4)set访问器类似于返回类型为void的方法。它使用属性类型的value隐式参数,当对属性赋值时,用提供新值的参数调用set访问器。

(5)在set访问器中,对局部变量声明使用隐式参数名称value是错误的。

注意

属性和字段的访问方式是相同的。与字段不同的是属性不作为变量来分类,所以不能将属性作为ref参数和out参数传递。

当一个类中的字段过多时,这些字段并不安全,而且也不一定符合开发者的要求。举例来说,假设表示学生的Student类中包含一个age字段,该字段要求学生的年龄在12~25岁之间。通过字段指定时,如果用户输入的内容不合法,那么需要通过条件语句进行判断,如果使用属性对字段进行封装,那么可以直接在访问器中进行设置。

【范例9】

创建Student类,该类中包含两个私有字段和两个公有属性,这两个属性是对字段的封装,代码如下。

    public class Student {
       private string name;            //学生姓名
       private int age;                //学生年龄
       public string Name {
    get { return name; }
    set { name = value; }
    }
    public int Age {
       get { return age; }
       set {
           if (age < 12 || age > 25)
              age = 25;
           else
              age = value;
       }
     }
    }

在Program.cs文件的Main()方法中添加代码,为Student类中的字段赋值,并输出属性的值测试,代码如下。

    Student stu = new Student();
    stu.Age = 100;
    Console.WriteLine("输出年龄:{0}",stu.Age);

运行Program.cs文件查看输出结果,内容如下。

    输出年龄:25

从输出代码中可以看出,在Age属性中对age的处理已经起了作用,因此,这里输出的结果不是100,而是25。

属性的访问修饰符可以是public、private、protected和internal,除了这些可访问修饰符,还可以使用其他的修饰符。如用static将属性声明为静态的,用virtual将属性标记为虚属性。另外,使用sealed修饰属性,表示属性对派生类不再是虚拟的;使用abstract声明属性,表示在派生类中可以对其实现。

3.3.4 方法

方法定义类可以执行的操作。方法可以接收提供输入数据的参数,并且可以通过参数返回输出数据。方法还可以不使用参数而直接返回值。可以为方法指定访问级别(如public和private)、可选修饰符(如abstract或sealed)、返回值、名称和任何方法参数。在类或结构中声明方法的语法如下:

    访问级别 [可选修饰符] 返回值 名称(参数列表){
        //方法代码
    }

方法的参数包含在小括号中,并且多个参数之间通过逗号进行分隔。括号中可以不包含参数,如果没有参数,则表示方法不需要参数。方法的返回值可以是内置类型(如int或double),也可以是自定义的类型;如果没有返回值,则直接使用void。

【范例10】

创建Motorcycle类,并在该类中定义三个方法,代码如下。

    public class Motorcycle
     {
        public void StartEngine() {
        }
        protected void AddGas(int gallons) {
        }
        public virtual int Drive(int miles, int speed) {
            return 1;
        }
     }

其中,AddGas()方法和Drive()方法中的参数称为形式参数,即形参。在调用时,向方法中传入的参数称为实际参数,即实参。

一个类中可包含多个方法,有些方法之间存在着一定的关系。方法重载是指在同一个类中方法同名、参数不同、调用时根据实参的形式,选择与它匹配的方法执行操作的一种技术。这时的参数不同包含三种情况:参数的类型不同;参数的个数不同;参数的个数相同时它们的先后顺序不同。

注意

方法重载在两种情况下会认为是同一个方法,这样的两个方法不可以在同一个类里,否则系统会报错。第一种情况是返回类型不同时,方法名和参数个数、顺序、类型都相同的两个方法。第二种情况是返回类型相同时,方法名和参数个数、顺序、类型都相同的方法,但是参数的名字不同。

方法重载适用于普通的方法,也适用于构造函数。决定方法是否构成重载有以下几个条件。

(1)在同一个类中。

(2)方法名称相同。

(3)参数列表不同。

【范例11】

在类中声明4个名称为GetTest的方法,代码如下。

    protected void GetTest(){
        Console.WriteLine("方法1");
    }
    protected void GetTest(string s, int a){            //正确的方法重载
        Console.WriteLine("方法2"); 
    }
    protected void GetTest(string a, int s){
        Console.WriteLine("方法3");
    }
    protected void GetTest(int a,string s) {
        Console.WriteLine("方法3");
    }

上述所示的4个方法中,第一个和第二个是方法重载;第二个与第三个是同一个方法,因为它们只是参数的名称不同,因此不能出现在同一个类中;第二个与第四个是正确的方法重载,因为它们参数的顺序不同。