C#权威指南第9课:类

瓦茨S.汉弗莱曾经说过"软件开发的历史就是软件规模逐渐变大的历史",在软硬件环境逐渐复杂的情况下,软件如何得到良好的维护?面向对象程序设计在某种程度上通过强调可重复性解决了这一问题。20世纪70年代的Smalltalk语言在面向对象方面堪称经典------以至于40年后的今天,我们依然将这一语言视为面向对象语言的基础。

传统的程序设计将程序看做一系列函数的集合,而面向对象程序设计中的基本单元为对象,每一个对象都可以接收数据、处理数据并将数据传送给其他对象,每个对象都是责任和数据的结合体。

C#就是一种面向对象的语言,其他还包括大家熟悉的Java、C++等。面向对象的语言具有一些基础理论,或者叫做基本特征,它们是类、对象、方法、消息传递机制、继承性、封装性、多态性、抽象性。

本课将要学习其中最核心的概念之一:类。学习一门语言时,熟悉语法是基本的要求,更进一步则是学习其中的编程思想,C#语言本身也是语言开发者思想的载体,或者是一种思想表达。我们在本课中也会适时为读者补充一些相关的知识介绍,如果要更深入地学习,望大家能够自行阅读面向对象编程方面的相关书籍,以更好地使用C#这个工具。

阅读本系列课程就像漫步在知识的沙滩上,希望大家偶尔能够捡到几只漂亮的贝壳。

9.1类是什么

我们以ATM取款机软件为例来说明类的稳定性。一开始,用户希望ATM支持本行银行卡,然后又想支持银联银行卡,再往后可能又想支持跨行、跨国账户结转,等等。这说明软件的功能最易发生变化,数据次之。我们将功能和数据分别比做鸡蛋的蛋清和蛋黄,它们本身是不稳定的,如果没有一个容器,你很难将它们一起拿走,但是如果将它们放到一个蛋壳里面,作为一个完整的鸡蛋的话,整体就是稳定的,如图9-1所示。

这个整体就是一个对象,数据就是对象的属性、字段,而功能就是对象的函数成员,它可以提供数据运算。这个对象映射到计算机语言中,就是类。还拿鸡蛋做例子,鸡蛋是类,但是鸡蛋在这里只是一个概念,它表示所有鸡蛋,如果我们要吃鸡蛋,或者拿鸡蛋炒西红柿的话,这时都是某个具体鸡蛋,这些鸡蛋就是类的实例,或者说类具体化了。

可以说,类描述了它所代表对象的共同属性和行为。因此,一个类最基本的两个组成部分是数据和行为(功能)。

其中,数据包含字段(Field)和属性(Property),用于保存与该类有关的信息变量;而行为则是功能、动作,表示类所能提供的服务。

9.2 "Hello World!"程序回顾

我们再来回顾一下第3课中的Hello World程序示例,如代码清单9-1

所示。

代码清单9-1 Hello World程序示例

cs 复制代码
using System;

namespace ProgrammingCSharp4
{
    class HelloWorldClass
    {
        static void Main()
        {
            Console.WriteLine("Hello World!");
        }
    }
}

这段代码共涉及两个类:

❑HelloWorldClass:它是我们自定义的类,没有属性成员,但包括一个静态成员方法------Main()。其实,除了Main方法,它还从基类(Object)继承了四个方法:Equals、GetHashCode、GetType、ToString。关于继承我们将在第11课进行详述。

❑Console:控制台类,这里用到的是将字符串写入控制台的WriteLine方法。

如图9-2所示。

9.3 类的声明

声明一个类非常简单,C#中使用class关键字来定义类,如代码清单9-2所示。

代码清单9-2 类的声明

cs 复制代码
class ClassExample
{
 //类成员定义
}

这段代码定义了一个类ClassExample,此时就可以在项目中允许访问该类的地方对类进行实例化了,大家注意到,class关键字前没有使用任何访问修饰符(后面会讲到),此时默认为internal修饰符,即仅在同一个程序集内可访问,也可以显式地使用internal修饰符,见代码清单9-3。

代码清单9-3 显式使用internal修饰符

cs 复制代码
internal class ClassExample
{
//类成员定义
}

如果想要将ClassExample类声明为公共类,可以使用public修饰符,更多的修饰符我们将在9.9节进行介绍,大家也可以自行参阅。

9.4 类的成员

从级别来分,类的成员包括静态成员和实例成员。静态成员是类级别,不属于类的实例,而实例成员则属于类的实例(对象)。

从功能来分,类的成员包括字段、属性、方法、索引器、构造函数等,其中字段属于数据成员,方法属于函数成员。

表9-1是类所能包含的各种成员。

9.4.1 字段

字段用于存储类所需要的数据。例如,一个类(汽车)可能有三个字段:速度、方向、行驶里程数(当然还可能有很多其他属性,我们此处只是举例说明,尽量将它简化而已)。如图9-3所示。

声明一个字段时,只需要说明如下3个要素即可:

❑访问级别

❑字段的类型

❑字段名称

下面我们提供一个示例供读者参考,如代码清单9-4所示。

代码清单9-4 字段

cs 复制代码
class Car
{ 
    public double speed;
    protected string direction;
    private double distance;
}

可以使用点运算符访问对象中的字段,如下:

对象1名.字段名

现在,我们通过代码清单9-5进行示例说明。

代码清单9-5 为字段赋值

cs 复制代码
using System;

class ClassExample
{
    public static void Main()
    {
        Car car = new Car();
        // 为字段赋值
        car.speed = 10f;
        // 获取字段的值
        Console.WriteLine("汽车的速度为:{0}", car.speed);
    }
}

读者可能已经注意到了,Car类有三个字段:speed、direction、distance,这里仅仅对speed进行了赋值和读取操作,那么能否也对另外两个字段进行同样的操作呢?读者可以自行试验,答案是否定的。原因如下:

❑direction字段的访问级别为protected,属于保护级别,只有使用该类作为基类的派生类型才能访问;

❑distance字段的访问级别为private,属于私有级别,只有声明它的类或结构才能访问。

关于访问级别的更多内容,请参考9.9节。注意,这里讲的字段均为实例字段,和实例字段相对的是静态字段,如下面内容所述。

9.4.2 静态字段

和前述的实例字段不同的是,静态字段是类级别的,就是说访问它不需要先实例化类,直接使用"类名.静态字段名"即可访问。先来看一个静态字段的例子,如代码清单9-6所示。

代码清单9-6 静态字段声明

cs 复制代码
//汽车型号
public static string Type;

我们看到,和9.10节讲的静态方法类似,在实例字段声明的基础上添加static修饰符即可,static关键字要位于字段类型之前。下面看一个示例,如代码清单9-7所示。

代码清单9-7 静态字段的声明及访问

cs 复制代码
using System;

namespace ProgrammingCSharp4
{
    // 示例类
    class ClassExample
    {
        public static void Main()
        {
            // 实例化汽车对象
            Car car = new Car();
            
            // 为实例字段赋值
            car.speed = 10f;
            
            // 静态字段只能通过类名访问
            Car.Type = "BENZ";
            
            // 错误!通过类实例无法访问静态成员
            // car.Type = "CHERY";
            
            // 输出汽车速度
            Console.WriteLine("汽车的速度为:{0}", car.speed);
        }
    }

    // 汽车类
    class Car
    {
        // 当前行驶速度(实例字段)
        public double speed;
        
        // 当前行驶方向(受保护字段)
        protected string direction;
        
        // 已行驶距离(私有字段)
        private double distance;
        
        // 汽车型号(静态字段)
        public static string Type;

        // 构造函数
        public Car()
        {
            // 初始化静态字段默认值
            Type = "BMW";
        }
    }
}

上述代码中,Type字段为静态字段,意味着Car类的所有实例的Type字段的值都相同,而speed等实例字段则各不相同。

