c#高级编程 类与结构 (一)
1、类
类和结构实际上都是创建对象的模板,每个对象都包含数据,并提供了处理和访问数据的方法。类定义了类的每个对象(称为实例)可以包含什么数据和功能。
class PhoneCustomer { public const string DayOfSendingBill = "Monday"; public int CustomerID; public string FirstName; public string LastName; }
1.1 成员
类包含成员,成员可以是静态或实例成员。静态成员属于类;实例成员属于对象。静态字段的值对每个对象都是相同的。而每个对象的实例字段都可以有不同的值。静态成员关联了static修饰符。
成员 | 说明 |
字段 | 字段是类的数据成员,它是类型的一个变量,该类型是类的一个成员 |
常量 | 常量与类相关(尽管它们没有static修饰符)。编译器使用真实值代替常量 |
方法 | 方法是与特定类相关联的函数 |
属性 | 属性是可以从客户端访问的函数组,其访问方式与访问类的公共字段类似。C#为读写类中的属性提供了专用语法,所以不必使用那些名称中嵌有Get或Set的方法。因为属性的这种语法不同于一般函数的语法,所有在客户端代码中,虚拟的对象被当作实际的东西 |
构造函数 | 构造函数是在实例化对象时自动调用的特殊函数。它们必须与所属的类同名,且不能有返回类型。构造函数用于初始化字段的值 |
索引器 | 索引器允许对象用访问数组的方式访问。 |
运算符 | 运算符执行的最简单的操作就是加法和减法。在两个整数相加时,严格地说,就是对整数使用“+”运算符。C#还允许指定把已有的运算符应用于自己的类(运算符重载)。 |
事件 | 事件是类的成员,在发生某些行为(如修改类的字段或属性,或者进行了某种形式的用户交互操作)时,它可以让对象通知调用方。 客户可以包含所谓“事件处理程序”的代码来响应该事件。 |
析构函数 | 析构函数或终结器的语法类似于构造函数的语法,但是在CLR检测到不再需要某个对象时调用它。它们的名称与类相同,但前面有一个“~”符号。不可能预测什么时候调用终结器。 |
类型 | 类可以包含内部类。如果内部类型只和外部类型结合使用,就很有趣 |
1.1.1 字段
字段是与类相关的变量。
var customer1 = new PhoneCustomer(); customer1.FirstName = "Simon";
1.1.2 常量
常量与类的关联方式和变量与类的关联方式相同。使用const关键字来声明常量。
class PhoneCustomer { public const string DayOfSendingBill = "Monday"; public int CustomerID; public string FirstName; public string LastName; }
最好不把字段声明为public。如果修改类的公共成员,使用这个公共成员的每个调用程序也需要更改。
1.1.3 属性
属性的概念是:它是一个方法或一对方法,在客户端代码看来,它(们)是一个字段。
下面把前面示例中变量名为_firstName的名字字段改为私有。FirstName属性包含get和set访问器,来检索和设置支持字段的值:
class PhoneCustomer { private string _firstName; public string FirstName { get { return _firstName; } set { firstName = value; } } }
get访问器不带任何参数,且必须返回属性声明的类型。也不应为set访问器指定任何显式参数,但编译器假定它带一个参数,其类型也与属性相同,并表示为value。
1.1.4 方法
正式的C#术语区分函数和方法。在C#术语中,“函数成员”不仅包含方法,也包含类或结构的一些非数据成员,如索引器、运算符、构造函数和析构函数等,甚至还有属性。这些都不是数据成员,字段、常量和事件才是数据成员。
1.1.4.1 方法的声明
方法的定义包括任意方法修饰符(如方法的可访问性)、返回值的类型, 然后依次是方法名、输入参数的列表(用圆括号括起来)和方法体(用花括号括起来)。
[modifiers] return_type MethodName([parameters]) { // Method body }
每个参数都包括参数的类型名和在方法体中的引用名称。但如果方法有返回值,则return语句就必须与返回值一起使用,以指定出口点,例如:
public bool IsSquare(Rectangle rect) { return (rect.Height == rect.Width); }
如果方法没有返回值,就把返回类型指定为void,因为不能省略返回类型。如果方法不带参数,仍需要在方法名的后面包含一对空的圆括号()。此时return语句就是可选的——当到达右花括号时,方法会自动返回。
1.1.4.2 表达式体方法
如果方法的实现只有一个语句,C# 6为方法定义提供了一个简化的语法:表达式体方法。使用新的语法,不需要编写花括号和return关键字,而使用运算符= >(lambda操作符)区分操作符左边的声明和操作符右边的实现代码。
lambda操作符的右侧定义了方法的实现代码。不需要花括号和返回语句。返回的是语句的结果,该结果的类型必须与左边方法声明的类型相同,在下面的代码片段中,该类型是bool:
public bool IsSquare(Rectangle rect) => rect.Height == rect.Width;
1.1.4.3 调用方法
在下面的例子中,说明了类的定义和实例化、方法的定义和调用的语法。类Math定义了静态成员和实例成员:
public class Math { public int Value { get; set; } public int GetSquare() => Value * Value; public static int GetSquareOf(int x) => x * x; public static double GetPi() => 3.14159; }
Program类利用Math类,调用静态方法,实例化一个对象,来调用实例成员
using static System.Console; namespace MathSample { class Program { static void Main() {// Try calling some static functions. WriteLine($"Pi is {Math.GetPi()}"); int x = Math.GetSquareOf(5); WriteLine($"Square of 5 is {x}"); // Instantiate a Math object var math = new Math(); // instantiate a reference type // Call instance members math.Value = 30; WriteLine($"Value field of math variable contains {math.Value}"); WriteLine($"Square of 30 is {math.GetSquare()}"); } }
运行MathSample示例,会得到如下结果:
Pi is 3.14159 Square of 5 is 25 Value field of math variable contains 30 Square of 30 is 900
可以看出,Math类包含一个属性和一个方法,该属性包含一个数字,该方法计算该数字的平方。这个类还包含两个静态方法,一个返回pi的值,另一个计算作为参数传入的数字的平方。
1.1.4.4 方法的重载
C#支持方法的重载——方法的几个版本有不同的签名(即,方法名相同,但参数的个数和/或数据类型不同)。为了重载方法,只需要声明同名但参数个数或类型不同的方法即可:
class ResultDisplayer{ public void DisplayResult(string result) { // implementation } public void DisplayResult(int result) { // implementation } }
注意: 对于方法重载,仅通过返回类型不足以区分重载的版本。仅通过参数名称也不足以区分它们。需要区分参数的数量和/或类型。
1.1.4.5 命名的参数
调用方法时,变量名不需要添加到调用中。然而,如果有如下的方法签名,用于移动矩形:
public void MoveAndResize(int x, int y, int width, int height)
用下面的代码片段调用它,就不能从调用中看出使用了什么数字,这些数字用于哪里:
r.MoveAndResize(30, 40, 20, 40);
可以改变调用,明确数字的含义:
r.MoveAndResize(x: 30, y: 40, width: 20, height: 40);
任何方法都可以使用命名的参数调用。只需要编写变量名,后跟一个冒号和所传递的值。
1.1.4.6 可选参数
参数也可以是可选的。必须为可选参数提供默认值。可选参数还必须是方法定义的最后的参数:
public void TestMethod(int notOptionalNumber, int optionalNumber = 42) { WriteLine(optionalNumber + notOptionalNumber); }
这个方法可以使用一个或两个参数调用。传递一个参数,编译器就修改方法调用,给第二个参数传递42。
TestMethod(11); TestMethod(11, 22);
可以定义多个可选参数,如下所示:
public void TestMethod(int n,int opt1 = 11,int opt2 =22,int opt3 =33) { WriteLine(n + opt1 + opt2 + opt3); }
通过多个可选参数,命名参数的特性就会发挥作用。使用命名参数,可以传递任何可选参数,例如,下面的例子仅传递最后一个参数:
TestMethod(1, opt3: 4);
1.1.4.7 个数可变的参数
使用可选参数,可以定义数量可变的参数。然而,还有另一种语法允许传递数量可变的参数——这个语法没有版本控制问题。
声明数组类型的参数(示例代码使用一个int数组),添加params关键字,就可以使用任意数量的int参数调用该方法。
public void AnyNumberOfArguments(params int[] data) { foreach (var x in data) { WriteLine(x); } }
AnyNumberOfArguments方法的参数类型是int[],可以传递一个int数组,或因为
params关键字,可以传递一个或任何数量的int值:
AnyNumberOfArguments(1); AnyNumberOfArguments(1, 3, 5, 7, 11, 13);
如果应该把不同类型的参数传递给方法,可以使用object数组:
public void AnyNumberOfArguments(params object[] data) { // etc. }
现在可以使用任何类型调用这个方法:
AnyNumberOfArguments("text", 42);
如果params关键字与方法签名定义的多个参数一起使用,则params只能使用一次,而且它必须是最后一个参数:
WriteLine(string format, params object[] arg);
1.1.5 构造函数
声明基本构造函数的语法就是声明一个与包含的类同名的方法,但该方法没有返回类型:
public class MyClass { public MyClass() { } }
一般情况下,如果没有提供任何构造函数,编译器会在后台生成一个默认的构造函数。
构造函数的重载遵循与其他方法相同的规则。换言之,可以为构造函数提供任意多的重载,只要它们的签名有明显的区别即可:
public MyClass() // zeroparameter constructor { // construction code }public MyClass(int number) // another overload { // construction code }
如果提供了带参数的构造函数,编译器就不会自动提供默认的构造函数。只有在没有定义任何构造函数时,编译器才会自动提供默认的构造函数。
1.1.5.1 从构造函数中调用其他构造函数
在一个类中有几个构造函数,以容纳某些可选参数,这些构造函数包含一些共同的代码。例如,下面的情况:
class Car { private string _description; private uint _nWheels; public Car(string description, uint nWheels) { _description = description; _nWheels = nWheels; } public Car(string description) { _description = description; _nWheels = 4; }
这两个构造函数初始化相同的字段,显然,最好把所有的代码放在一个地方。C#有一个特殊的语法,称为构造函数初始化器,可以实现此目的:
class Car { private string _description; private uint _nWheels; public Car(string description, uint nWheels) { _description = description; _nWheels = nWheels; } public Car(string description): this(description, 4) { }
这里,this关键字仅调用参数最匹配的那个构造函数。注意,构造函数初始化器在构造函数的函数体之前执行。现在假定运行下面的代码:
var myCar = new Car("Proton Persona");
在本例中,在带一个参数的构造函数的函数体执行之前,先执行带两个参数的构造函数(但在本例中,因为在带一个参数的构造函数的函数体中没有代码,所以没有区别)。
C#构造函数初始化器可以包含对同一个类的另一个构造函数的调用(使用前面介绍的语法),也可以包含对直接基类的构造函数的调用(使用相同的语法,但应使用base关键字代替this)。初始化器中不能有多个调用。
1.1.5.2 静态构造函数
C#的一个新特征是也可以给类编写无参数的静态构造函数。这种构造函数只执行一次,而前面的构造函数是实例构造函数,只要创建类的对象,就会执行它。
class MyClass { static MyClass() { // initialization code } // rest of class definition }
编写静态构造函数的一个原因是,类有一些静态字段或属性,需要在第一次使用类之前,从外部源中初始化这些静态字段和属性。
在C#中,通常在第一次调用类的任何成员之前执行静态构造函数,确保静态构造函数至多运行一次。
注意,静态构造函数没有访问修饰符。出于同样原因,静态构造函数不能带任何参数,一个类也只能有一个静态构造函数。
很显然,静态构造函数只能访问类的静态成员,不能访问类的实例成员。
1.1.6 只读成员
如果不希望在初始化后修改数据成员,就可以使用readonly关键字。
为了保证对象的字段不能改变,字段可以用readonly修饰符声明。带有readonly修饰符的字段只能在构造函数中分配值。
。它与const修饰符不同。编译器通过const修饰符,用其值取代了使用它的变量。编译器知道常量的值。只读字段在运行期间通过构造函数指定。
与常量字段相反,只读字段可以是实例成员。使用只读字段作为类成员时,需要把static修饰符分配给该字段。
如果有一个用于编辑文档的程序,因为要注册,所以需要限制可以同时打开的文档数。现在假定要销售该软件的不同版本,而且顾客可以升级他们的版本,以便同时打开更多的文档。显然,不能在源代码中对最大文档数进行硬编码,而是需要一个字段来表示这个最大文档数。这个字段必须是只读的——每次启动程序时,从注册表键或其他文件存储中读取。代码如下所示:
public class DocumentEditor { private static readonly uint s_maxDocuments; static DocumentEditor() {s_maxDocuments = DoSomethingToFindOutMaxNumber(); } }
1.1.6.1 只读属性
在属性定义中省略set访问器,就可以创建只读属性。因此,如下代码把Name变成只读属性:
private readonly string _name; public string Name { get { return _name; } }
用readonly修饰符声明字段,只允许在构造函数中初始化属性的值。
同样,在属性定义中省略get访问器,就可以创建只写属性。但是,这是不好的编程方式,因为这可能会使客户端代码的作者感到迷惑。一般情况下,如果要这么做,最好使用一个方法替代。
1.1.6.2 自动实现的只读属性
C# 6提供了一个简单的语法,使用自动实现的属性创建只读属性,访问只读字段。这些属性可以使用属性初始化器来初始化。
public string Id { get; } = Guid.NewGuid().ToString();
在后台,编译器会创建一个只读字段和一个属性,其get访问器可以访问这个字段。初始化器的代码进入构造函数的实现代码,并在调用构造函数体之前调用。当然,只读属性也可以在构造函数中初始化,如下面的代码片段所示:
public class Person { public Person(string name) { Name = name; } public string Name { get; } }
1.1.6.3 表达式体属性
C# 6中与属性相关的另一个扩展是表达式体属性。类似于表达式体方法,表达式体属性不需要花括号和返回语句。表达式体属性是带有get访问器的属性,但不需要编写get关键字。只是get访问器的实现后跟lambda操作符。对于Person类,FullName属性使用表达式体属性实现,通过该属性返回FirstName和LastName属性值的组合:
public class Person { public Person(string firstName, string lastName) { FirstName = firstName; LastName = lastName; } public string FirstName { get; } public string LastName { get; } public string FullName => $"{FirstName} {LastName}"; }
1.1.6.4 不可变的类型
如果对象没有任何可以改变的成员,只有只读成员,它就是一个不可变类型。其内容只能在初始化时设置。这对于多线程是非常有用的,因为多个线程可以访问信息永远不会改变的同一个对象。因为内容不能改变,所以不需要同步。
不可变类型的一个例子是String类。这个类没有定义任何允许改变其内容的成员。诸如ToUpper(把字符串更改为大写)的方法总是返回一个新的字符串,但传递到构造函数的原始字符串保持不变。
2、结构
结构与类非常相似,主要的区别是使用关键字struct代替class来声明结构。
public struct Dimensions { public double Length { get; set; } public double Width { get; set; } }
为结构定义函数与为类定义函数完全相同。下面的代码说明了结构的构造函数和属性:
public struct Dimensions {public double Length { get; set; } public double Width { get; set; } public Dimensions(double length, double width) { Length = length; Width = width; } public double Diagonal => Math.Sqrt(Length * Length + Width * Width); }
结构是值类型,不是引用类型。它们存储在栈中或存储为内联(如果它们是存储在堆中的另一个对象的一部分),其生存期的限制与简单的数据类型一样。
结构不支持继承。
对于结构,构造函数的工作方式有一些区别。如果没有提供默认的构造函数,编译器会自动提供一个,把成员初始化为其默认值。
使用结构,可以指定字段如何在内存中布局。
因为结构实际上是把数据项组合在一起,所以有时大多数或者全部字段都声明为public。
2.1 结构是值类型
虽然结构是值类型,但在语法上常常可以把它们当作类来处理。例如,在上面的Dimensions类的定义中,可以编写下面的代码:
var point = new Dimensions(); point.Length = 3; point.Width = 6;
注意,因为结构是值类型,所以new运算符与类和其他引用类型的工作方式不同。
new运算符并不分配堆中的内存,而是只调用相应的构造函数,根据传送给它的参数,初始化所有的字段。对于结构,可以编写下述完全合法的代码:
Dimensions point; point.Length = 3; point.Width = 6;
结构遵循其他数据类型都遵循的规则:在使用前所有的元素都必须进行初始化。在结构上调用new运算符,或者给所有的字段分别赋值,结构就完全初始化了。当然,如果结构定义为类的成员字段,在初始化包含的对象时,该结构会自动初始化为0。
但当把结构作为参数传递给方法时,应把它作为ref参数传递,以避免性能损失——此时只传递了结构在内存中的地址,这样传递速度就与在类中的传递速度一样快了。但如果这样做,就必须注意被调用的方法可以改变结构的值。
2.2 结构和继承
结构不是为继承设计的。这意味着:它不能从一个结构中继承。唯一的例外是对应的结构最终派生于类System.Object。因此,结构也可以访问System.Object的方法。在结构中,甚至可以重写System.Object中的方法——如重写ToString()方法。
结构的继承链是:每个结构派生自System.ValueType类,System.ValueType类又派生自System.Object。ValueType并没有给Object添加任何新成员,但提供了一些更适合结构的实现方式。注意,不能为结构提供其他基类:每个结构都派生自ValueType。
2.3 结构的构造函数
为结构定义构造函数的方式与为类定义构造函数的方式相同。
前面说过,默认构造函数把数值字段都初始化为0,且总是隐式地给出,即使提供了其他带参数的构造函数,也是如此。
在C# 6中,也可以实现默认的构造函数,为字段提供初始值(这一点在早期的C#版本中未实现)。为此,只需要初始化每个数据成员:
public Dimensions() { Length = 0;Width = 1; } public Dimensions(double length, double width) { Length = length; Width = width; }
另外,可以像类那样为结构提供Close()或Dispose()方法。
拓展:
3、类与结构
结构不同于类,因为它们不需要在堆上分配空间(类是引用类型,总是存储在堆(heap)上),而结构是值类型,通常存储在栈(stack)上,另外,结构不支持继承。
对于类和结构,都使用关键字new来声明实例,这个关键字创建对象并对其进行初始化。在下面的例子中,类和结构的字段值都默认为0:
var myCustomer = new PhoneCustomer(); // works for a class var myCustomer2 = new PhoneCustomerStruct(); // works for a struct
在大多数情况下,类要比结构常用得多。因此,我们先讨论类,然后指出类和结构的区别,以及选择使用结构而不使用类的特殊原因。但除非特别说明,否则就可以假定用于类的代码也适用于结构。
4、部分类
partial关键字允许把类、结构、方法或接口放在多个文件中。一般情况下,某种类型的代码生成器生成了一个类的某部分,所以把类放在多个文件中是有益的。
partial关键字的用法是:把partial放在class、struct或interface关键字的前面。在下面的例子中,SampleClass类驻留在两个不同的源文件SampleClassAutogenerated.cs和SampleClass.cs中:
//SampleClassAutogenerated.cs partial class SampleClass { public void MethodOne() { } } //SampleClass.cs partial class SampleClass { public void MethodTwo() { } }
编译包含这两个源文件的项目时,会创建一个SampleClass类,它有两个方法MethodOne()和MethodTwo()。
如果声明类时使用了下面的关键字,则这些关键字就必须应用于同一个类的所有部分:
● public ● private ● protected ● internal
● abstract ● sealed ● new ● 一般约束
在嵌套的类型中,只要partial关键字位于class关键字的前面,就可以嵌套部分类。在把部分类编译到类型中时,属性、XML注释、接口、泛型类型的参数属性和成员会合并。有如下两个源文件:
// SampleClassAutogenerated.cs [CustomAttribute] partial class SampleClass: SampleBaseClass, ISampleClass { public void MethodOne() { } } // SampleClass.cs [AnotherAttribute] partial class SampleClass: IOtherSampleClass { public void MethodTwo() { } }
编译后,等价的源文件变成:
[CustomAttribute] [AnotherAttribute] partial class SampleClass: SampleBaseClass, ISampleClass, IOtherSampleClass { public void MethodOne() { }public void MethodTwo() { } }
注意: 尽管partial关键字很容易创建跨多个文件的巨大的类,且不同的开发人员处理同一个类的不同文件,但该关键字并不用于这个目的。在这种情况下,最好把大类拆分成几个小类,一个类只用于一个目的。
部分类可以包含部分方法。如果生成的代码应该调用可能不存在的方法,这就是非常有用的。扩展部分类的程序员可以决定创建部分方法的自定义实现代码,或者什么也不做。下面的代码片段包含一个部分类,其方法MethodOne调用APartialMethod方法。APartialMethod方法用partial关键字声明;因此不需要任何实现代码。如果没有实现代码,编译器将删除这个方法调用:
//SampleClassAutogenerated.cs partial class SampleClass { public void MethodOne() { APartialMethod(); } public partial void APartialMethod(); }
部分方法的实现可以放在部分类的任何其他地方,如下面的代码片段所示。有了这个方法,编译器就在MethodOne内创建代码,调用这里声明的APartialMethod:
// SampleClass.cs partial class SampleClass: IOtherSampleClass { public void APartialMethod() { // implementation of APartialMethod} }
部分方法必须是void类型,否则编译器在没有实现代码的情况下无法删除调用。
5、扩展方法
扩展方法是给对象添加功能的另一个选项,在不能使用继承时,也可以使用这个选项(例如类是密封的)。
注意: 扩展方法也可以用于扩展接口。
扩展方法是静态方法,它是类的一部分,但实际上没有放在类的源代码中。
假设希望用一个方法扩展string类型,该方法计算字符串中的单词数。GetWordCount方法利用String.Split方法把字符串分割到字符串数组中,使用Length属性计算数组中元素的个数:
public static class StringExtension { public static int GetWordCount(this string s) => s.Split().Length; }
使用this关键字和第一个参数来扩展字符串。这个关键字定义了要扩展的类型。即使扩展方法是静态的,也要使用标准的实例方法语法。注意,这里使用fox变量而没有使用类型名来调用GetWordCount ()。
string fox = "the quick brown fox jumped over the lazy dogs down " + "9876543210 times"; int wordCount = fox.GetWordCount(); Console.WriteLine($"{wordCount} words"); Console.Read();
在后台,编译器把它改为调用静态方法:
int wordCount = StringExtension.GetWordCount(fox);
使用实例方法的语法,而不是从代码中直接调用静态方法,会得到一个好得多的语法。这个语法还有一个好处:该方法的实现可以用另一个类取代,而不需要更改代码—只需要运行新的编译器。
编译器如何找到某个类型的扩展方法?
this关键字必须匹配类型的扩展方法,而且需要打开定义扩展方法的静态类所在的名称空间。如果把StringExtensions类放在名称空间Wrox. Extensions中,则只有用using指令打开Wrox.Extensions,编译器才能找到GetWordCount方法。
如果类型还定义了同名的实例方法,扩展方法就永远不会使用。类中已有的任何实例方法都优先。
当多个同名的扩展方法扩展相同的类型,打开所有这些类型的名称空间时,编译器会产生一个错误,指出调用是模棱两可的,它不能决定在多个实现代码中选择哪个。然而,如果调用代码在一个名称空间中,这个名称空间就优先。
注意: 语言集成查询(Language Integrated Query, LINQ)利用了许多扩展方法。
6、Object类
所有的.NET类最终都派生自System.Object。实际上,如果在定义类时没有指定基类,编译器就会自动假定这个类派生自Object。
本章没有使用继承,所以前面介绍的每个类都派生自System.Object(如前所述,对于结构,这个派生是间接的:结构总是派生自System.ValueType, System.ValueType又派生自System.Object)。
其实际意义在于,除了自己定义的方法和属性等外,还可以访问为Object类定义的许多公有的和受保护的成员方法。这些方法可用于自己定义的所有其他类中。
下面将简要总结每个方法的作用:
ToString()方法
获取对象的字符串表示的一种便捷方式。
当只需要快速获取对象的内容,以进行调试时,就可以使用这个方法。在数据的格式化方面,它几乎没有提供选择:例如,在原则上日期可以表示为许多不同的格式,但DateTime.ToString()没有在这方面提供任何选择。如果需要更复杂的字符串表示,例如,考虑用户的格式化首选项或区域性,就应实现IFormattable接口。
GetHashCode()方法
如果对象放在名为映射(也称为散列表或字典)的数据结构中,就可以使用这个方法。处理这些结构的类使用该方法确定把对象放在结构的什么地方。如果希望把类用作字典的一个键,就需要重写GetHashCode()方法。
Equals()(两个版本)和ReferenceEquals()方法
注意有3个用于比较对象相等性的不同方法,这说明.NET Framework在比较相等性方面有相当复杂的模式。这3个方法和比较运算符“==”在使用方式上有微妙的区别。而且,在重写带
一个参数的虚Equals()方法时也有一些限制,因为System.Collections名称空间中的一些基类要调用该方法,并希望它以特定的方式执行。
Finalize()方法
它最接近C++风格的析构函数,在引用对象作为垃圾被回收以清理资源时调用它。Object中实现的Finalize()方法实际上什么也没有做,因而被垃圾回收器忽略。如果对象拥有对未托管资源的引用,则在该对象被删除时,就需要删除这些引用,此时一般要重写Finalize()。垃圾收集器不能直接删除这些对未托管资源的引用,因为它只负责托管的资源,于是它只能依赖用户提供的Finalize()。
GetType()方法
返回从System.Type派生的类的一个实例,因此可以提供对象成员所属类的更多信息,包括基本类型、方法、属性等。System.Type还提供了.NET的反射技术的入口点。、
MemberwiseClone()方法
这是System.Object中唯一没有在本书的其他地方详细论述的方法。不需要讨论这个方法,因为它在概念上相当简单,它只复制对象,并返回对副本的一个引用(对于值类型,就是一个装箱的引用)。注意,得到的副本是一个浅表复制,即它复制了类中的所有值类型。如果类包含内嵌的引用,就只复制引用,而不复制引用的对象。这个方法是受保护的,所以不能用于复制外部的对象。该方法不是虚方法,所以不能重写它的实现代码。