C# 是一种面向对象的编程语言。它支持继承、封装、多态和抽象。本章向您展示了 C# 如何支持面向对象程序设计。
在 C# 中,继承定义了两个类之间的关系,其中派生类可以重用基类的成员。一个简单的观点是,派生类是基类的一个更具体的版本。我们将重复使用前几章中的计算器示例,但对其进行了修改,以提供两种不同类型的计算器:科学计算器和程序员计算器。因为它们都是计算器,所以创建一个与Calculator
基类和ScientificCalculator
及ProgrammerCalculator
派生类的关系会很有用,如下所示:
Calculator
在 C# 中,您可以这样表达这种关系。
public class Calculator { }
public class ScientificCalculator : Calculator { }
public class ProgrammerCalculator : Calculator { }
代码清单 51
如您所见,我们在派生类中添加了一个冒号后缀(作为继承操作符),并指定了它所派生的基类。下图说明了这些类之间的继承关系。
图 1:计算器继承图
你可以假设Calculator
将拥有所有计算器拥有的所有标准运算,如加法、减法等。下面的代码清单是一个扩展示例,显示了具有公共方法的基类,以及具有只属于这些类的专用方法的派生类。
using System;
public class Calculator
{
public double Add(double num1, double num2)
{
return num1 + num2;
}
}
public class ScientificCalculator : Calculator
{
public double Power(double num, double power)
{
return Math.Pow(num, power);
}
}
public class ProgrammerCalculator : Calculator
{
public int Or(int num1, int num2)
{
return num1 | num2;
}
}
代码清单 52
上一个例子中的派生类的方法使用了 FCL Math
类作为Power
,它有更多的数学方法可以在你自己的代码中使用,并且还使用了内置的 C# |
操作符作为Or
。下面的示例演示如何编写利用前面的类继承的代码。
using System;
public class Program
{
public static void Main()
{
ScientificCalculator sciCalc = new ScientificCalculator();
double powerResult = sciCalc.Power(2, 5);
Console.WriteLine($"Scientific Calculator 2**5: {powerResult}");
double sciSum = sciCalc.Add(3, 3);
Console.WriteLine($"Scientific Calculator 3 + 3: {sciSum}");
ProgrammerCalculator prgCalc = new ProgrammerCalculator();
double orResult = prgCalc.Or(5, 10);
Console.WriteLine($"Programmer Calculator 5 | 10: {orResult}");
double prgSum = prgCalc.Add(3, 3);
Console.WriteLine($"Programmer Calculator 3 + 3: {prgSum}");
Console.ReadKey();
}
}
代码清单 53
无论是ScientificCalculator
实例,sciCalc
,还是ProgrammerCalculator
实例,prgCalc
,都称之为Add
法。此外,这些类没有定义自己的Add
,但它们确实从Calculator
派生,因此继承了Calculator
的Add
。
继承的能力并不总是有保证的;下一节将详细解释一个类成员何时对其他类可见。
在前面的例子中,所有的类和方法都有public
修饰符,这意味着任何其他类或代码都可以在代码中看到和访问它们。您可以不使用访问修饰符,接受默认值。在这种情况下,类访问变成了所谓的internal
,类成员默认为private
。
类只能是internal
或public
。如果它们是internal
,它们只能由包含它们的程序集中的代码访问。
类成员可用的访问修饰符包括public
、private
、internal
、internal protected
和protected
。public
和internal
修饰符对于类成员的意义与对于类的意义相同。
类成员的默认修饰符private
,表示类外的代码不能使用该成员;只有同一个类中的其他成员可以使用它。如果您想通过将一个方法分成不同的支持方法来模块化它,但是这些支持方法在类之外没有任何意义,这将非常有用。
protected
修饰符允许程序集内部和外部的派生类使用protected
基类成员。internal protected
修饰符进一步将受保护的行为限制为同一程序集中的派生类。
除了protected
和internal protected
之外,大多数访问修饰符默认和行为都适用于struct
类型和类,下面我会解释。
A struct
是另一个 C# 类型,看起来类似于 a class
,但是行为不同。一个struct
不能衍生出另一个class
或者struct
。由于实现继承不适用于struct
,因此protected
和internal protected
修饰符也不适用。A struct
确实有接口继承,这一点我会在本章后面的暴露接口部分详细解释。
此外,struct
通过值进行复制,而class
通过引用进行复制。不同的是,如果您将一个struct
实例传递给一个方法,该方法将获得一个struct
值的全新副本。如果您将一个class
实例复制到一个方法中,该方法将获得一个对堆中class
的引用的副本,堆是计算机内存中的一个区域,CLR 使用它来为引用类型对象分配空间。这些事实帮助你决定你应该设计一个类型作为class
还是struct
。想象一个有很多属性的类型,如果你必须通过值把它作为struct
传递给一个方法,它会如何影响性能;该类型的状态被复制到栈中,这是 CLR 为每个方法调用分配的内存,用于保存参数和局部变量等项。在这种情况下,正确的设计决策可能是将类型定义为class
,以便仅复制参考。
大多数内置类型,如int
、double
、char
,都是值类型。如果您有一个具有这些语义的类型——小且单一的值——那么将类型设计为struct
可能是一个好处。否则,把一个字体设计成class
就可以了。这里有一个可能成为好的struct
的类型的例子。
public struct Complex
{
public Complex(double real, double imaginary)
{
Real = real;
Imaginary = imaginary;
}
public double Real { get; set; }
public double Imaginary { get; set; }
public static Complex operator +(Complex complex1, Complex complex2)
{
Complex complexSum = new Complex();
complexSum.Real = complex1.Real + complex2.Real;
complexSum.Imaginary = complex1.Imaginary + complex2.Imaginary;
return complexSum;
}
public static implicit operator Complex(double dbl)
{
Complex cmplx = new Complex();
cmplx.Real = dbl;
return cmplx;
}
// This is not a safe operation.
public static explicit operator double(Complex cmplx)
{
return cmplx.Real;
}
}
代码清单 54
Complex
可以做一个很好的struct
,因为你可能有很多数学运算,在栈上传递数字的副本会比让 CLR 分配内存更有效率,就像对一个class
一样。
Complex
有一个构造函数,以类本身命名,带有几个参数。这使得初始化Complex
的新实例变得容易。
Complex
中有几个运算符重载:一个加法运算符和两个转换运算符。加法运算符允许您将两个复数相加。其中运算符标识符(+
)在参数列表之前,要添加的值在参数中指定,返回类型是签名的一部分。操作员永远是static
。
这两个转换运算符允许您在包含类型和您选择的另一种类型之间进行赋值。分配给的类型是运算符标识符,分配的类型是参数。implicit
修饰符表示转换是安全的,explicit
修饰符表示转换可能会丢失数据或提供无效结果。例如,将一个double
分配给一个int
将会是explicit
,因为失去了精度,并且前面例子中的显式转换导致了数字的虚部的损失。下面的代码示例演示了如何使用Complex
。
using System;
class Program
{
static void Main()
{
Complex complex1 = new Complex();
complex1.Real = 3;
complex1.Imaginary = 1;
Complex complex2 = new Complex(7, 5);
Complex complexSum = complex1 + complex2;
Console.WriteLine(
$"Complex sum - Real: {complexSum.Real}, " +
$"Imaginary: {complexSum.Imaginary}");
Complex complex3 = 9;
double realPart = (double)complex3;
Console.ReadKey();
}
}
代码清单 55
Main
方法实例化complex1
,然后填充其值。接下来,Main
使用Complex
构造函数实例化complex2
,这是更简单的初始化代码。
您还可以看到使用加法运算符是多么自然,而不是以前的Calculator
演示中使用的Add
方法。
因为int
到double
有隐式转换,Complex
有double
到Complex
的隐式转换运算符,Main
可以将9
赋值给complex3
。把complex3
赋给realPart
就不能这么说了,因为Complex
到double
是Complex
类型的explicit
转换运算符。任何时候进行explicit
转换,都必须使用强制转换操作符,如(double)complex3
所示。
使用值类型时,您需要注意的一个事项是一个称为装箱和取消装箱的概念。任何时候将值类型赋给object
都会发生装箱,而将object
赋给值类型时会发生拆箱。下面的代码演示了可能发生这种情况的一个场景。
ArrayList intCollection = new ArrayList();
intCollection.Add(7);
int number = (int)intCollection[0];
代码清单 56
一个ArrayList
是属于System.Collections
命名空间的集合类。它比数组更强大,在类型object
上运行。Add
方法接受类型为object
的参数。由于所有类型都源自object
,因此ArrayList
足够灵活,允许您处理任何类型的对象。将7
传递到Add
方法时发生装箱,因为7
是一个int
(值类型)并转换为object
。真正发生的是,CLR 在内存中创建了一个装箱的int
。由于ArrayList
保存类型object
,所以从ArrayList
读取时,还需要执行转换来取消某个值的装箱。(int)
演职人员读取intCollection
第一个元素时,从object
(盒装int
)转换为int
。
装箱拆箱造成的问题与性能有关。在这种情况下,您会使用集合的原因是因为您想要保存大量int
值,可能是数百或数千。想想花在访问那个ArrayList
上的所有时间,以及在每个操作上产生装箱和拆箱的性能损失。
| | 注意:ArrayList 是 C# v1.0 中存在的一个旧的集合类,在现代开发中不再使用。C# v2.0 引入了泛型,它使用强类型的新集合类,避免了装箱和取消装箱的惩罚。虽然数组列表的例子在今天不太可能出现,但是这个场景仍然突出了任何其他情况下的性能损失,在这些情况下,您可能会将值类型分配给对象类型。 |
class
(引用类型)和struct
(值类型)的另一个区别是相等求值。值类型平等通过比较struct
的相应成员起作用。引用类型相等通过验证引用是否相等来工作。换句话说,如果结构的值匹配,则它们是相等的,但是如果它们引用内存中的同一对象,则类是相等的。在后面关于多态性的部分中,您将学习如何覆盖object.Equals
方法,从而为您提供对类相等性的更多控制。
enum
是一种值类型,允许您创建一组强类型助记符值。当您有一组有限的值,并且不想将这些值表示为字符串或数字时,它们非常有用。这里有一个enum
的例子。
public enum MathOperation
{
Add,
Subtract,
Multiply,
Divide
}
代码清单 57
像struct
一样,enum
也是值类型。您使用enum
关键字作为类型定义。前enum
名为MathOperation
,有四名成员。下面的例子展示了如何使用这个enum
。
using System;
using static MathOperation;
class Program
{
static void Main()
{
string[] possibleOperations = Enum.GetNames(typeof(MathOperation));
Console.Write($"Please select ({string.Join(", ", possibleOperations)}): ");
string operationString = Console.ReadLine();
MathOperation selectedOperation;
if (!Enum.TryParse<MathOperation>(operationString, out selectedOperation))
selectedOperation = MathOperation.Add;
switch (selectedOperation)
{
case MathOperation.Add:
Console.WriteLine($"You selected {nameof(Add)}");
break;
case MathOperation.Subtract:
Console.WriteLine($"You selected {nameof(Subtract)}");
break;
case MathOperation.Multiply:
Console.WriteLine($"You selected {nameof(Multiply)}");
break;
case MathOperation.Divide:
Console.WriteLine($"You selected {nameof(Divide)}");
break;
}
Console.ReadKey();
}
}
代码清单 58
FCL 有一个Enum
类,可以让你使用枚举,之前的Main
方法展示了如何使用它的一些方法。Enum.GetNames
返回一个string
数组,代表enum
中的名字,用typeof
运算符指定。string.Join
方法,即Console.WriteLine
内插字符串中的表达式,创建这些名称的逗号分隔字符串。
上例中的Enum.TryParse
方法取一个字符串,产生一个类型参数中指定类型的enum
,在本例中为MathOperation
。out
参数意味着TryParse
将返回selectedOperation
变量中的解析值。这很实用,因为TryParse
的返回类型是bool
,允许您知道输入字符串operationString
是否有效。
selectedOperation
变量属于MathOperation
类型。枚举的默认语法是用枚举类型名称作为前缀,如MathOperation.Add
所示。但是,您也可以在文件的顶部添加一个using static
子句,只允许您指定成员名称,如前面的例子在switch
语句中所示。switch
语句可以对数字、字符串或枚举进行操作。
多态性允许派生类专门化基类实现。允许多态性的机制是用virtual
修饰符修饰基类方法,用override
修饰符修饰派生类方法。如果您正在设计Calculator
类,您可以允许派生类实现它们自己的Add
方法的改进或专用版本,如下例所示。
using System;
public class Calculator
{
public virtual double Add(double num1, double num2)
{
Console.WriteLine("Calculator Add called.");
return num1 + num2;
}
}
public class ProgrammerCalculator : Calculator
{
public override double Add(double num1, double num2)
{
Console.WriteLine("ProgrammerCalculator Add called.");
return MyMathLib.Add(num1, num2);
}
}
public class MyMathLib
{
public static double Add(double num1, double num2)
{
return num1 + num2;
}
}
public class ScientificCalculator : Calculator
{
public override double Add(double num1, double num2)
{
Console.WriteLine("ScientificCalculator Add called.");
return base.Add(num1, num2);
}
}
代码清单 59
多态性是 C# 的选择。注意基类Calculator
中的Add
方法有一个virtual
修改器。除非基类方法有virtual
修饰符,否则多态性不会发生。另外,注意派生类ScientificCalculator
和ProgrammerCalculator
有override
修饰符。同样,这些方法不会被多形态调用,除非它们有override
修饰符。此外,带有override
修饰符的方法也是其任何派生类的virtual
。
通过多态性,派生类中被重写的方法在运行时执行。如果您想调用该方法的基类实现,请使用base
关键字调用基类方法。ScientificCalculator
调用base.Add(num1, num2)
调用Calculator
中的Add
方法。这是一个如何工作的例子。
using System;
public class Program
{
public static void Main()
{
Calculator sciCalc = new ScientificCalculator();
double sciCalcResult = sciCalc.Add(2, 5);
Console.WriteLine($"Scientific Calculator 2 + 5: {sciCalcResult}");
Calculator prgCalc = new ProgrammerCalculator();
double prgCalcResult = prgCalc.Add(5, 10);
Console.WriteLine($"Programmer Calculator 5 + 10: {prgCalcResult}");
Console.ReadKey();
}
}
代码清单 60
该程序的输出将是:
ScientificCalculator Add called.
Calculator Add called.
Scientific Calculator 2 + 5: 7
ProgrammerCalculator Add called.
Programmer Calculator 5 + 10: 15
Main
将ScientificCalculator
和ProgrammerCalculator
的实例分配给类型为Calculator
的变量。正如您在前面的列表中看到的,ScientificCalculator
和ProgrammerCalculator
是派生类型,Calculator
是它们的基类型。派生实例是运行时类型(程序运行时的实际类型),基类是编译时类型。运行时类型重写在运行时执行。
查看ScientificCalculator
、Calculator
、Main
中Add
的定义,查看输出,可以追溯这个程序的多态行为。Main
在ScientificCalculator
实例上调用Add
。ScientificCalculator.Add
执行是因为它覆盖了virtual
Calculator.Add
方法。写完第一行输出后,ScientificCalculator.Add
用base
关键字调用Calculator.Add
方法。Calculator.Add
将第二行打印到输出,执行加法计算,并返回总和。ScientificCalculator.Add
返回Calculator.Add
的返回值。Main
将ScientificCalculator.Add
的返回值赋给sciCalc
变量,并将结果打印到输出的第三行。追踪对ProgrammerCalculator.Add
的调用类似,只是基类中没有对Calculator.Add
的调用。
另一个你想使用多态性的例子是定义引用类型相等。默认情况下,引用类型只有在引用相同时才相等。下面的示例演示如何控制引用类型相等。
public class Customer
{
int id;
string name;
public Customer(int id, string name)
{
this.id = id;
this.name = name;
}
public override bool Equals(object obj)
{
if (obj == null)
return false;
if (obj.GetType() != typeof(Customer))
return false;
Customer cust = obj as Customer;
return id == cust.id;
}
public static bool operator ==(Customer cust1, Customer cust2)
{
return cust1.Equals(cust2);
}
public static bool operator !=(Customer cust1, Customer cust2)
{
return !cust1.Equals(cust2);
}
public override int GetHashCode()
{
return id;
}
public override string ToString()
{
return $"{{ id: {id}, name: {name} }}";
}
}
代码清单 61
因为所有类都隐式地从object
派生,所以它们可以override
对象virtual
方法Equals
、GetHashCode
和ToString
。Customer
覆盖Equals
。当您覆盖Equals
时,在使用对象之前检查null
和类型是否相等,以防止调用方意外比较null
或不兼容的类型。Customer
实例相同,则相等id
。
Customer
有一个初始化类状态的构造函数。this
运算符允许您访问包含实例的成员,并有助于避免歧义。
实现自定义等式时,还应该重载 equals 和 not equals,并重写GetHashCode
方法。GetHashCode
的默认实现是一个系统定义的对象id
,因此您可以覆盖它来实现哈希值的更好分布。
| | 提示:您可以在字符串插值中将不想作为表达式计算的{和}字符分别加倍为{{和}}来转义它们。 |
下面是一个如何检查Customer
实例相等性的例子。
using System;
class Program
{
static void Main()
{
Customer cust1 = new Customer(1, "May");
Customer cust2 = new Customer(2, "Joe");
Console.WriteLine($"cust1 == cust2: {cust1 == cust2}");
Customer cust3 = new Customer(1, "May");
Console.WriteLine($"\ncust1 == cust3: {cust1 == cust3}");
Console.WriteLine($"cust1.Equals(cust3): {cust1.Equals(cust3)}");
Console.WriteLine($"object.ReferenceEquals(cust1, cust3): {object.ReferenceEquals(cust1, cust3)}");
Console.WriteLine($"\ncust1: {cust1}");
Console.WriteLine($"cust2: {cust2}");
Console.WriteLine($"cust3: {cust3}");
Console.ReadKey();
}
}
代码清单 62
使用==
运算符时,代码调用运算符重载,Equals
按预期调用 equals 方法。ReferenceEquals
是一个有用的object
方法,因为它允许在类型定义了自定义Equals
覆盖的情况下进行引用相等性检查。
如果Customer
没有覆盖ToString
,那么前面代码清单中的最后三个Console.WriteLine
语句会打印类型名,这是ToString
的默认行为。
在前面的例子中,你可以创建一个Calculator
的实例。但是,实例化基类可能有意义,也可能没有意义。基类可能只是作为类似派生类的公共功能的可重用类型,并支持多态性,但还没有足够的内容可以单独使用。在这种情况下,您可以将类定义修改为abstract
,如下例所示。
public abstract class Calculator
{
// ...
}
代码清单 63
在abstract
类中,可以有virtual
或非虚拟成员。另外,你可以有abstract
方法。一个abstract
方法没有实现。派生类应该指定实现,并且您不希望基类中有一个可能没有意义的默认实现。abstract
方法的目的是指定派生类必须实现的接口。在Calculator
的情况下,您可以定义一个abstract
Add
方法,如下面的代码示例所示。
public abstract class Calculator
{
public abstract double Add(double num1, double num2);
}
代码清单 64
Add
方法有一个abstract
修改器。这个方法是隐式虚拟的,但是不能被派生类调用,因为它没有实现。需要分号来终止abstract
方法签名。当一个abstract
类有abstract
方法时,所有的派生类都必须有override
方法。如果将非抽象Calculator
类的定义更改为以前的abstract
Calculator
,上一节中的Main
方法仍会运行。
抽象类非常适合您想要一些默认行为、指定类的公共接口以及支持多态性的情况。但是,有一些限制,因为 C# 类只能有一个基类。此外,一个结构不能继承另一个类或结构,所以如果您需要编写允许您用基类实现替换任意数量的值类型的代码,它们没有帮助。还有一种选择,我接下来会讨论。
如果您只想要一个为一组公共操作指定接口的基类,那么您可以创建一个只有抽象方法的抽象类。这确保了所有派生类都有这些抽象方法。然而,还有一个更好的选择,以它的功能命名:一个interface
。
interface
类型的好处是class
和struct
类型都可以继承多个接口。您也可以用接口实现多态性。它们没有任何实现,您必须在派生类中编写实现。下面的代码清单显示了重写为接口的Calculator
类。
public interface ICalculator
{
double Add(double num1, double num2);
}
代码清单 65
您将使用interface
类型,而不是class
或struct
。接口标识符的一个常见约定是I
前缀,如在ICalculator
中。接口方法是隐式公共的和虚拟的,因此您不需要访问、抽象或虚拟修饰符。和abstract
方法一样,interface
方法有签名,但没有实现。开发人员在其从接口派生的类中提供该实现。下面的代码示例是之前实现ICalculator
接口的类的修订版。
public class ScientificCalculator : ICalculator
{
public double Add(double num1, double num2)
{
return num1 + num2;
}
}
public class ProgrammerCalculator : ICalculator
{
public double Add(double num1, double num2)
{
return MyMathLib.Add(num1, num2);
}
}
public class MyMathLib
{
public static double Add(double num1, double num2)
{
return num1 + num2;
}
}
代码清单 66
从接口派生使用与从类派生相同的语法,即在类名后添加冒号和接口名。与虚拟方法不同,您不能在方法上使用override
修改器。
派生类实现必须是public
。这很有意义,因为接口定义了一个契约,任何派生类都将在接口中定义成员。这意味着,每当您通过类的接口使用类时,您都知道它将拥有由接口定义的成员。下面的代码示例是对使用ICalculator
接口的Main
方法的修改。
using System;
public class Program
{
public static void Main()
{
ICalculator sciCalc = new ScientificCalculator();
double sciCalcResult = sciCalc.Add(2, 5);
Console.WriteLine($"Scientific Calculator 2 + 5: {sciCalcResult}");
ICalculator prgCalc = new ProgrammerCalculator();
double prgCalcResult = prgCalc.Add(5, 10);
Console.WriteLine($"Programmer Calculator 5 + 10: {prgCalcResult}");
Console.ReadKey();
}
}
代码清单 67
这个例子和上一个例子唯一的语法区别是sciCalc
和prgCalc
的编译时类型是ICalculator
。因为每个变量都是ICalculator
,所以可以保证运行时类型实现了该接口的成员。
接口也可以继承其他接口。在这种情况下,派生类必须实现继承链中每个接口的所有成员。同样,一个class
或struct
可以实现多个接口,如下例所示。
public interface ICalculator { }
public interface IMath { }
public class ScientificCalculator : ICalculator, IMath
{
public double Add(double num1, double num2)
{
return num1 + num2;
}
}
代码清单 68
在第一个接口之后,其他接口出现在逗号分隔的列表中。类或结构必须实现它所派生的所有接口的方法。
值类型(struct
或enum
)的生存期取决于它的分配位置。参数和变量值类型实例驻留在栈上,只要它们在作用域内就存在。引用类型实例(class
)在其构造函数执行时开始生命。CLR 在托管堆上分配它们的空间,它们一直存在,直到 CLR 垃圾收集器(GC)清理它们。
您可以使用构造函数来初始化类。这样做的同时,您还可以影响静态、基类型和其他构造函数重载的初始化。下面的演示展示了类初始化的几个特性。
using System;
public class Calculator
{
static double pi = Math.PI;
double startAngle = 0;
public DateTime Created { get; } = DateTime.Now;
static Calculator()
{
Console.WriteLine("static Calculator()");
}
public Calculator()
{
Console.WriteLine("public Calculator()");
}
public Calculator(int val)
{
Console.WriteLine("public Calculator(int)");
}
}
代码清单 69
Calculator
有一个static
构造函数和两个实例构造函数重载。一个static
构造函数在对象的生存期内和第一个构造函数执行之前执行一次。下面的示例是一个具有相似成员的派生类。
using System;
public class ScientificCalculator : Calculator
{
static double pi = Math.PI;
double startAngle = 0;
static ScientificCalculator()
{
Console.WriteLine("static ScientificCalculator()");
}
public ScientificCalculator() : this(0)
{
Console.WriteLine("public ScientificCalculator()");
}
public ScientificCalculator(int val)
{
Console.WriteLine("public ScientificCalculator(int)");
}
public ScientificCalculator(int val, string word) : base(val)
{
Console.WriteLine("public ScientificCalculator(int, string)");
}
public double EndAngle { get; set; }
}
代码清单 70
ScientificCalculator
来源于Calculator
,除了this
和base
操作符外,构造函数相似。使用this
运算符调用带有匹配参数的构造函数重载。由于0
是int
,默认(无参数)构造函数首先调用ScientificCalculator(int val)
。base
运算符调用基类中的匹配构造函数,所以调用base(0)
先调用Calculator(int val)
。下面的代码清单是一个实例化这些类的程序。
using System;
class Program
{
static void Main()
{
var calc1 = new ScientificCalculator();
var calc2 = new ScientificCalculator(0, "x")
{
EndAngle = 360
};
Console.ReadKey();
}
}
代码清单 71
这是程序的输出:
static ScientificCalculator()
static Calculator()
public Calculator()
public ScientificCalculator(int)
public ScientificCalculator()
public Calculator(int)
public ScientificCalculator(int, string)
查看输出,您可以看到首先执行的是什么。以下是管理这些类实例化的规则:
- 静态构造函数在实例构造函数之前执行。
- 静态构造函数在程序的生命周期内执行一次。
- 基类构造函数在派生类构造函数之前执行。
this
运算符会首先执行与this
参数列表匹配的重载构造函数。- 基类默认构造函数执行,除非派生类使用基类显式选择不同的基类构造函数重载。
- 这在前面示例的输出中没有显示,但是静态字段在静态构造函数之前初始化,实例字段在实例构造函数之前初始化。
- 自动实现的属性初始化器,如
Created
,与字段同时初始化。 - 对象初始化语法中的属性最后执行,因为对象初始化语法相当于在实例化后通过实例变量填充属性。
| | 注意:在 Visual Studio 中,您可以在代码中设置断点,并使用即时窗口检查字段值。您可以尝试不同的对象初始化场景,以获得初始化序列的感觉。 |
在所有这些生命周期事件中,垃圾收集是最不可预测的。CLR 优化资源,并在需要时运行垃圾收集。这意味着引用类型对象的生存期是非确定性的。围绕 GC 的方式和原因有大量的理论讨论,但是我将把讨论限制在资源管理的实际考虑上。这包括关闭文件、数据库连接、操作系统句柄等。
为了释放资源,有一种模式通常被称为处置模式。它依赖于IDisposable
接口、管理对象处置状态的标志和析构函数。下面的代码有构造函数和Dispose
方法注释,暗示了一个场景,在这个场景中,类可以在其生命周期内记录操作,日志应该在实例化期间打开,当不再需要对象时关闭。
using System;
public class Calculator : IDisposable
{
static Calculator()
{
// Initialize log file stream.
}
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls.
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
// TODO: dispose managed state (managed objects).
// Close log file stream.
}
// TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
// TODO: set large fields to null.
disposedValue = true;
}
}
// TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
// ~Calculator() {
// // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
// Dispose(false);
// }
// This code added to correctly implement the disposable pattern.
public void Dispose()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose(true);
// TODO: uncomment the following line if the finalizer is overridden above.
// GC.SuppressFinalize(this);
}
#endregion
}
代码清单 72
#region
和#endregion
之间的代码由 VS 自动生成,要生成该代码,在编辑器中选择IDisposable
,会出现快速动作图标(一个灯泡)。打开快速操作菜单,选择Implement interface with Dispose pattern
。#region
和#endregion
让 VS 折叠代码,这样你就不必在编辑器中看到它。
Calculator
类实现IDisposable
接口,只是Dispose
方法。构造函数初始化您想要打开的资源,如文件句柄或数据库,如果没有注释,垃圾收集器调用析构函数~Calculator()
。Dispose()
方法用true
参数调用Dispose(bool)
,用false
参数调用Dispose(bool)
。这让Dispose(bool)
知道它应该清理属于 CLR 的托管资源还是属于操作系统的非托管资源。标志disposedValue
有助于防止物体被多次处理。
下面的示例显示了调用代码如何使用此类,并在不再需要时处置它。
ScientificCalculator calc3 = null;
try
{
calc3 = new ScientificCalculator();
// Do stuff.
}
finally
{
if (calc3 != null)
calc3.Dispose();
}
代码清单 73
这说明了try
- finally
存在的原因,保证资源可以关闭或处置。因为finally
块保证在try
块中的代码启动后执行,calc3
可以安全处置。虽然这很有效,但它过于冗长。下面的清单简化了代码。
using (var calc4 = new ScientificCalculator())
{
// Do stuff.
}
代码清单 74
using
语句接受实现IDisposable
的任何类型的参数。它负责在程序块完成执行后调用Dispose()
。幕后的逻辑和之前的try
- finally
街区差不多。
C# 支持面向对象编程。对于继承,类有单个继承,接口有多个继承,结构只能继承接口。对不应该独立的类使用抽象类,但是为派生类提供接口和结构。当您没有实现、需要值类型(struct)多态性或需要实现多个接口时,请使用接口。我还讨论了结构,以及它们是如何理想地适用于按值复制导致性能提高和值类型语义有意义的情况。与您需要公开的接口不同,使用封装来隐藏您不希望其他开发人员在他们的代码中使用的内部工作。多态性是一个强大的概念,它允许您编写一个算法,为每个实例编写相同的代码,但允许每个实例随特定于实例运行时类型的实现而变化。注意对象实例化的顺序,以确保您的类型正确初始化。如果需要处置一个类型,用处置模式使该类型实现IDisposable
。您可以使用using
语句来简化类型的实例化和安全清理。