静态字段的特点如下:

❑它不属于特定对象,而属于某一个类;

❑它不用创建类的实例即可访问(使用点运算符,且满足相应访问级别的情况下)。

接下来,我们对实例字段和静态字段做个对比,以加深对静态字段的理解。如表9-2所示。

9.4.3 方法

"方法"是类的成员之一,第9课有详细介绍,此处不再赘述。

9.4.4 字段的初始化

所有的字段级变量被编译器初始化为所属类型中等价于0的值。如布尔型被初始化为false,数值型被初始化为0或者0.0,所有的引用类型都被初始化为null。各种数据类型的默认值如表9-3所示。

当然,也可以在声明时就立即进行初始化,而且我们推荐这种方式,这是一个好的编程习惯。下面我们通过示例来说明,如代码清单9-8所示。

代码清单9-8 字段的初始化

cs 复制代码
using System;

namespace ProgrammingCSharp4
{
    class ClassExample
    {
        public static void Main()
        {
            Car car = new Car();
            
            // 获取字段speed的默认值(默认0)
            Console.WriteLine("汽车的速度为:{0}", car.speed);
            
            // 获取字段distance的默认值(默认0)
            Console.WriteLine("汽车的行驶距离为:{0}", car.distance);
            
            // 获取字段isOutOfWarranty的默认值(默认False)
            Console.WriteLine("汽车是否过保修期:{0}", car.isOutOfWarranty);
            
            // 声明时已初始化,输出初始化值
            Console.WriteLine("汽车的品牌为:{0}", car.type);
        }
    }

    class Car
    {
        // 当前行驶速度(double类型默认值:0)
        public double speed;
        
        // 当前行驶距离(uint类型默认值:0)
        public uint distance;
        
        // 是否已过保修期(bool类型默认值:false)
        public bool isOutOfWarranty;
        
        // 车辆品牌(声明时显式初始化:BMW)
        public string type = "BMW";
    }
}

上述代码输出为:

汽车的速度为:0

汽车的行驶距离为:0

汽车是否过保修期:False

9.4.5 属性

C#属性是字段的扩展,它配合C#中的字段使用,用以构造一个安全的应用程序。属性提供了灵活的机制来读取、编写或计算私有字段的值,可以像使用公共数据成员一样使用属性,但实际上它们是称做"访问器"的特殊方法,其设计目的主要是为了实现面向对象(ObjectOriented,OO)中的封装思想。根据该思想,字段最好设为private,一个设计完善的类最好不要直接把字段声明为公有或受保护的,以阻止客户端直接进行访问,其中一个主要原因是,客户端直接对公有字段进行读写,使得我们无法对字段的访问进行灵活的控制,比如控制字段只读或者只写将很难实现。

下面,我们将进一步学习属性及访问器。

1.属性声明和访问器

属性的声明主要包含以下几个部分:访问修饰符、属性类型、属性名称、访问器。

先来看一个属性声明的例子,如代码清单9-9所示。

代码清单9-9 属性声明

属性访问器包括get访问器和set访问器,分别用于字段的读写操作,但要注意的是,属性本身并不一定和字段相联系。仅包含get访问器的属性为只读属性,仅包含set访问器的属性为只写属性,同时包含两种访问器的属性可读也可写,称做读写属性。代码清单9-9中声明的属性就是一个读写属性。

get访问器的责任是返回字段的值,字段就是该属性所封装的字段,那么很自然地,返回值的类型应该和字段的类型一致。代码清单9-9中的get访问器返回的就是name字段的值,且类型和name字段类型相同,都是string类型。

set访问器的责任是为字段赋值,怎么赋值呢?它是通过一个隐式的参数value来实现值的传入,在代码清单9-9中的set访问器中,name的值就是通过value这个隐式参数赋予的。

注意 在属性中,除了get和set访问器,不允许有其他方法出现。

2.属性和关联字段

代码清单9-10 中第4行的speed字段就是属性的关联字段。在一个属性中,get访问器和set访问器的职责之一就是对关联字段的封装。其语法为:

❑声明一个私有的字段级变量(这里是类字段,和局部变量不同,请注意区分);

❑使用下列语法声明一个属性,将私有字段封装起来:

public数据类型属性名

{

get

{

//返回字段值

}

set

{

//使用隐式参数value为字段赋值

}

}

我们先来看一个示例,如代码清单9-10所示。

代码清单9-10 属性声明和访问器

cs 复制代码
class Car
{
    // 当前行驶速度(私有字段)
    private double speed;

    // 属性(封装私有字段speed)
    public double Speed
    {
        // 访问器 - 获取字段值
        get
        {
            // 返回私有字段speed的值
            return speed;
        }
        // 访问器 - 设置字段值
        set
        {
            // 为私有字段speed赋值(value为传入的参数值)
            speed = value;
        }
    }
}

我们对上述代码进行简要的讲解,如表9-4所示。

注意 从内存分配的角度来看私有字段的声明和初始化,CLR为其分配了内存,而并未给属性分配内存,因为属性本身并不存储数据,它操作的是关联字段。

3.自动实现的属性

自C#3.0以来,我们可以使用另外一种更加简洁的语法来定义属性,其语法如下:

public数据类型属性名

{

get;

set;

}

可见,上述代码可使属性声明变得更加简洁。不过这种语法形式也是有限制的,就是仅当属性访问器中不需要其他的逻辑时,才可以使用这种语法形式。如果属性的访问器中需要执行某些计算,就还是需要使用关联字段的方式。本质上,自动实现的属性这种语法也有自己的关联字段,只不过这个关联字段也是隐式的,是编译器自动生成的。由此可见,编译器帮助我们做了很多的工作,减少了我们的工作量。

下面,我们将从本质上对自动实现的属性进行剖析,了解一下编译器究竟为我们做了哪些工作,并且是如何做的,这对于深刻理解C#的工作原理是大有裨益的。通过查看生成后的CIL和原C#代码来一探究竟,可能是一个不错的主意。你可以在"开始"菜单中"Visual Studio 2010"目录的"Microsoft Windows SDK Tools"下找到"IL反汇编程序",它就是

将生成的exe反编译成CIL语言的工具,如图9-4所示。

工具准备好了,接下来看一个示例程序,如代码清单9-11所示。

代码清单9-11 C#自动属性的工作原理

cs 复制代码
namespace ProgrammingCSharp4
{
    class ClassExample
    {
        static void Main()
        {
            Car car = new Car();
            car.Speed = 10f;
            double result = car.Speed;
        }
    }

    class Car
    {
        // 属性
        public double Speed
        {
            // 访问器
            get;
            set;
        }
    }
}

注意,这段代码声明了一个类Car,它有一个属性Speed,这里采用C#3.0的自动实现属性语法,可以看到,并没有单独声明一个私有字段变量(关联字段)。接下来,我们查看编译后生成的CIL代码,以了解C#编译器是如何工作的,如图9-5所示。

图9-5为编译后的Car类的成员组成图,从中可以看到:

❑编译器生成了一个私有的、类型为float64的私有字段级变量:<Speed>k__BackingField,只不过这个字段我们无法从源代码进行访问;

❑编译器生成了两个访问器方法,分别为get_Speed()和set_Speed(float64),我们特别注意到后者有一个float64类型的参数,它就是隐式的参数。

综合来看,可以得出如下结论:

❑本质上,编译器仍然使用的是和C#2.0相同的语法声明属性,即仍然使用关联字段;

❑属性的本质是方法,是一种特殊的方法。

知道了这些以后,我们来具体看一下CIL代码。阅读CIL代码有一定难度,但和汇编相比还是非常简单,因此后面会先介绍几个常见的CIL指令。话说回来,不能完全读懂这些代码也没有关系,我们的重点在于揭示工作原理,而不是学习CIL。只需要借助CIL让大家明白大致的工作原理就算达到了目的。为了帮助大家理解CIL代码,表9-5列举了几个必需的CIL指令。

在继续下文之前,我们还要强调两个概念:入栈和出栈。因为C#在本质上是基于栈的。在CIL中用来负责这个栈实现的部分叫做虚拟执行栈(Virtual Execution Stack,VES)。在下面的CIL代码中,你将看到CIL提供了一系列指令来完成将值压入到栈中,这个过程叫加载(load)。另外,CIL也定义了一系列指令来将栈顶的值移到内存中(例如局部变量),这个过程叫存储(store)。要注意的是,CIL不允许直接访问一个数据,包括局部变量、方法参数或者类的字段数据。为了实现访问,必须显式地将数据加载到栈中,并在使用时弹出。请务必注意这一点。

预备知识讲完了,我们可以尝试阅读CIL代码了,先来看Speed属性的CIL代码(对应图9-5中的②部分),如代码清单9-12所示。

代码清单9-12 Speed属性的CIL代码

html 复制代码
.property instance float64 Speed()
{
.get instance float64 ProgrammingCSharp4.Car:get_Speed()
.set instance void ProgrammingCSharp4.Car:set_Speed(float64)
}//end of property Car:Speed

从上述CIL代码可以看到:

❑第3行:get访问器,调用get_Speed()方法;

❑第4行:set访问器,调用set_Speed(float64)方法,类型为float64的参数即是前面讲到的隐式值参数(value)。

我们继续看这两个访问器的CIL代码(对应图9-5中的①部分),如代码清单9-13和代码清单9-14所示。

代码清单9-13 get访问器(get_Speed方法)的CIL代码

html 复制代码
.method public hidebysig specialname instance float64 
    get_Speed() cil managed
{
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (01 00 00 00)
    // Code size 11 (0xb)
    .maxstack 1
    .locals init (float64 V_0)
    IL_0000: ldarg.0
    IL_0001: ldfld float64 ProgrammingCSharp4.Car::'<Speed>k__BackingField'
    IL_0006: stloc.0
    IL_0007: br.s IL_0009
    IL_0009: ldloc.0
    IL_000a: ret
} // end of method Car::get_Speed

为了突出实质内容,我们将略过无关的部分。关于上述代码的说明

如下:

❑第7行:声明一个局部变量V_0,类型为float64(也就是

double);

❑第8行:将局部变量V_0装入堆栈;

❑第9行:将编译器生成的<Speed>k__BackingField字段放入堆栈;

❑第10行:将栈中<Speed>k__BackingField的值赋给V_0变量,并

弹出栈;

❑第12行:将局部变量V_0的值放入堆栈;

❑第13行:方法返回,因为栈中有值,此值就作为返回值。

代码清单9-14 set访问器(set_Speed方法)的CIL代码

html 复制代码
.method public hidebysig specialname instance void
    set_Speed(float64 'value') cil managed
{
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (01 00 00 00)
    // Code size 8 (0x8)
    .maxstack 8
    IL_0000: ldarg.0
    IL_0001: ldarg.1
    IL_0002: stfld float64 ProgrammingCSharp4.Car::'<Speed>k__BackingField'
    IL_0007: ret
} // end of method Car::set_Speed

关于上述代码的说明如下:

❑第2行:可以看到前面提到的隐式值参数(value),其类型和属性类型相同;

❑第7行:value参数的值放入栈中;

❑第9行:将栈中value参数的值赋给<Speed>k__BackingField字段;

❑第10行:方法返回,赋值结束。

现在,从更深的层次了解了C#中属性访问器的工作原理,内容比较多且有些抽象,请大家多思考、多实践,对CIL感兴趣的读者也可以自行查阅相关资料。

最后,需要注意的是,如果使用自动实现的属性,get和set访问器必须成对出现,如果只有get而没有set,如下面的代码:

cs 复制代码
class Car
{
  public double Speed2
  {
    //访问器
    get;
  }
}

上述代码将无法通过编译,产生的编译错误如下:

"CarCar.Speed2.get"必须声明主体,因为它未标记为abstract或extern。自动实现的属性必须同时定义get访问器和set访问器。

4.只读和只写属性

可以提供灵活的访问控制,是我们使用属性的一个重要理由之一。前面谈过只读、只写和读写属性,如下:

❑只读属性:不具有set访问器或者set访问器为private级别的属性,被视为只读属性;

❑只写属性:不具有get访问器或者get访问器为private级别的属性,被视为只写属性;

❑读写属性:具有set访问器和get访问器。

下面查看一下使用访问修饰符实现属性,语法如下:

public数据类型属性名

{

访问修饰符get;

访问修饰符set;

}

实现一个只读属性,如代码清单9-15所示。

代码清单9-15 只读属性示例

cs 复制代码
class Car
{
    // 只读属性
    public double Speed
    {
        // 访问器
        get;
        private set;
    }

    // 只读属性
    public double Speed2
    {
        // 访问器
        get;
        private set;
    }
}

如果试图为Speed属性赋值,CLR将会引发一个异常,意为Speed属性再不可用,因为set访问器不可访问:由于set访问器不可访问,因此不能在此上下文中使用属性或索引器'ProgrammingCSharp4.Car.Speed'同理,一个只写的属性如代码清单9-16所示。

代码清单9-16 只写属性示例

cs 复制代码
// 只写属性
public double Speed
{
    // 访问器
    private get;
    set;
}

// 只写属性
public double Speed
{
    // 访问器
    set; // 语法错误:C# 属性不能只有 set 访问器,需至少包含 get 或同时有 get/set
}

一个字段要么可读可写,要么不可读不可写,可见,仅使用字段是无法获得如此灵活的访问控制特性的。

5.执行计算

属性的访问器不但可以为关联字段赋值和返回关联字段的值,还可以根据需要加入更多的逻辑控制代码。例如,还是使用Car这个类,假如为这辆车限速120km/h,当我们试图让速度超过120km/h时,车辆拒绝加速,而将最大速度设为120km/h,用代码表示如下:

cs 复制代码
set
{ 
  if(value>120)
  { 
    speed=120;
  }
}

这里就在set访问器中加入了逻辑计算功能,同理,get访问器也是一样的,比如在get访问器中进行单位换算等,下面我们给出完整的示例代码,如代码清单9-17所示。

代码清单9-17 在属性中进行逻辑计算

cs 复制代码
namespace ProgrammingCSharp4
{
    class ClassExample
    {
        static void Main()
        {
            Car car = new Car();
            car.Speed = 130f;
            System.Console.WriteLine("当前速度为:{0}", car.Speed);
        }
    }

    class Car
    {
        private double speed;
        // 属性
        public double Speed
        {
            // 访问器
            get
            {
                return speed;
            }
            set
            {
                if (value > 120)
                {
                    speed = 120;
                }
            }
        }
    }
}

说明:

❑第8行:为Speed属性设置130。

❑第26行:检测(130>120)表达式成立,拒绝接受,转而将120赋给了speed字段。

运行结果为:

当前速度为:120

6.静态属性

和前面讲到的静态变量、静态方法类似,属性也可以声明为静态,使用static关键字即可。只不过因为静态属性属于类级别,因此不能通过类的实例进行访问,也不能在静态属性中使用非静态的关联字段,示例如代码清单9-18所示。代码清单9-18 静态属性示例

cs 复制代码
namespace ProgrammingCSharp4
{
    class ClassExample
    {
        static void Main()
        {
            Car.Speed = 130f;
            System.Console.WriteLine("当前速度为:{0}", Car.Speed);
        }
    }

    class Car
    {
        // 如果属性为静态,则关联字段也必须为静态
        private static double speed;
        // 静态属性
        public static double Speed
        {
            // 访问器
            get
            {
                return speed;
            }
            set
            {
                if (value > 120)
                {
                    speed = 120;
                }
            }
        }

        // 自动属性
        public static int Number
        {
            get;
            set;
        }
    }
}

说明:

类Car的两个属性Speed和Number均为静态属性,其中Speed属性有关联字段speed,此关联字段同样必须为静态。

9.5 类的实例:对象

前面已经讲了类的成员变量和成员方法,如果要访问到这些成员,必须通过类的实例(除了静态成员以外)。前面讲过了,类是对数据和功能的封装,但封装不是目的,将类进行实例化,并使用对象的数据和服务完成某种任务才是目的。

要得到一个类的实例对象,必须先声明一个该类类型的变量,然后使用new运算符创建一个实例对象,后面会讲到,new运算符还会调用实例对象的构造函数。我们来看一个简单的例子,如代码清单9-19所示。代码清单9-19 实例化一个类

cs 复制代码
// 创建 Car 类的实例
Car car = new Car();

// 定义 Car 类
class Car
{
    // 当前行驶速度(最大速度)
    public double maxSpeed;
    // 当前行驶方向
    protected string direction;
    // 已行驶距离
    private double distance;
    // 汽车型号(静态成员)
    public static string Type;

    // 无参构造函数
    public Car()
    {
        Type = "Benz";
    }
}

此时,这个car对象就是一个具体的汽车了,而不是概念中的汽车,如图9-6所示,对象1和对象2都是Car类的实例对象,它们各自的实例字段值均不相同,只有Type静态字段值是一样的,因为它和具体实例对象无关。

9.6 实例化中的内存分配

大家还记得类是值类型还是引用类型吗?如果记不太清楚了,可以翻阅之前的课程,温故而知新嘛!类是引用类型,我们知道,引用类型是在堆里分配内存的,在栈中保存的是对象的引用。因此,类的实例化涉及两个位置的内存分配。

❑在栈中为对象的引用分配空间;

❑在堆中为对象分配空间。

一起来看看下面代码中的内存是如何分配的:

Car car=new Car();

这行代码看似简单,实则不然,其过程大致可分为两个步骤:

(1)首先,声明类型为Car的变量car,并使用null初始化,此时会在栈中为car变量分配一个内存,它指向null,因为在堆中还没有创建一个car对象实例以供它指向;

(2)其次,使用new运算符在堆中创建一个新的Car实例对象,并将其引用赋予变量car,此时栈中的car变量指向新创建的实例对象(见图9-7)。

9.7 实例的成员

类的成员主要分两类:一类和实例相关,对于不同的实例,其值各不相同,我们把这些为特有的对象所持有的数据成员称为实例成员;另一类为静态成员,它和类相关,不为特有的对象所持有,只能通过类进行访问。

9.8 this关键字

this关键字主要有几个用途,如下:

❑当局部变量名称和类字段相同时,用以引用类字段;

❑将当前对象实例作为参数传递到其他方法;

❑可以声明索引器,我们将在后面课程进行说明。

注意 由于静态成员函数属于类级别,不是对象的一部分,因此在静态方法中使用this是错误的。

对于前面两种用途,我们通过实例代码进行说明,具体说明在代码注释里,如代码清单9-20所示。代码清单9-20 this关键字示例

cs 复制代码
using System;

namespace ProgrammingCSharp4
{
    // 主示例类
    class ClassExample
    {
        static void Main()
        {
            // 创建Car类的实例
            Car car = new Car();
            // 调用方法并传入参数"Chery"
            car.DoSmothing("Chery");
            // 输出汽车名称
            Console.WriteLine(car.Name);
        }
    }

    // 汽车类
    class Car
    {
        // 私有字段存储汽车名称
        private string name;

        // 执行操作的方法
        public void DoSmothing(string name)
        {
            // 使用this关键字区分字段和参数(同名时)
            this.name = name;
            // 将当前Car实例传递给CarTest的静态Test方法
            CarTest.Test(this);
        }

        // 公开的属性,用于获取私有字段name的值
        public string Name
        {
            get
            {
                return name;
            }
        }
    }

    /// <summary>
    /// 提供车辆检验服务
    /// </summary>
    class CarTest
    {
        /// <summary>
        /// 执行车辆检验
        /// </summary>
        /// <param name="car">要检验的车辆实例</param>
        public static void Test(Car car)
        {
            // 输出接受检验的车辆名称
            Console.WriteLine("当前接受检验车辆名称是:{0}", car.Name);
        }
    }
}

9.9 访问修饰符

访问修饰符用于限制类、结果以及它们的成员的可访问性。访问修饰符包括4个关键字:public、protected、internal、private,使用这四个关键字可组成下列5个可访问性级别:

❑public:最高访问级别,访问不受限制;

❑protected:保护级别,受保护成员可由自身及派生类访问;

❑internal:内部访问级别,只有在同一程序集中,内部类型或成员才可访问;

❑protected internal:内部保护级别,访问仅限于当前程序集,可由自身及派生类访问;

❑private:私有访问,最低访问级别,私有成员只有在声明它们的类和结构体中才是可访问的。

下面,我们比较这5种访问级别,如表9-6所示。

使用图9-8进行说明可以更加直观,有箭头指向的为可以访问,反之则不能访问。

需要注意的是,访问修饰符并非哪里都可以用,它们可用于类、字段、属性,而不可用于命名空间、局部变量、方法参数。

9.10 访问类的成员

访问类的成员,包括访问类的属性、方法、字段等。我们将从以下几个方面来讲解:

❑从类内部访问成员,这里的成员指的是实例成员;

❑从类外部访问成员,这里的成员指的是实例成员;

❑从类外部访问静态成员。

9.10.1 从类内部访问成员

学习了访问修饰符,我们知道了5种访问级别,由表9-6可知,从类内部可访问任何访问级别的成员,当然包括声明为private的成员,可以在任何方法内访问到类的所有成员,如代码清单9-21所示。

代码清单9-21 从类内部访问成员

9.10.2 从类外部访问成员

从类的外部访问类的成员时,先要声明类的实例变量,并实例化类,然后使用点运算符访问类成员(字段或者方法)。结合学习到的访问级别,除了private修饰符只能从类内部访问以外,其他均为从类外部访问的,只是分几种不同的情况。下面是一个示例,在代码清单9-21中的Car类外访问Car类的成员,如代码清单9-22所示。

代码清单9-22 从类外部访问成员

cs 复制代码
class ClassExample
{
    static void Main()
    {
        //创 建 对 象 car
        Car car = new Car("Benz");
        //调用对象car的公共方法DoSmothing
        car.DoSmothing("something");
    }
}

说明:

DoSmothing方法为Car类的公共成员函数,我们首先声明了一个Car变量car,然后使用点运算符访问DoSmothing方法。

9.10.3 从类外部访问静态成员

由于类的静态成员不属于类的实例,它是类级别的,因此要访问这类成员不需要创建对象这一过程,而是直接使用类名。同样,使用点运算符访问类的静态成员,如代码清单9-23所示。代码清单9-23 从类外部访问静态成员

cs 复制代码
namespace ProgrammingCSharp4
{
    class ClassExample
    {
        static void Main()
        {
            //通过类而不是对象访问静态成员
            Car.Name = "Polo";
            System.Console.WriteLine(Car.Name);
            //调用静态方法
            Car.DoSelfCheck();
        }
    }

    class Car
    {
        //静态字段
        public static string Name;
        //静态方法
        public static void DoSelfCheck()
        {
            System.Console.WriteLine("self check ok!");
        }
    }
}

9.11 构造函数

构造函数是一类特殊的成员函数,它主要用于为对象分配内存空间,并对类的数据成员进行初始化,或者说设置实例对象到某一个初始状态(数据和状态相对应)。构造函数分为实例构造函数和静态构造函数两种,下面我们将分别对它们进行介绍。

实例构造函数

实例构造函数用于创建和初始化实例。在使用new运算符创建新的对象时,将调用类的实例构造函数,如代码清单9-24所示。

代码清单9-24 实例构造函数

cs 复制代码
class Car
{ 
   //字段
   private string name;
   //构造函数
   public Car()
   {
     name="Polo";
   }
}

实例构造函数特点如下:

❑构造函数的名字必须和类同名;

❑构造函数不允许有返回类型;

❑构造函数可以具有0~n个参数;

❑构造函数可以重载,以提供初始化类的不同方法;

❑若在声明类时没有定义构造函数,则系统自动生成一个函数体为

空的默认构造函数;

❑可以对实例构造函数使用public、protected、private修饰符;

❑引用基类构造函数时使用base()方法,如果有参数则将参数传入,如base(参数1,参数2......);

❑引用自身构造函数时使用this()方法,如果有参数则将参数传入,如this(参数1,参数2......)。

1.默认构造函数

如果没有为类提供任何构造函数,那么CLR将会自动提供一个构造函数作为默认构造函数,它是实例构造函数的一种,它的特点是:

❑没有参数;

❑构造函数体即方法体为空。

我们来看这样一个类,如代码清单9-25所示。

代码清单9-25 默认构造函数

//没有提供任何构造函数,此时CLR会自动提供一个默认构造函数

cs 复制代码
class Car
{
    private string name;
    public void DoSmothing(string name)
    {
        this.name = name;
    }
    public string Name
    {
        get
        {
            return name;
        }
    }
}

从上述代码可以看到,我们没有为类提供任何构造函数,那么对它进行编译,看看CLR是否自动生成了默认构造函数呢?从反编译后的Car类和代码清单9-26可以看出,CLR提供了一个默认构造函数.ctor,如图9-9所示。

其内容如代码清单9-26所示。

代码清单9-26 Car类默认构造函数.ctor的CIL代码

html 复制代码
.method public hidebysig specialname rtspecialname
instance void.ctor()cil managed
{ //Code size 7(0x7)
.maxstack 8
IL_0000:ldarg.0
IL_0001:call instance
void[mscorlib]System.Object:.ctor()
IL_0006:ret
}//end of method Car:.ctor

这是由CLR生成的默认构造函数,它没有任何参数,它的任务就是简单地调用下基类(此处是System.Object,任何类都是System.Object的

直接或间接的派生类,这里是直接派生自System.Object类)的构造函数(IL_0001行)。

现在,我们为Car类提供了一个构造函数,如代码清单9-27所示。

代码清单9-27 带构造函数的Car类

cs 复制代码
class Car
{
    private string name;
    //我们提供了一个构造函数,简单的为name自动赋值
    public Car()
    {
        name = "Polo";
    }
    public void DoSmothing(string name)
    {
        this.name = name;
    }
    public string Name
    {
        get
        {
            return name;
        }
    }
}

我们看看添加了构造函数以后,反编译出的CIL代码有何不同,如代码清单9-28所示。

代码清单9-28 添加了构造函数的Car类的CIL代码

html 复制代码
.method public hidebysig specialname rtspecialname
instance void.ctor()cil managed
{ //Code size 21(0x15)
.maxstack 8
IL_0000:ldarg.0
IL_0001:call instance
void[mscorlib]System.Object:.ctor()
IL_0006:nop
IL_0007:nop
IL_0008:ldarg.0
IL_0009:ldstr"Polo"
IL_000e:stfld string ProgrammingCSharp4.Car:name
IL_0013:nop
IL_0014:ret
}//end of method Car:.ctor

我们可以发现,除了仍然调用基类的构造函数以外(IL_0001),新增了为字段赋值的代码(加粗部分),这也正是我们提供的构造函数所做的工作。

2.带参数的构造函数

如果想在创建类实例的同时就使用一些值进行初始化,该怎么办呢?答案就是构造函数,而且是带参数的构造函数,上节讲到的构造函数是默认构造函数,它没有参数。前面我们讲了,构造函数可以重载,那么我们在上例的基础上,再提供一个带参数的构造函数,先来看看C#代码,如代码清单9-29所示。

代码清单9-29 带参数的构造函数

cs 复制代码
class Car
{
    private string name;
    public Car()
    {
        name = "Polo";
    }
    //带参数的构造函数
    public Car(string name)
    {
        this.name = name;//使用this关键字以和参数的name分开
    }
    public void DoSmothing(string name)
    {
        this.name = name;
    }
    public string Name
    {
        get
        {
            return name;
        }
    }
}

那么,我们就可以在实例化Car类的同时,初始化类的字段name,如代码清单9-30所示。

代码清单9-30 使用带参数的构造函数初始化类的实例

cs 复制代码
class ClassExample
{ 
   static void Main()
   {
    //使用带参数的构造函数实例化Car类
   Car car=new Car("Benz");
   Console.WriteLine(car.Name);
   } 
}

其输出如下所示:

Benz

下面,我们看看代码清单9-29编译生成的CIL代码,如图9-10所示。

可见,不带参数的默认构造函数仍然存在。其中,带参数的构造函数CIL代码如代码清单9-31所示。

代码清单9-31 构造函数的CIL代码

html 复制代码
.method public hidebysig specialname rtspecialname
instance void.ctor(string name)cil managed
{ //Code size 17(0x11)
.maxstack 8
IL_0000:ldarg.0
IL_0001:call instance
void[mscorlib]System.Object:.ctor()
IL_0006:nop
IL_0007:nop
IL_0008:ldarg.0
IL_0009:ldarg.1
IL_000a:stfld string ProgrammingCSharp4.Car:name
IL_000f:nop
IL_0010:ret
}//end of method Car:.ctor

从代码清单9-31可以看出:

❑构造函数执行时,首先调用基类的默认构造函数(不带参数的)(IL_0001);

❑执行参数的赋值操作(IL_000a)。

3.静态构造函数

静态构造函数用于初始化任何静态数据,或用于执行仅需执行一次的特定操作。在创建第一个实例或引用任何静态成员之前,将自动调用静态构造函数。

下面来看一个静态构造函数示例,如代码清单9-32所示。

代码清单9-32 静态构造函数

cs 复制代码
class Car
{
    private static string name;
    //静态构造函数,仅执行一次
    static Car()
    {
        Console.WriteLine("静态构造函数调用了!");
        name = "Polo";
    }
    //静态属性
    public static string Name
    {
        get
        {
            return name;
        }
    }
}

静态构造函数有以下几个特性:

❑静态构造函数不能使用任何访问修饰符;

❑静态构造函数不能具有任何参数;

❑静态构造函数的执行时机,是在创建类的第一个实例,或者访问任何类的静态成员之前,自动执行的,并仅执行一次,以完成对静态成员的初始化工作;

❑不能直接调用静态构造函数;

❑程序中,用户无法控制执行静态构造函数的时机。

我们看一个实例,如代码清单9-33所示。

代码清单9-33 静态构造函数使用示例

cs 复制代码
class ClassExample
{ 
   static void Main()
   { 
      System.Console.WriteLine(Car.Name);
      System.Console.WriteLine(Car.Name);
   }
}

在这里,我们练习调用了两次Car类的静态成员属性Name,其输出

如下:

静态构造函数调用了!

Polo

Polo

我们连续访问了两次Car对象的静态Name属性,只打印出了一次"静态构造函数调用了!",这说明静态构造函数仅仅执行了一次。同样,我们也观察一下静态构造函数生成的CIL代码,大家注意一下有什么不同,如图9-11所示。

4.构造函数的可访问性

除了刚刚讲过的静态构造函数没有访问修饰符以外,实例构造函数都要有一个访问修饰符,如果没有表示隐式的private修饰符,构造函数是private,当前类只能通过自身进行实例化(构造函数虽然为private,但自身仍然是可以访问的)。除了private以外,构造函数还可以使用public以及protected修饰符,前者不需多讲,我们一直在用;后者表示构造函数是"受保护"的,如何保护呢?这意味着,当前类不能直接被实例化,而只能实例化当前类的子类,相关内容我们将在第10章具体阐述。

5.私有构造函数

下面,我们讨论私有构造函数。顾名思义,构造函数的访问修饰符为private,这意味着其他对象无法访问类的构造函数,也就无法直接将类进行实例化。因此,只能依赖类自身提供访问自身实例的途径。下面,我们看一个私有构造函数的例子,如代码清单9-34所示。

代码清单9-34 私有构造函数

cs 复制代码
class Car
{
    private string name;
    // 私有构造函数,只能在内部调用,也就是说只能自己实例化自己
    private Car()
    {
        Console.WriteLine("私有实例构造函数调用了!");
        name = "Polo";
    }
    // 静态方法,用以返回类的实例
    public static Car getInstance()
    {
        return new Car();
    }
    // 属性
    public string Name
    {
        get
        {
            return name;
        }
    }
}

在上述代码中,特意提供了一个静态的、返回Car类型的方法:getInstance(),它的责任是实例化自身,并将实例返回。建议大家参阅《设计模式之禅》中的单例模式,此模式是私有构造函数的典型应用之一。接下来我们看看如何使用这样一个构造函数私有的类,如代码清单9-35所示。

代码清单9-35 私有构造函数类的调用

cs 复制代码
class ClassExample
{
    static void Main()
    {
        // 通过调用静态方法getInstance()返回一个Car类型的实例
        Car car = Car.getInstance();
        System.Console.WriteLine(car.Name);
    }
}

输出为:

私有实例构造函数调用了!

Polo

9.12 对象初始化列表

使用对象初始化列表是一种初始化对象数据的快捷方式。使用对象初始化列表可以在创建对象时,向对象的任何可访问的字段或属性分配值,而无须显式调用构造函数。接下来看一个示例,先声明一个类Car,它具有若干属性和若干公共字段,如代码清单9-36所示。

代码清单9-36 Car类

cs 复制代码
class Car
{
    //私有字段
    private string name;
    
    //公共字段
    public string produceDate;
    
    //实例构造函数
    public Car()
    {
        System.Console.WriteLine("实例构造函数调用了!");
        name = "Polo";
    }
    
    //属性
    public string Name
    {
        get
        {
            return name;
        }
        set
        {
            name = value;
        }
    }
    
    //自动实现的属性
    public string Model { get; set; }
}

由于我们想要测试的是为对象快捷赋值,因此我们设计了这样一个仅有数据成员的类Car。接下来,我们分析下,如果想要在初始化Car类的同时初始化Car类中的某些数据字段,有两种解决方案可行,第一种方案是提供一个带参数构造函数,类似这样:

cs 复制代码
//带参数的构造函数,可以为类的属性或公共字段提供初始值
public Car(string _name, string _model, string _produceDate)
{
    Name = _name;
    Model = _model;
    produceDate = _produceDate;
}

甚至,我们还可以学以致用,使用在前面讲过的命名和可选参数特性,如:

cs 复制代码
//_produceDate参数是可选的,因为它具有默认值
public Car(string _name, string _model,string _produceDate = "2010-1-2")
{
    Name = _name;
    Model = _model;
    produceDate = _produceDate;
}

然后,像下面的代码这样对Car类进行实例化:

cs 复制代码
class ClassExample
{
    static void Main()
    {
        //使用构造函数对公共字段或属性进行初始化
        Car car = new Car("Benz", "S600", "2010-5-1");
        //省略了_produceDate参数,因为它有默认值
        Car car2 = new Car("BMW", "760i");
        System.Console.WriteLine(car.Name);
        System.Console.WriteLine(car2.Name);
    }
}

第一种方案有一个特点:都必须提供一个或多个构造函数。

第二种方案是使用我们现在要讲的"对象初始化列表"。上面的示例,我们还可以这样写:

cs 复制代码
class ClassExample
{
    static void Main()
    {
        //使用对象初始化列表表达式进行初始化
        //方法很简单,将()换为{},在里面为每个属性或公共字段赋值,以逗号分隔即可
        Car car = new Car { Name = "Benz", Model = "S600",
            produceDate = "2010-1-1" };
        //可以灵活、有选择地对某些属性进行初始化
        Car car2 = new Car { Name = "BMW", Model = "760i" };
        Car car3 = new Car { Name = "Cadillac" };
        System.Console.WriteLine(car.Name);
        System.Console.WriteLine(car2.Name);
        System.Console.WriteLine(car3.Name);
    }
}

可见,第二种方案使得代码更加紧凑和易读,还省略了编写若干个构造函数的工作量。需要注意的是,对象初始化列表只是简化了这样一个过程,实际上它和下列代码是等价的:

cs 复制代码
Car car=new Car();
car.Name="Benz";
car.Model="S600";
car.produceDate="2010-1-1";

因此,在使用对象初始化列表的时候,仍然会先调用对象的实例构造函数(不带参数的,默认构造函数),下一课我们还会专门研究对象初始化的顺序。可以从运行结果印证这一点:

实例构造函数调用了!

实例构造函数调用了!

实例构造函数调用了!

Benz

BWM

Cadillac

9.13 析构函数

析构函数的作用是在类被销毁之前,对类实例使用的托管或非托管资源进行释放。通常,与运行时不进行垃圾回收的开发语言相比,C#无须太多的内存管理。这是因为.NET Framework垃圾回收器会隐式地管理对象的内存分配和释放。但是,当应用程序封装窗口、文件和网络连接这类非托管资源时,应当使用析构函数释放这些资源。

析构函数具有如下特点:

❑析构函数不能有访问修饰符;

❑析构函数不能有参数;

❑一个类只能有一个析构函数;

❑无法继承或重载析构函数;

❑无法调用析构函数;

❑无法预知析构函数何时被调用,因为它是被自动调用的。

接下来,看一个析构函数示例,如代码清单9-37所示。

代码清单9-37 析构函数

cs 复制代码
~Car()
{ 
  System.Console.WriteLine("析构函数调用了!");
}

然后,看一下这个析构函数编译生成CIL后的结构,注意一点,析构函数编译成CIL后转化为了Finalize方法,如图9-12所示。

代码清单9-38 是析构函数Finalize方法的CIL代码。

代码清单9-38 析构函数的CIL代码

html 复制代码
.method family hidebysig virtual instance void
Finalize()cil managed
{ //Code size 25(0x19)
.maxstack 1
.try
{ IL_0000:nop
IL_0001:ldstr bytearray(90 67 84 67 FD 51 70 65 03 8C 28
75 86 4E 01 FF)
//.g.g.Qpe......(u.N......
IL_0006:call void[mscorlib]System.Console:
WriteLine(string)
IL_000b:nop
IL_000c:nop
IL_000d:leave.s IL_0017
//end.try
finally
{ IL_000f:ldarg.0
IL_0010:call instance void[mscorlib]System.Object:
Finalize()
IL_0015:nop
IL_0016:endfinally
}//end handler
IL_0017:nop
IL_0018:ret
}//end of method Car:Finalize

先从较高的视角观察代码清单9-38的整体结构,它使用了try-finally语句,在try块中获取并使用资源,并在finally块中释放资源,如下:

cs 复制代码
try
{
//执行语句
}
finally
{
//这里的代码一定会被执行
}

这表示,析构函数中编写的代码被移到了try语句块中,而在finally语句块中添加了对基类的Finalize()方法的调用,这意味着将会对继承链中的所有实例递归地调用Finalize方法(从子类到基类)。

前面的析构函数代码被隐式地转换为以下代码:

cs 复制代码
protected override void Finalize()
{
   try
    {
      //执行清理操作的语句
    } 
  finally
  {
     base.Finalize();
  }
}

9.14 只读字段和常数

如果想将某个字段声明为只读的,可以使用readonly关键字,如:

private readonly string name;

如果在声明只读字段的时候没有为它赋值,还可以在构造函数中为它赋值。除此之外,无法对只读字段重新赋值,如果尝试赋值,就会收到如下错误信息,意思是无法给一个只读字段赋值,除非在构造函数或者变量初始化语句中:

A readonly field cannot be assigned to(except in a constructor or a variable initializer)

下面就是可以对只读变量初始化的两种方式,如代码清单9-39所示。

代码清单9-39 只读字段的初始化

cs 复制代码
class Car
{ 
    //声明时进行初始化
    private readonly string name = "BENZ";
    public Car()
    { 
        //在构造函数中初始化只读字段
        name = "Polo";
    }
    //属性
    public string Name
    { 
        get
        { 
            return name;
        }
    }
}

常数(使用const关键字修饰)和只读字段有些类似,但它们有本质的区别。接下来,介绍常数的特性:

❑常数必须在声明的时候就初始化,然后就再也不能改变;

❑常数从编译时开始就保持不变,而不是像只读字段那样可以在运行时再指定。

为了便于说明,请参考表9-7。

9.15 索引器

我们前面讲了属性和属性访问器,索引器非常类似于属性。先来看一个实例,如代码清单9-40所示。

代码清单9-40 索引器

cs 复制代码
class Class
{
    private string[] students = new string[3];
    public string this[int studentNo]
    {
        get
        {
            return students[studentNo];
        }
        set
        {
            students[studentNo] = value;
        }
    }
}

从结构上来看,索引器它和属性相似,同样都有get和set访问器,不同的是:

❑每一个属性的名称必须唯一,而每一个索引器的签名必须唯一(注意,比属性的名称唯一更宽泛,只需要签名唯一即可),不同的索引器签名可以实现索引器重载;

❑索引器的"属性名"统一为this,而不能为其他,专门用以定义索引器,它和9.8节讲的this返回当前类实例不同;

❑索引器的参数列表包含在方括号而不是圆括号之内;

❑属性可以是静态的,而索引器只能为实例成员;

❑属性的get访问器没有参数,而索引器的get访问器可以有参数,而且后者的get访问器和set访问器的参数相同;

❑索引器可以有多个形参,比如访问多维数组时。

索引器主要用于封装类的内部集合或数组,其使用方式也类似于数组。下面,我们对比分析一下索引器和数组的异同,如表9-8所示。

接下来,我们看看如何使用索引器,索引器可以让我们像访问数组一样访问类中的集合或者数组,下面的示例演示如何使用定义了索引器的类,如代码清单9-41所示。

代码清单9-41 通过索引器访问类内部集合或数组

cs 复制代码
class ClassExample
{
    static void Main()
    {
        Class classOne=new Class();
        classOne[0]="杨康";
        classOne[1]="郭靖";
        classOne[2]="黄蓉";
        System.Console.WriteLine(classOne[0]);
        System.Console.WriteLine(classOne[1]);
        System.Console.WriteLine(classOne[2]);
        for(int i=0;i<3;i++)
        {
            System.Console.WriteLine(classOne[i]);
        }
    }
}

9.16 分部类型和分部方法------修饰符:partial

如何理解分部类型和分部方法呢?简单地说,就是将一个类型或方法拆分到两个或多个源文件中,每个源文件只包含类型定义的一部分。类、结构、接口、方法都可以拆分。

那么,为什么要进行拆分呢?或者说什么情况下才需要拆分?以下是几种使用场景:

❑当处理大型项目时,把一个类分布于多个独立文件中可以让多位程序员同时对该类进行处理;

❑使用自动生成的源时,无须重新创建源文件便可将代码添加到类中。Visual Studio在创建Windows窗体、Web服务包装代码等时都使用此方法。无须修改Visual Studio创建的文件,就可创建使用这些类的代码,如图9-13所示。

对Windows窗体代码中的分部类进行说明,如表9-9所示。

9.16.1 分部类

这里主要介绍分部类。分部接口和分部结构将在相应章节学习。对于类来说,可以将不同的方法拆分到分部类中去,如图9-12所示。

分部类具有如下特征:

❑类的定义前加partial修饰符;

❑分部类可以定义在两个不同的.cs文件中,也可以位于同一个.cs文件中;

❑分部类必须同属一个命名空间。

下面,我们看一个分部类的例子,如代码清单9-42和代码清单9-43所示。代码清单9-42 分部类Car1.cs

cs 复制代码
namespace ProgrammingCSharp4
{
    class ClassExample
    {
        static void Main()
        {
            Car car=new Car();
            System.Console.WriteLine(car.DoSomething2());
        }
    }

    partial class Car
    {
        public string DoSomething1()
        {
            return"DoSomething1()";
        }
    }
}

代码清单9-43 分部类Car2.cs
namespace ProgrammingCSharp4
{
    partial class Car
    {
        public string DoSomething2()
        {
            return "DoSomething2()";
        }
    }
}

在Car1.cs和Car2.cs文件的分部类中,分部定义了DoSomething1(代码清单9-42,第14行)和DoSomething2(代码清单9-43,第5行)方法,在ClassExample类的Main函数中,就可以调用Car实例的DoSomething2方法。

我们来看一下编译后生成的Car类的CIL代码,如图9-15所示。

由此可以说明,分部类虽然定义在了不同的位置,但编译器会将它们合并为一,就像它们原本就没有分开过。

9.16.2 分部方法

分部方法声明由两个部分组成:定义和实现。分部方法包含在分部类或分部结构中。分部类方法的签名和它的可选实现可以位于同一个或两个不同的分部类中。如果未提供分部方法的实现,则编译器将自动移除方法签名,以及对所有其他地方代码对该方法的调用。因此,分部类中的任何方法都可以随意地使用分部方法。和分部类一样,分部方法也会在编译期被合并成一个方法定义。

分部方法有着严格的限制,如下:

❑声明必须以上下文关键字partial开头;

❑声明不能有访问修饰符,因此是隐式私有的;

❑不能有返回值;

❑可以有ref参数,不能有out参数;

❑分部方法可以使用static和unsafe1修饰符;

❑参数名称在实现声明和定义声明中虽然可以不同,但仍然推荐使用一致的方法签名。

因为对没有实现的分部方法的调用都会被编译器移除,所以说这些限制是非常有必要的。分部方法的一个典型应用场景就是分离代码生成器生成的代码和用户编写的代码。

接下来,我们来看一个示例代码,如代码清单9-44和代码清单9-45所示。代码清单9-44 分部代码Car1.cs

cs 复制代码
namespace ProgrammingCSharp4
{
    class ClassExample
    {
        static void Main()
        {
            Car car = new Car();
            System.Console.WriteLine(car.DoSomething2());
        }
    }

    partial class Car
    {
        public string DoSomething1()
        {
            return "DoSomething1()";
        }

        partial void DoSomething3(string sth1);
    }
}

代码清单9-45 分部代码Car2.cs

cs 复制代码
namespace ProgrammingCSharp4
{
    partial class Car
    {
        public string DoSomething2()
        {
            DoSomething3("sth");
            return "DoSomething2()";
        }

        partial void DoSomething3(string sth)
        {
            System.Console.WriteLine("DoSomething3()");
        }
    }
}

说明:

我们在Car1.cs中的分部类Car中声明了DoSomething3(代码清单9-44,第19行)方法,这类只是一个方法签名。然后在Car2.cs中为DoSomething3方法提供了实现(代码清单9-45,第11行),注意此处需要包含分部方法的签名,并在DoSomething2方法中调用了DoSomething3方法(代码清单9-45,第7行)。然后,我们观察一下在提供了DoSomething3方法实现和没有提供该方法实现这两种情况下编译器生成的类Car有哪些区别。

先来看看第一种情况,如图9-16所示。

可以看到,Car类中包含DoSomething3代码,签名和分部方法的签名一致。这里需要特别关注DoSomething2的代码,如代码清单9-46所示。

代码清单9-46 DoSomething2方法的CIL代码

html 复制代码
method public hidebysig instance string
DoSomething2()cil managed
{
//Code size 23(0x17)
.maxstack 2
.locals init([0]string CS$1$0000)
IL_0000:nop
IL_0001:ldarg.0
IL_0002:ldstr"sth"
IL_0007:call instance void
ProgrammingCSharp4.Car:DoSomething3(string)
IL_000c:nop
IL_000d:ldstr"DoSomething2()"
IL_0012:stloc.0
IL_0013:br.s IL_0015
IL_0015:ldloc.0
IL_0016:ret
}//end of method Car:DoSomething2

在第10行可见对DoSomething3方法的调用。接下来,将代码清单9-45中的第11~14行的注释去掉,如代码清单9-47所示。

代码清单9-47 注释掉DoSomething3方法实现的情况

cs 复制代码
namespace ProgrammingCSharp4
{
    partial class Car
    {
        public string DoSomething2()
        {
            DoSomething3("sth");
            return "DoSomething2()";
        }

        // partial void DoSomething3(string sth)
        // {
        //     System.Console.WriteLine("DoSomething3()");
        // }
    }
}

此时,DoSomething3方法的实现已经去除了,因为DoSomething2方法调用了未实现的DoSomething3方法,因此编译器会将此调用移除,而不会导致编译时错误或运行时错误,编译后的Car如图9-17所示。

再观察一下DoSomething2方法的CIL代码,观察它和代码清单9-46的异同,如代码清单9-48所示。

代码清单9-48 DoSomething2方法的CIL代码

html 复制代码
.method public hidebysig instance string
DoSomething2()cil managed
{ //Code size 11(0xb)
.maxstack 1
.locals init([0]string CS$1$0000)
IL_0000:nop
IL_0001:ldstr"DoSomething2()"
IL_0006:stloc.0
IL_0007:br.s IL_0009
IL_0009:ldloc.0
IL_000a:ret
}//end of method Car:DoSomething2

很显然,代码清单9-48中对DoSomething3方法调用的代码已经被移除。

9.17 注释

注释,顾名思义,其主要作用是对程序中的代码进行提示、说明,是程序中的重要的组成部分。养成良好的写注释的习惯,有助于提高代码的可读性和可维护性。在C#中,注释有三种类型:

❑行注释

❑块注释

❑XML文档注释

在下文中,我们将分别进行详细介绍。

9.17.1 行注释

行注释要求注释内容在同一行,可以用来为某行代码提供注释,也可以注释掉一行代码,或者连续使用多个行注释符号以注释掉多行代码,行注释必须以2个斜杠开头,如:

//后为被注释掉的内容

如果使用行注释对一行代码进行注释说明,可以将行注释放于该行代码的上方(一般不放于下方)、右方,但不能在代码的左方,那样会将该行代码也一起注释了,如代码清单9-49所示。代码清单9-49 行注释示例

cs 复制代码
using System;
using System.Linq;
using System.Xml.Linq;

namespace ProgrammingCSharp4
{
    class XmlHandleSample
    {
        public static void Main()
        {
            try
            {
                // 行注释
                XDocument doc = XDocument.Load(@"c:\books.xml");
                XElement root = doc.Element("books"); // 行注释

                var book = (from b in root.Elements()
                            where b.ToString().IndexOf("计算机") > -1
                            select b).First();

                book.Element("title").Value = "Computer Art";

                // 下方一行代码被注释
                // doc.Save(@"c:\books.xml");
            }
            catch (System.Exception)
            {
                Console.WriteLine("处理过程出错,请检查。");
                throw;
            }
        }
    }
}

除了可以手动输入2个斜杠用于注释以外,还可以使用Visual Studio2010提供的工具按钮,方便地为一行或者多行添加或者取消注释,使用方法很简单,只需选中要注释的一行或者多行代码,然后单击注释或取消注释按钮即可,如图9-18所示。

如果你的Visual Studio 2010没有此工具栏,请在工具栏单击鼠标右键,选择"文本编辑器"选项即可出现此工具栏,如图9-19所示。

9.17.2 块注释

块注释(/*......*/)可以方便地对多行代码进行注释,一般用于注释掉一个代码块,当然也可以使用工具栏上的注释按钮。在VisualStudio 2010中并未对块注释提供工具栏按钮,块注释如图9-20所示。

9.17.3 XML文档注释

XML文档注释是和文档有关的注释,它允许我们为代码创建文档。该文档可由专门的程序从源代码中读取,并根据需要生成HTML文件或者CHM帮助文件等。添加XML文档注释非常简单,只需要在要添加的程序对象上连续输入3个斜杠即可,然后Visual Studio 2010会自动生成大部分的注释文本,我们只需在其中添加对各注释元素的说明即可,如代码清单9-50所示。

代码清单9-50 XML文档注释示例

///<summary>

///演示异步调用的代码示例

///</summary>

class AsynchronousSample

{

//......

}

当我们连续输入3个斜杠(/)时,一个注释框架会自动产生,如下所示:

///<summary>

///

///</summary>

如果被注释的程序元素,假设是一个方法,具有参数和返回值,那么自动生成的注释框架也会包含这些内容,如代码清单9-51所示。

代码清单9-51 另一个XML文档注释

cs 复制代码
/// <summary>
/// 判断是否相等
/// </summary>
/// <param name="x">对象x</param>
/// <param name="y">对象y</param>
/// <returns>相等返回true,否则返回false</returns>
public bool Equals(Product x, Product y)
{
    // 先判断引用是否相等(同一对象/均为null)
    if (Object.ReferenceEquals(x, y))
        return true;

    // 判断任一对象为null(排除均为null的情况,已在上一步处理)
    if (Object.ReferenceEquals(x, null) || Object.ReferenceEquals(y, null))
        return false;

    // 比较核心业务属性是否相等
    return x.Code == y.Code && x.Name == y.Name;
}

截至现在,我们已经见过XML文档注释中的<summary>、<param>、<returns>等标记,这些标记的含义是什么呢?接下来将对这些标记进行介绍,如表9-10所示。




相关推荐
iCxhust29 分钟前
C# 生成命令行程序 将hex格式烧录程序转换成bin烧录格式
开发语言·汇编·单片机·嵌入式硬件·c#·微机原理
xiaoshuaishuai830 分钟前
C# 封装与继承
开发语言·c#
FL162386312933 分钟前
基于C#winform使用纯opencv部署ppocrv5和ppocrv6的onnx模型进行OCR文件检测识别
opencv·c#·ocr
小满Autumn6 小时前
log4net 日志框架 — 从配置到实战速查手册
笔记·c#·.net·wpf·上位机·log4net
JaydenAI11 小时前
[对比学习LangChain和MAF-07]如何引入人机交互的审批流程
python·ai·langchain·c#·agent·hitl·maf
北域码匠13 小时前
SHA-1算法:安全哈希原理与应用解析
算法·c#·哈希算法
123的故事14 小时前
工具分享(7)-多Excel文件内容查询工具
c#·excel·实用工具
iCxhust16 小时前
C#进程管理程序
开发语言·汇编·stm32·单片机·c#·微机原理
ceclar12317 小时前
C# 的任务并行库(TPL)
开发语言·c#·.net
xiaoshuaishuai818 小时前
C# 委托与事件
开发语言·c#