4. 类、结构体、接口
4.1. 示例对象
4.1.1. 概述
现在,我们将通过实例来探讨面向对象编程。对象是一个实体,它包含定义其状态的数据(称为字段、属性等)以及函数(称为方法)。对象是根据称为类的模型创建的:
public class C1{
Type1 p1 ; // field p1
Type2 p2 ; // p2 field
…
Type3 m3(… ) { // m3 method
…
}
Type4 m4(… ) { // m4 method
…
}
…
}
根据上文的类 C1,你可以创建多个对象 O1、O2、……它们都将拥有字段 p1、p2、……以及方法 m3、m4、……但它们的字段 pi 值各不相同,各自处于不同的状态。如果 o1 是类型为 C1 的对象,则 o1.p1 表示 o1 的属性 p1,o1.m1 表示 o1 的方法 m1。
让我们考虑一个最初的对象模型:Person。
4.1.2. 创建一个 C# 项目
在之前的示例中,我们只有一个 Program.cs 文件。从现在开始,我们可以在一个项目中包含多个源文件。下面我们将向您展示具体方法。
![]() |
在 [1] 处,创建一个新项目。在 [2] 处,选择“控制台应用程序”。在 [3] 处,保留默认值。在 [4] 处,确认。在 [5] 处,生成的项目。Program.cs 的内容如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication1 {
class Program {
static void Main(string[] args) {
}
}
}
让我们保存生成的项目:
![]() |
在 [1] 中,选择要保存的选项。在 [2] 中,选择保存项目的文件夹。在 [3] 中,为项目命名。在 [5] 中,选择创建解决方案。解决方案是一组项目的集合。在 [4] 中,为解决方案命名。在 [6] 中,确认保存。
![]() |
在 [1] 中,显示已保存的项目。在 [2] 中,向项目中添加新元素。
![]() |
在 [1] 中,选择要添加类。在 [2] 中,输入类名。在 [3] 中,验证信息。在 [4] 中,项目 [01] 已生成一个新的源文件 Personne.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication1 {
class Personne {
}
}
将每个源文件的命名空间更改为 Chap2,并省去导入不必要命名空间的步骤:
using System;
namespace Chap2 {
class Personne {
}
}
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
}
}
}
4.1.3. Person 类的定义
源文件 [Personne.cs] 中 Person 类的定义如下:
using System;
namespace Chap2 {
public class Personne {
// attributes
private string prenom;
private string nom;
private int age;
// method
public void Initialise(string P, string N, int age) {
this.prenom = P;
this.nom = N;
this.age = age;
}
// method
public void Identifie() {
Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
}
}
}
这里给出了类的定义,即一种数据类型。当我们创建该类型的变量时,称其为对象或类实例。因此,类是构建对象的模板。
类的成员或字段可以是数据(属性)、方法(函数)或属性。属性是用于获取或设置对象属性值的特殊方法。这些字段可以伴随以下三个关键字之一:
私有字段仅可被类的内部方法访问 | |
公共字段可被任何方法访问,无论该方法是否定义在 | |
受保护字段(protected)仅可被该类的内部方法或派生对象访问(参见后文的继承概念)。 |
通常,类数据被声明为私有,而其方法和属性则被声明为公有。这意味着对象的使用者(即程序员)
- 将无法直接访问对象的私有数据
- 能够调用对象的 public 方法,特别是那些提供对其私有数据访问权限的方法。
C语言类声明的语法如下:
public class C{
private donnée ou méthode ou propriété privée;
public donnée ou méthode ou propriété publique;
protected donnée ou méthode ou propriété protégée;
}
私有、受保护和公有的属性声明顺序是任意的。
4.1.4. Initialize 方法
回到我们声明的 Person 类:
using System;
namespace Chap2 {
public class Personne {
// attributes
private string prenom;
private string nom;
private int age;
// method
public void Initialise(string p, string n, int age) {
this.prenom = p;
this.nom = n;
this.age = age;
}
// method
public void Identifie() {
Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
}
}
}
Initializes 的作用是什么?因为 name、first_name 和 age 是 Person 类的私有数据,请说明:
是无效的。我们需要通过一个公共方法来初始化一个 Person 类型的对象。这就是 Initializes 的作用。我们写:
编写 p1.Initialise 是合法的,因为 Initializes 面向公众。
4.1.5. new 运算符
指令序列
是错误的。该指令
将 p1 定义为 Person 类型对象的引用。该对象尚未存在,因此 p1 未被初始化。这就像写:
其中关键字 null 表示变量 p1 尚未引用任何对象。当你随后写
时,我们调用的是由 p1 引用的对象的 Initialise 方法。如果该对象尚未存在,编译器将报错。为确保 p1 引用了对象,请写成:
这将创建一个尚未初始化的 Person 类型对象:属性 name 和 first_name 作为 String 类型对象的引用将取值为 null,而 age 的值为 0。因此存在默认初始化。既然 p1 现在引用了一个对象,那么该对象的初始化语句
是有效的。
4.1.6. 关键字 this
让我们看看 initialize 的代码:
public void Initialise(string p, string n, int age) {
this.prenom = p;
this.nom = n;
this.age = age;
}
语句 this.prenom=p 表示当前对象(this)的姓名字段被赋值为 p。关键字 this 指代当前对象:即包含正在执行的方法的那个对象。我们如何得知这一点?让我们看看调用程序中由 p1 引用的对象的初始化情况:
这是对象 p1 的 Initialize 方法。当该方法引用 this 时,实际上指的就是对象 p1。Initialize 方法也可以写成如下形式:
public void Initialise(string p, string n, int age) {
prenom = p;
nom = n;
this.age = age;
}
当对象的方法引用该对象的属性 A 时,默认会使用 this.A。当标识符发生冲突时,必须显式使用 this.A。例如:
this.age=age;
其中 age 既指当前对象的属性,也是方法接收的 age 参数。此时必须通过 this.age 来明确该 age 是指哪个属性,从而消除歧义。
4.1.7. 一个测试程序
以下是一个简短的测试程序。它编写在源文件 [Program.cs] 中:
using System;
namespace Chap2 {
class P01 {
static void Main() {
Personne p1 = new Personne();
p1.Initialise("Jean", "Dupont", 30);
p1.Identifie();
}
}
}
在执行项目 [01] 之前,您可能需要指定要执行的源文件:
![]() |
在项目属性 [01] 中,待执行的类在 [1] 中指定。
执行完成后获得的结果如下:
4.1.8. 另一种方法 Initialise
让我们考虑 Person 类,并添加以下方法:
public void Initialise(Personne p) {
prenom = p.prenom;
nom = p.nom;
age = p.age;
}
现在我们有两个名为 Initializes 的方法:只要它们接受不同的参数,这便是合法的。此处的情况正是如此。参数现在是一个指向 Person 的引用 p。Person 的属性 p 随后被赋值给当前对象 (this)。请注意,尽管这些属性是私有的,但 Initializes 方法仍可直接访问对象属性 p。这一点始终成立:类 C 的对象 o1 总能访问同类 C 对象的属性。
以下是对新类 Person 的测试:
using System;
namespace Chap2 {
class Program {
static void Main() {
Personne p1 = new Personne();
p1.Initialise("Jean", "Dupont", 30);
p1.Identifie();
Personne p2 = new Personne();
p2.Initialise(p1);
p2.Identifie();
}
}
}
及其结果:
4.1.9. Person类的构造函数
构造函数是一种以类名命名的方法,在创建对象时被调用。它通常用于初始化对象。它可以接受参数,但不返回任何结果。其原型或定义前不带任何类型(甚至不带 void)。
如果一个 C 类有一个接受 n 个参数 argi 的构造函数,则该类对象的声明和初始化可以采用以下形式:
或者
当类 C 具有一个或多个构造函数时,必须使用其中一个构造函数来创建该类的对象。如果类 C 没有构造函数,则它有一个默认构造函数,即无参数的构造函数:public C()。此时,对象的属性将使用默认值进行初始化。这就是之前程序中发生的情况,其中:
现在,让我们为 Person 类创建两个构造函数:
using System;
namespace Chap2 {
public class Personne {
// attributes
private string prenom;
private string nom;
private int age;
// manufacturers
public Personne(String p, String n, int age) {
Initialise(p, n, age);
}
public Personne(Personne P) {
Initialise(P);
}
// method
public void Initialise(string p, string n, int age) {
...
}
public void Initialise(Personne p) {
...
}
// method
public void Identifie() {
Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
}
}
}
我们的两个构造函数仅使用了之前学过的 Initializes。请记住,当构造函数使用 Initialise(p) 这样的语法时,编译器会将其转换为 this.Initialise(p)。在构造函数中,Initializes 会被调用以对 this 所引用的对象(即当前对象,正在构建的对象)进行初始化。
以下是一个简短的测试程序:
using System;
namespace Chap2 {
class Program {
static void Main() {
Personne p1 = new Personne("Jean", "Dupont", 30);
p1.Identifie();
Personne p2 = new Personne(p1);
p2.Identifie();
}
}
}
以及所得结果:
4.1.10. 对象引用
我们始终使用同一个 Person 对象。测试程序变为:
using System;
namespace Chap2 {
class Program2 {
static void Main() {
// p1
Personne p1 = new Personne("Jean", "Dupont", 30);
Console.Write("p1="); p1.Identifie();
// p2 references the same object as p1
Personne p2 = p1;
Console.Write("p2="); p2.Identifie();
// p3 references an object that will be a copy of the object referenced by p1
Personne p3 = new Personne(p1);
Console.Write("p3="); p3.Identifie();
// change the state of the object referenced by p1
p1.Initialise("Micheline", "Benoît", 67);
Console.Write("p1="); p1.Identifie();
// as p2=p1, the object referenced by p2 must have changed state
Console.Write("p2="); p2.Identifie();
// as p3 does not reference the same object as p1, the object referenced by p3 must not have changed
Console.Write("p3="); p3.Identifie();
}
}
}
结果如下:
在声明变量 p1 时
p1 是一个 Personne 对象的引用(即 "John", "Smith", 30),但并非对象本身。在 C 语言中,我们会说它是一个指针,即所创建对象的地址。如果你接着写:
这并非指 Person("John", "Smith", 30) 被修改,而是指引用 p1 的值发生了变化。如果对象 Person("John", "Smith", 30) 不再被任何其他变量引用,它将被“丢失”。
当我们写:
这会初始化指针 p2:它“指向”(即指定)与指针 p1 相同的对象。因此,如果我们修改了 p1 “指向”(或引用的)对象,也会同时修改 p2 引用的对象。
当我们写:
会创建一个新的 Person 对象。这个新对象将由 p3 引用。如果你修改了 p1 “指向”(或引用的)对象,那么 p3 引用的对象也会随之改变。这就是结果所显示的情况。
4.1.11. 传递对象引用参数
在上一章中,我们探讨了当函数参数表示由 .NET 结构体表示的简单 C# 类型时,参数是如何传递的。现在让我们看看当参数是 : 时会发生什么:
using System;
using System.Text;
namespace Chap1 {
class P12 {
public static void Main() {
// example 4
StringBuilder sb0 = new StringBuilder("essai0"), sb1 = new StringBuilder("essai1"), sb2 = new StringBuilder("essai2"), sb3;
Console.WriteLine("Dans fonction appelante avant appel : sb0={0}, sb1={1}, sb2={2}", sb0,sb1, sb2);
ChangeStringBuilder(sb0, sb1, ref sb2, out sb3);
Console.WriteLine("Dans fonction appelante après appel : sb0={0}, sb1={1}, sb2={2}, sb3={3}", sb0, sb1, sb2, sb3);
}
private static void ChangeStringBuilder(StringBuilder sbf0, StringBuilder sbf1, ref StringBuilder sbf2, out StringBuilder sbf3) {
Console.WriteLine("Début fonction appelée : sbf0={0}, sbf1={1}, sbf2={2}", sbf0,sbf1, sbf2);
sbf0.Append("*****");
sbf1 = new StringBuilder("essai1*****");
sbf2 = new StringBuilder("essai2*****");
sbf3 = new StringBuilder("essai3*****");
Console.WriteLine("Fin fonction appelée : sbf0={0}, sbf1={1}, sbf2={2}, sbf3={3}", sbf0, sbf1, sbf2, sbf3);
}
}
}
- 第 8 行:定义了 3 个 StringBuilder。StringBuilder 对象与 string 对象相似。处理 string 对象时,会返回一个新的 string 对象。因此,在代码序列中:
第 1 行创建了一个字符串,s 是该字符串的指针。第 2 行中的 s.ToUpperCase() 在内存中创建了另一个字符串对象。因此,在第 1 行和第 2 行之间,s 的值发生了变化(它现在指向了新的对象)。StringBuilder 类允许你在不创建第二个对象的情况下修改字符串。这就是上文给出的示例:
- 第 8 行:4 个引用 [sb0, sb1, sb2, sb3] 指向 StringBuilder 类型的对象
- 第 10 行:将这些对象以不同模式传递给 ChangeStringBuilder 方法:sb0、sb1 采用默认模式,sb2 带 ref 关键字,sb3 带 out 关键字。
- 第 15-22 行:一个具有形式参数 [sbf0, sbf1, sbf2, sbf3] 的方法。形式参数 sbf1 与工作参数 sbi 之间的关系如下:
- sbf0 和 sb0 在方法开始时是两个指向同一对象的独立引用(按地址传递)
- sbf1 与 sb1 亦同
- sbf2 和 sb2 在方法开始时,是指向同一对象的同一引用(关键字 ref)
- sbf3 和 sb3 在方法执行后,是指向同一对象的同一引用(关键字 out)
结果如下:
说明:
- sb0 和 sbf0 是指向同一对象的两个独立引用。该对象已通过 sbf0 被修改——第 3 行。此修改可通过 sb0 查看——第 4 行。
- sb1 和 sbf1 是指向同一对象的两个不同引用。sbf1 在方法中被修改,现在指向一个新对象——第 3 行。这不会改变 sb1 的值,它继续指向同一对象——第 4 行。
- sb2 和 sbf2 是指向同一对象的同一引用。sbf2 在方法中被修改,现在指向一个新对象——第 3 行。由于 sbf2 和 sb2 是同一个实体,sb2 的值也被修改,sb2 指向与 sbf2 相同的对象——第 3 和 4 行。
- 在调用该方法之前,sb3 毫无意义。方法执行后,sb3 接收到了 sbf3 的值。因此,我们拥有两个指向同一对象的引用——第 3 行和第 4 行
4.1.12. 临时对象
在表达式中,我们可以显式调用对象的构造函数:对象会被创建,但我们无法访问它(例如对其进行修改)。这个临时对象是为了求解该表达式而创建的,求解完成后即被丢弃。它所占用的内存空间随后会由一个名为“垃圾回收器”的程序自动回收,该程序的作用是回收那些不再被程序数据引用的对象所占用的内存空间。
请看以下这个新的测试程序:
using System;
namespace Chap2 {
class Program {
static void Main() {
new Personne(new Personne("Jean", "Dupont", 30)).Identifie();
}
}
}
并修改 Person 类的构造函数以显示一条消息:
// manufacturers
public Personne(String p, String n, int age) {
Console.WriteLine("Constructeur Personne(string, string, int)");
Initialise(p, n, age);
}
public Personne(Personne P) {
Console.Out.WriteLine("Constructeur Personne(Personne)");
Initialise(P);
}
我们得到以下结果:
展示了两个临时对象的连续构造过程。
4.1.13. 用于读写私有属性的方法
我们在 Person 类中添加了用于读取或修改对象属性状态的方法:
using System;
namespace Chap2 {
public class Personne {
// attributes
private string prenom;
private string nom;
private int age;
// manufacturers
public Personne(String p, String n, int age) {
Console.WriteLine("Constructeur Personne(string, string, int)");
Initialise(p, n, age);
}
public Personne(Personne p) {
Console.Out.WriteLine("Constructeur Personne(Personne)");
Initialise(p);
}
// method
public void Initialise(string p, string n, int age) {
this.prenom = p;
this.nom = n;
this.age = age;
}
public void Initialise(Personne p) {
prenom = p.prenom;
nom = p.nom;
age = p.age;
}
// accessors
public String GetPrenom() {
return prenom;
}
public String GetNom() {
return nom;
}
public int GetAge() {
return age;
}
//modifiers
public void SetPrenom(String P) {
this.prenom = P;
}
public void SetNom(String N) {
this.nom = N;
}
public void SetAge(int age) {
this.age = age;
}
// method
public void Identifie() {
Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
}
}
}
我们使用以下程序测试新类:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Personne p = new Personne("Jean", "Michelin", 34);
Console.Out.WriteLine("p=(" + p.GetPrenom() + "," + p.GetNom() + "," + p.GetAge() + ")");
p.SetAge(56);
Console.Out.WriteLine("p=(" + p.GetPrenom() + "," + p.GetNom() + "," + p.GetAge() + ")");
}
}
}
我们得到的结果是:
4.1.14. 属性
访问类属性的另一种方法是创建属性。这些属性允许我们像操作公共属性一样操作私有属性。
请看 P 类,其中之前的访问器和修饰符已被读写属性所取代:
using System;
namespace Chap2 {
public class Personne {
// attributes
private string prenom;
private string nom;
private int age;
// manufacturers
public Personne(String p, String n, int age) {
Initialise(p, n, age);
}
public Personne(Personne p) {
Initialise(p);
}
// method
public void Initialise(string p, string n, int age) {
this.prenom = p;
this.nom = n;
this.age = age;
}
public void Initialise(Personne p) {
prenom = p.prenom;
nom = p.nom;
age = p.age;
}
// properties
public string Prenom {
get { return prenom; }
set {
// valid first name?
if (value == null || value.Trim().Length == 0) {
throw new Exception("prénom (" + value + ") invalide");
} else {
prenom = value;
}
}//if
}//first name
public string Nom {
get { return nom; }
set {
// valid name?
if (value == null || value.Trim().Length == 0) {
throw new Exception("nom (" + value + ") invalide");
} else { nom = value; }
}//if
}//name
public int Age {
get { return age; }
set {
// valid age?
if (value >= 0) {
age = value;
} else
throw new Exception("âge (" + value + ") invalide");
}//if
}//age
// method
public void Identifie() {
Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
}
}
}
属性允许您读取(get)或设置(set)某个属性的值。属性的声明如下:
其中 Type 必须是该属性所管理的属性的类型。它可以包含两个名为 get 和 set 的方法。get 方法通常负责渲染其所管理的属性的值(当然也可以渲染其他内容,没有任何限制)。 set 方法接收一个名为 value 的参数,通常将其赋值给所管理的属性。它可利用此机制检查接收到的值是否有效,若值无效,则抛出异常。这就是 ici 的作用。
这些 get 和 set 方法是如何被调用的?请看以下测试程序:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Personne p = new Personne("Jean", "Michelin", 34);
Console.Out.WriteLine("p=(" + p.Prenom + "," + p.Nom + "," + p.Age + ")");
p.Age = 56;
Console.Out.WriteLine("p=(" + p.Prenom + "," + p.Nom + "," + p.Age + ")");
try {
p.Age = -4;
} catch (Exception ex) {
Console.Error.WriteLine(ex.Message);
}//try-catch
}
}
}
在
Console.Out.WriteLine("p=(" + p.Prenom + "," + p.Nom + "," + p.Age + ")");
我们需要获取人物 p 的声望、名望和年龄的数值。这是这些属性的 get 方法,调用该方法后,它会返回其管理的属性的数值。
在
中,我们希望设置 Age 属性的值。这是随后被调用的 set 方法。它将接收 56 作为参数值。
如果类 C 的属性 P 仅定义了 get 方法,则该属性被称为只读属性。如果 c 是类 C 的对象,那么操作 c.P=valeur 将被编译器拒绝。
执行上述测试程序将得到以下结果:
属性使我们能够像操作公共属性一样操作私有属性。属性的另一个特点是,它们可以与构造函数结合使用,语法如下:
该语法等同于以下代码:
属性的顺序无关紧要。以下是一个示例。
Person 类添加了一个无参构造函数:
public Personne() {
}
该构造函数不会初始化对象的成员。这被称为默认构造函数。当类未定义构造函数时,会使用该构造函数。
以下代码使用上述语法创建并初始化(第 6 行)一个新的 Person 对象:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Personne p2 = new Personne { Age = 7, Prenom = "Arthur", Nom = "Martin" };
Console.WriteLine("p2=({0},{1},{2})", p2.Prenom, p2.Nom, p2.Age);
}
}
}
上文第 6 行,这里使用了无参构造函数 Person()。在这种特定情况下,我们也可以这样写:
Personne p2 = new Personne() { Age = 7, Prenom = "Arthur", Nom = "Martin" };
但在这种语法中,无参构造函数 Person() 的圆括号并非必需。
结果如下:
在许多情况下,get 和 set 属性只是读取和写入一个私有字段,而不会进行任何进一步的处理。在这种情况下,我们可以使用如下声明的自动属性:
与该属性关联的私有字段无需显式声明,它将由编译器自动生成。该字段只能通过其属性进行访问。因此,无需编写:
private string prenom;
...
// propriété associée
public string Prenom {
get { return prenom; }
set {
// prénom valide ?
if (value == null || value.Trim().Length == 0) {
throw new Exception("prénom (" + value + ") invalide");
} else {
prenom = value;
}
}//if
}//prenom
我们可以写成:
而无需先声明名为 firstname 的私有字段。前两个属性之间的区别在于,第一个会在设置时验证名字的有效性,而第二个则不会。
使用自动属性 First name 来声明一个名为 First name 的公共字段:
我们想知道这两种声明方式是否有区别。不建议将类字段声明为 public。这违背了封装对象状态的概念,对象的状态必须是私有的,并通过 public 方法进行暴露。
如果自动属性被声明为虚拟的,则可以在子类中对其进行重定义:
class Class1 {
public virtual string Prop { get; set; }
}
class Class2 : Class1 {
public override string Prop { get { return base.Prop; } set {... } }
}
在上面的第 2 行中,子类 Class2 可以在 set 语句中添加代码,用于验证赋给父类 Class1 的自动属性 base.Prop 的值的有效性。
4.1.15. 类方法和属性
假设我们想统计应用程序中创建的 Person 对象的数量。你可以自己管理一个计数器,但这样有可能会遗漏那些零零散散创建的临时对象。更稳妥的做法是在 Person 类的构造函数中加入一条增量计数器的语句。 问题在于需要传递该计数器的引用以便构造函数能对其进行递增:必须传递一个新参数。或者,也可以将计数器包含在类定义中。由于它是类本身的属性,而非该类的特定实例的属性,因此我们需要使用 static 关键字以不同的方式进行声明:
private static long nbPersonnes;
要引用它,我们写 Personne.nbPersonnes 来表明它是 Person 类本身的属性。这里,我们创建了一个私有属性,该属性在类外部无法被直接访问。 因此,我们需要创建一个公共属性来访问类属性 nbPersonnes。为了返回 nbPersonnes 的值,该属性的 get 方法不需要特定的 Person 对象:实际上,nbPersonnes 是整个类的属性。因此,我们需要将该属性也声明为 static:
public static long NbPersonnes {
get { return nbPersonnes; }
}
该方法在外部调用时将采用 Personne.NbPersonnes 的语法。以下是一个示例。
Personne 类将变为如下形式:
using System;
namespace Chap2 {
public class Personne {
// class attributes
private static long nbPersonnes;
public static long NbPersonnes {
get { return nbPersonnes; }
}
// instance attributes
private string prenom;
private string nom;
private int age;
// manufacturers
public Personne(String p, String n, int age) {
Initialise(p, n, age);
nbPersonnes++;
}
public Personne(Personne p) {
Initialise(p);
nbPersonnes++;
}
...
}
在第 20 行和第 24 行,构建器递增了第 7 行中的静态字段。
使用以下程序:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Personne p1 = new Personne("Jean", "Dupont", 30);
Personne p2 = new Personne(p1);
new Personne(p1);
Console.WriteLine("Nombre de personnes créées : " + Personne.NbPersonnes);
}
}
}
我们得到以下结果:
4.1.16. 人员表
对象与其他数据一样,因此可以将多个对象组合成一个表:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
// a table of people
Personne[] amis = new Personne[3];
amis[0] = new Personne("Jean", "Dupont", 30);
amis[1] = new Personne("Sylvie", "Vartan", 52);
amis[2] = new Personne("Neil", "Armstrong", 66);
// display
foreach (Personne ami in amis) {
ami.Identifie();
}
}
}
}
- 第 7 行:创建了一个包含 3 个 Person 类型元素的数组。这 3 个元素在此处被初始化为 null 值,也就是说它们不引用任何对象。同样,这是一种对“对象数组”一词的误用,实际上它只是一个对象引用的数组。 对象数组的创建(其本身也是一个对象,因为使用了 new 关键字)并不会创建与其元素同类型的任何对象。
- 第 8-10 行:创建 3 个 Person 类型的对象
- 第12-14行:显示friends表的内容
我们得到以下结果:
4.2. 以身作则的传承
4.2.1. 概述
我们引入“继承”的概念。继承的目的是为了“定制”现有类以满足我们的需求。假设我们要创建一个 Enstructor 类:教师是一个特殊的人。他拥有其他人所没有的属性,例如他所教授的学科。但他同时也拥有任何其他人的属性:名字、姓氏和年龄。 因此,教师完全属于 Person 类,但拥有额外的属性。与其从头开始编写 Enstructor 类,我们更愿意利用 Person 类中的知识,并根据教师的特殊性质进行调整。正是继承这一概念使这一切成为可能。
为了表示 Teacher 类继承了 Person 的属性,我们编写如下代码:
Person 被称为父类,而 Enseignant 被称为派生类(或子类)。一个 Enseignant 对象具有 Person 对象的所有特性:它拥有相同的属性和方法。父类的这些属性和方法在子类的定义中不会重复:我们只需列出子类新增的属性和方法:
假设 Person 的定义如下:
using System;
namespace Chap2 {
public class Personne {
// class attributes
private static long nbPersonnes;
public static long NbPersonnes {
get { return nbPersonnes; }
}
// instance attributes
private string prenom;
private string nom;
private int age;
// manufacturers
public Personne(String prenom, String nom, int age) {
Nom = nom;
Prenom = prenom;
Age = age;
nbPersonnes++;
Console.WriteLine("Constructeur Personne(string, string, int)");
}
public Personne(Personne p) {
Nom = p.Nom;
Prenom = p.Prenom;
Age = p.Age;
nbPersonnes++;
Console.WriteLine("Constructeur Personne(Personne)");
}
// properties
public string Prenom {
get { return prenom; }
set {
// valid first name?
if (value == null || value.Trim().Length == 0) {
throw new Exception("prénom (" + value + ") invalide");
} else {
prenom = value;
}
}//if
}//first name
public string Nom {
get { return nom; }
set {
// valid name?
if (value == null || value.Trim().Length == 0) {
throw new Exception("nom (" + value + ") invalide");
} else { nom = value; }
}//if
}//name
public int Age {
get { return age; }
set {
// valid age?
if (value >= 0) {
age = value;
} else
throw new Exception("âge (" + value + ") invalide");
}//if
}//age
// property
public string Identite {
get { return String.Format("[{0}, {1}, {2}]", prenom, nom, age);}
}
}
}
Identifies 方法已被用于标识该人的 Identity 所取代。我们创建一个继承自 Person 的 Enstructor:
using System;
namespace Chap2 {
class Enseignant : Personne {
// attributes
private int section;
// manufacturer
public Enseignant(string prenom, string nom, int age, int section)
: base(prenom, nom, age) {
// the section is saved using the Section property
Section = section;
// follow-up
Console.WriteLine("Construction Enseignant(string, string, int, int)");
}//manufacturer
// property Section
public int Section {
get { return section; }
set { section = value; }
}// Section
}
}
Teacher 类在 Person 类的基础上增加了以下方法和属性:
- 第 4 行:Teacher 类继承自 Person 类
- 第 6 行:一个属性 Section,表示教师在教师团队中所隶属的分组(大致每门学科一个分组)。可以通过公共属性 Section(第 18-21 行)访问此私有属性
- 第 9 行:一个用于初始化所有教师属性的新构造函数
4.2.2. 创建 Teacher 对象
女生班级并未继承其父类的构造函数,因此必须定义自己的构造函数。Enstructor 的构造函数如下:
// manufacturer
public Enseignant(string prenom, string nom, int age, int section)
: base(prenom, nom, age) {
// section is memorized
Section = section;
// follow-up
Console.WriteLine("Construction enseignant(string, string, int, int)");
}//manufacturer
声明
public Enseignant(string prenom, string nom, int age, int section)
: base(prenom, nom, age) {
声明构造函数接收四个参数:first name、name、age、section,并向其基类(此处为 Person 类)传递另外三个参数(firstname、lastname、age)。我们知道该类有一个构造函数 Person(string, string, int),它将根据传入的参数(firstname、lastname、age)创建一个 Person 对象。当基类的构造完成后,Teacher 的构造将继续执行,并执行构建器的主体代码:
// on mémorise la section
Section = section;
请注意,等号左侧并非所用对象的 section 属性,而是与其关联的 Section 对象。这使得构造函数能够利用该方法可能执行的任何有效性检查。这样就避免了在构造函数和属性中分别放置这些检查的必要。
简而言之,派生类的构造函数:
- 将自身构建所需的参数传递给基类
- 使用其余参数来初始化自身的属性
我们可能更倾向于这样写:
// constructeur
public Enseignant(string prenom, string nom, int age, int section){
this.prenom=prenom;
this.nom=nom;
this.age=age;
this.section=section;
}
这是行不通的。Person 类将其三个字段 first_name、name 和 age 声明为 private(私有)。只有同类的对象才能直接访问这些字段。所有其他对象,包括 ici 这样的子类对象,都必须使用 public 方法来访问它们。如果 Person 类将这三个字段声明为 protected(受保护),情况就会不同:这允许派生类直接访问这三个字段。 因此,在本例中,使用父类的构造函数是正确的解决方案,这也是通常的做法:在构造子类对象时,我们首先调用父类的构造函数,然后完成子类对象特有的初始化(在本例中为 section)。
让我们尝试编写第一个测试程序 [Program.cs]:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Console.WriteLine(new Enseignant("Jean", "Dupont", 30, 27).Identite);
}
}
}
该程序仅创建了一个 Enstructor 对象(new)并对其进行标识。Enstructor 类本身没有 Identite 方法,但其父类有一个同名的公共方法:通过继承,该方法也成为了 Enstructor 类的公共方法。
整个项目如下:
![]() |
结果如下:
我们可以看到:
- Person 对象(第 1 行)在 Enstructor 对象(第 2 行)之前被创建
- 得到的标识是 Person 对象的标识
4.2.3. 重定义方法或属性
在前面的示例中,我们获得了 Person 部分的标识,但缺少 Enstructor(该部分)的一些类特定信息。这促使我们编写一个属性来标识教师:
using System;
namespace Chap2 {
class Enseignant : Personne {
// attributes
private int section;
// manufacturer
public Enseignant(string prenom, string nom, int age, int section)
: base(prenom, nom, age) {
// the section is saved using the Section property
Section = section;
// follow-up
Console.WriteLine("Construction Enseignant(string, string, int, int)");
}//manufacturer
// property Section
public int Section {
get { return section; }
set { section = value; }
}// section
// property Identity
public new string Identite {
get { return String.Format("Enseignant[{0},{1}]", base.Identite, Section); }
}
}
}
第 24-26 行,Enstructor 类的 Identity 属性基于其父类的 Identity(baseidentity)(第 25 行)来显示其“Person”,然后补充 Enstructor 特有的 Section。请注意 Identity 属性的声明:
public new string Identite{
设有一个名为 E 的教师对象。该对象包含一个 Person:
![]() |
属性 Identity 既定义在 Teacher 类中,也定义在其父类 Person 中。在 Teacher 类中,属性 Identity 必须在前面加上关键字 new,以表明正在为 Teacher 类重新定义一个名为 Identity 的新属性。
public new string Identite{
现在,*Teacher 类拥有两个名为 *Identite 的属性:
- 一个是从父类 Person 继承的
- 以及它自己的
如果 E 是 Enstructor 类,则 E.Identite 表示 Enstructor 类的 Identite 属性。我们说该属性 Identite 重新定义或隐藏了父类的 Identite 属性。一般而言,如果 O 是对象,M 是方法,要执行 O.M,系统将按以下顺序查找方法 M:
- 在 O 类中
- 在其父类中(如果存在)
- 在其父类的父类中(如果存在)
- 以此类推。
继承允许你在子类中重写父类中同名的方法/属性。这使你能根据自身需求调整子类。结合多态性(我们稍后将探讨),重写方法/属性是继承的主要优势。
考虑与上文相同的测试程序:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Console.WriteLine(new Enseignant("Jean", "Dupont", 30, 27).Identite);
}
}
}
此次获得的结果如下:
4.2.4. 多态性
考虑一个类继承序列:C0 ← C1 ← C2 ← … ← Cn
其中 Ci ← Cj 表示 Cj 由 Ci 派生而来。这意味着 Cj 不仅具备 Ci 的所有特征,还拥有其他特征。设对象 Oi 的类型为 Ci。以下写法是合法的:
事实上,根据继承关系,类 Cj 不仅具备类 Ci 的所有特征,还拥有其他特征。因此,类型为 Cj 的对象 Oj 包含一个类型为 Ci 的对象。该运算
表示 Oi 是对象 Oj 中所包含的 Ci 类型对象的引用。
变量 Oi 不仅可以引用类 Ci 的对象,实际上还可以引用任何从 Ci 派生的对象,这一特性被称为多态性:即变量能够引用不同类型的对象。
让我们通过一个例子来考虑以下与类无关的函数(静态函数):
我们不妨这样写
那个
在后一种情况下,形式参数 p(类型为 Person)的静态方法 Affiche 将接收类型为 Enstructor 的值。由于 Teacher 类型从 Person 派生而来,因此这是合法的。
4.2.5. 重定义与多态性
让我们完善我们的 Affiche 方法:
public static void Affiche(Personne p) {
// displays identity of p
Console.WriteLine(p.Identite);
}//poster
属性 p.Identite 返回一个字符串,用于标识对象 Person p。如果传递给 Poster 的参数是类型为 Teacher 的对象,那么在上一个示例中会发生什么:
Enseignant e = new Enseignant(...);
Affiche(e);
让我们来看以下示例:
using System;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// a teacher
Enseignant e = new Enseignant("Lucile", "Dumas", 56, 61);
Affiche(e);
// a person
Personne p = new Personne("Jean", "Dupont", 30);
Affiche(p);
}
// poster
public static void Affiche(Personne p) {
// displays identity of p
Console.WriteLine(p.Identite);
}//poster
}
}
结果如下:
运行结果表明,p.Identite(第17行)首先执行了Person的Identity(第7行),即Teacher e中包含的人,然后(第10行)执行了Person p本身。它并未根据实际作为参数传递给Poster的对象进行适配。 我们本希望获取教师 e 的完整身份。这需要使用 p.Identite 这种表示法,即引用 p 实际指向的对象的 Identity 属性,而非 p 实际指向的对象的“Person”部分的 Identity 属性。
通过在基类 Person 中将 Identity 声明为虚拟属性(virtual),即可获得此结果:
public virtual string Identite {
get { return String.Format("[{0}, {1}, {2}]", prenom, nom, age); }
}
关键字 virtual 将 Identity 定义为虚拟属性。该关键字也可应用于方法。子类若要重定义虚拟属性或方法,必须使用关键字 override 而不是 new 来修饰其重定义的属性/方法。因此,在 Teacher 类中,属性 Identity 被重定义如下:
public override string Identite {
get { return String.Format("Enseignant[{0},{1}]", base.Identite, Section); }
}
上述程序随后产生以下结果:
这次,在第 3 行,我们得到了教师的完整身份信息。现在,让我们重定义一个方法,而不是一个属性。类对象(C# 中 System.Object 的别名)是所有 C# 类的“母”类。因此,当你编写:
我们实际上隐含地写下了:
System.Object 类定义了一个名为 ToString 的虚拟方法:
![]() |
ToString 方法返回对象所属类的名称,如下例所示:
using System;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// a teacher
Console.WriteLine(new Enseignant("Lucile", "Dumas", 56, 61).ToString());
// a person
Console.WriteLine(new Personne("Jean", "Dupont", 30).ToString());
}
}
}
结果如下:
请注意,尽管我们在 Person 和 Teacher 类中并未重写 ToString 方法,但我们可以看到,ToString 方法能够显示对象的实际类名。
让我们重写 Person 和 Teacher 类中的 ToString 方法:
// méthode ToString
public override string ToString() {
return Identite;
}
这两个类的定义是相同的。请看以下测试程序:
using System;
namespace Chap2 {
class Program3 {
public static void Main() {
// a teacher
Enseignant e = new Enseignant("Lucile", "Dumas", 56, 61);
Affiche(e);
// a person
Personne p = new Personne("Jean", "Dupont", 30);
Affiche(p);
}
// poster
public static void Affiche(Personne p) {
// displays identity of p
Console.WriteLine(p);
}//Poster
}
}
让我们来看一下方法 Poster,其参数是一个人 p。第 15 行,Console 的 WriteLine 类没有接受 Person 类型参数的变体。在各种 WriteLine 方法中,有一个接受 Object 的。编译器将使用此方法 WriteLine(Object o),因为该签名意味着 o 可以是 Object 或其派生类型。 由于 Object 是所有类的基类,因此任何对象都可以作为参数传递给 WriteLine 方法,包括 Person 或 Teacher 类型的对象。WriteLine(Object o) 方法会在输出流 Out 中写入 o.ToString()。由于 ToString 方法是虚拟的,如果对象 o(类型为 Object 或其派生类)重定义了 ToString 方法,则将使用后者。这里 Person 和 Teacher 类正是如此。
性能测试结果如下:
4.3. 为类重新定义运算符的含义
4.3.1. 简介
考虑以下指令
,其中 op1 和 op2 是两个操作数。可以重新定义 + 运算符的含义。如果操作数 op1 是类 C1 的对象,则必须在 C1 中定义一个具有以下签名的静态方法:
当编译器遇到
然后,它将其转换为 C1.operator+(op1,op2)。该运算符生成的类型非常重要。 考虑运算 op1+op2+op3。编译器将其转换为 (op1+op2)+op3。设 res12 为 op1+op2 的结果。接下来的运算是 res12+op3。如果 res12 的类型是 C1,它也将被转换为 C1.operator+(res12,op3)。这使得运算链成为可能。
具有单个操作数的单目运算符也可以被重定义。例如,如果 op1 是类型为 C1 的对象,则运算 op1++ 可以通过 C1 的静态方法重定义:
上述内容对大多数运算符均适用,但有少数例外:
- 运算符 == 和 != 必须同时重定义
- 运算符 &&、||、[]、()、+=、-= 等不能被重新定义
4.3.2. 一个示例
我们创建了一个从 ArrayList 派生的 ListeDePersonnes 类。该类实现了一个动态列表,将在下一章中介绍。我们仅使用该类的以下元素:
- L.Add(Object o) 方法,用于将对象 o 添加到 L 中。此处对象 o 将是 Person 类型的对象。
- 属性 L.Count,用于返回列表 L 中的元素个数
- L[i] 语法,用于获取列表 L 的第 i 个元素
ListeDePersonnes 类将继承 ArrayList 的所有属性、方法和属性。其定义如下:
using System;
using System.Collections;
using System.Text;
namespace Chap2 {
class ListeDePersonnes : ArrayList{
// redefine + operator, to add a person to the list
public static ListeDePersonnes operator +(ListeDePersonnes l, Personne p) {
// person p is added to the ListeDePersonnes l
l.Add(p);
// we return the ListeDePersonnes l
return l;
}// operator +
// ToString
public override string ToString() {
// render (él1, él2, ..., éln)
// opening parenthesis
StringBuilder listeToString = new StringBuilder("(");
// browse the list of people (this)
for (int i = 0; i < Count - 1; i++) {
listeToString.Append(this[i]).Append(",");
}//for
// last element
if (Count != 0) {
listeToString.Append(this[Count-1]);
}
// closing parenthesis
listeToString.Append(")");
// you must return a string
return listeToString.ToString();
}//ToString
}
}
- 第 6 行:ListeDePersonnes 类继承自 ArrayList
- 第 8-13 行:定义运算符 + 用于 l + p 运算,其中 l 的类型为 ListeDePersonnes,p 的类型为 Person 或其派生类。
- 第 10 行:将人 p 添加到列表 l 中。此处使用了父类 ArrayList 的 Add 方法。
- 第12行:对列表l的引用进行了处理,以便能够连链+运算符,例如l + p1 + p2。运算l+p1+p2将被解释(按运算符优先级)为(l+p1)+p2。 运算 l+p1 使引用 l 保持不变。运算 (l+p1)+p2 随后变为 l+p2,这将人 p2 添加到人列表 l 中。
- 第 16 行:我们重新定义 ToString 方法,使其将人员列表显示为 (person1, person2, ...) 的形式,其中 personi 本身是 Person 类 ToString 方法的返回结果。
- 第 19 行:我们使用了一个 StringBuilder 类型的对象。当需要进行大量字符串操作(此处为追加操作)时,该类比 String 更合适。实际上,对字符串进行的每次操作都会创建一个新的字符串对象,而对 StringBuilder 进行相同的操作则会修改现有对象,但不会创建新对象。我们使用 Append 方法来连接字符串。
- 第 21 行:遍历人员列表中的元素。此处该列表通过 this 表示。this 是指当前正在调用 ToString 方法的对象。Count 属性是父类 ArrayList 的属性。
- 第 22 行:当前列表中的第 i 个元素可通过 this[i] 表示法访问。同样,this 也是 ArrayList 的属性。由于涉及字符串操作,将使用 this[i].ToString()。由于 ToString 是虚拟方法,因此将调用 this 对象(类型为 Person 或其派生类)的 ToString 方法。
- 第 31 行:我们需要返回一个字符串类型的对象(第 16 行)。StringBuilder 类有一个 ToString 方法,允许将 StringBuilder 转换为字符串类型。
请注意,ListeDePersonnes 没有构造函数。在这种情况下,我们知道该
将被使用。该构造函数除了调用其父类的无参构造函数外,不执行任何操作:
一个测试类可能如下所示:
using System;
namespace Chap2 {
class Program1 {
static void Main(string[] args) {
// a list of people
ListeDePersonnes l = new ListeDePersonnes();
// add people
l = l + new Personne("jean", "martin",10) + new Personne("pauline", "leduc",12);
// display
Console.WriteLine("l=" + l);
l = l + new Enseignant("camille", "germain",27,60);
Console.WriteLine("l=" + l);
}
}
}
- 第 7 行:创建人员列表 l
- 第 9 行:使用 + 运算符添加 2 个人
- 第 12 行:添加教师
- 第 11 行和第 13 行:使用重新定义的方法 ListeDePersonnes.ToString()。
结果:
4.4. 为类定义索引器
我们在此继续使用 ListeDePersonnes 类。如果 l 是 ListeDePersonnes 类的对象,我们希望能够使用 l[i] 来指定列表 l 中第 i 号人,无论是在读取(Person p=l[i])还是写入(l[i]=new Person(...))时。
为了能够写出 l[i](其中 l[i] 表示 Person 类的一个对象),我们需要为 ListeDePersonnes 类定义以下方法:
public Personne this[int i] {
get { ... }
set { ... }
}
该方法名为 this[int i],是一个索引器,因为它赋予了表达式 obj[i] 意义——该表达式让人联想到数组表示法,尽管 obj 并非数组,而是一个对象。当执行 variable = obj[i] 时,会调用该对象 obj 的 get 方法;而当写入 obj[i] = value 时,则调用 set 方法。
ListeDePersonnes 类继承自 ArrayList,而 ArrayList 本身也具有索引器:
在 ListeDePersonnes 类中存在冲突:
public Personne this[int i]
以及该类的 ArrayList
public object this[int i]
因为它们具有相同的名称和相同的参数类型(int)。为了表明这个 ListeDePersonnes 类“封装”了 ArrayList 类中同名的方法,我们必须在 ListeDePersonnes 的声明中添加 new 关键字。因此,我们写为:
public new Personne this[int i]{
get { ... }
set { ... }
}
让我们来完善这个方法。当变量 l 的索引为 i 时(例如,其中 l 的类型为 ListeDePersonnes),会调用 this.get 方法。此时,我们需要返回列表 l 中的第 i 个元素。这通过表示法 base[i] 来实现,该表示法会获取 ListeDePersonnes 底层类 ArrayList 中的第 i 个对象。返回的对象类型为 Object,因此需要将其转换为 Person 类。
public new Personne this[int i]{
get { return (Personne) base[i]; }
set { ... }
}
当 l[i]=p(其中 p 是一个 Person 对象)时,会调用 set 方法。其目的是将对象 p 赋值给数组 l 的第 i 个元素。
public new Personne this[int i]{
get { ... }
set { base[i]=value; }
}
在此,由关键字 value 表示的对象 p 被赋值给基类 ArrayList 的第 i 个元素。
因此,类索引器 ListeDePersonnes 将如下所示:
public new Personne this[int i]{
get { return (Personne) base[i]; }
set { base[i]=value; }
}
现在,我们希望能够写出 Person p=l["name"],即不通过元素编号,而是通过人的名字来访问列表 l。为此,我们定义一个新的索引器:
// indexeur via un nom
public int this[string nom] {
get {
// on recherche la personne
for (int i = 0; i < Count; i++) {
if (((Personne)base[i]).Nom == nom)
return i;
}//for
return -1;
}//get
}
第一行
public int this[string nom]
表示 ListeDePersonnes 支持通过字符串名称进行访问,且 l[name] 的返回值为一个整数。该整数表示列表中名称为 name 的条目在列表中的位置;若该条目不存在,则返回 -1。该方法仅支持读取操作,禁止使用 l["name"]=value 这种写入操作,因为这需要定义 set 方法。 在索引器声明中无需使用 new 关键字,因为基类 ArrayList 并未定义 this[string] 索引器。
在 get 方法体内,遍历人员列表以查找参数中传入的名称。若在位置 i 处找到,则返回 i;否则返回 -1。
上述测试程序的完整实现如下:
using System;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// a list of people
ListeDePersonnes l = new ListeDePersonnes();
// add people
l = l + new Personne("jean", "martin",10) + new Personne("pauline", "leduc",12);
// display
Console.WriteLine("l=" + l);
l = l + new Enseignant("camille", "germain",27,60);
Console.WriteLine("l=" + l);
// change item 1
l[1] = new Personne("franck", "gallon",5);
// display element 1
Console.WriteLine("l[1]=" + l[1]);
// display list l
Console.WriteLine("l=" + l);
// people search
string[] noms = { "martin", "germain", "xx" };
for (int i = 0; i < noms.Length; i++) {
int inom = l[noms[i]];
if (inom != -1)
Console.WriteLine("Personne(" + noms[i] + ")=" + l[inom]);
else
Console.WriteLine("Personne(" + noms[i] + ") n'existe pas");
}//for
}
}
}
执行后得到以下结果:
4.5. 结构
C# 中的结构体与 C 语言中的结构体类似,且与类的概念非常接近。结构体的定义如下:
尽管声明方式相似,类和结构体之间存在显著差异。例如,结构体中不存在继承的概念。如果我们要编写一个无需派生的类,结构体和类之间有哪些区别能帮助我们做出选择?让我们通过以下示例来了解:
using System;
namespace Chap2 {
class Program1 {
static void Main(string[] args) {
// a sp1 structure
SPersonne sp1;
sp1.Nom = "paul";
sp1.Age = 10;
Console.WriteLine("sp1=SPersonne(" + sp1.Nom + "," + sp1.Age + ")");
// a sp2 structure
SPersonne sp2 = sp1;
Console.WriteLine("sp2=SPersonne(" + sp2.Nom + "," + sp2.Age + ")");
// sp2 is modified
sp2.Nom = "nicole";
sp2.Age = 30;
// checking sp1 and sp2
Console.WriteLine("sp1=SPersonne(" + sp1.Nom + "," + sp1.Age + ")");
Console.WriteLine("sp2=SPersonne(" + sp2.Nom + "," + sp2.Age + ")");
// an op1 object
CPersonne op1=new CPersonne();
op1.Nom = "paul";
op1.Age = 10;
Console.WriteLine("op1=CPersonne(" + op1.Nom + "," + op1.Age + ")");
// an op2 object
CPersonne op2=op1;
Console.WriteLine("op2=CPersonne(" + op2.Nom + "," + op2.Age + ")");
// op2 is modified
op2.Nom = "nicole";
op2.Age = 30;
// op1 and op2 verification
Console.WriteLine("op1=CPersonne(" + op1.Nom + "," + op1.Age + ")");
Console.WriteLine("op2=CPersonne(" + op2.Nom + "," + op2.Age + ")");
}
}
// structure SPersonne
struct SPersonne {
public string Nom;
public int Age;
}
// class CPersonne
class CPersonne {
public string Nom;
public int Age;
}
}
- 第 38-41 行:一个包含两个 public 字段的结构体:Nom、Age
- 第44-47行:一个包含两个公共字段的类:Nom、Age
如果运行该程序,将得到以下结果:
之前我们使用的是 Person,现在我们使用的是 SPersonne:
struct SPersonne {
public string Nom;
public int Age;
}
该结构体目前没有构造函数。它也可以有一个,我们稍后会展示。默认情况下,它总是具有一个无参构造函数,即 SPersonne()。
- 代码第 7 行:声明
SPersonne sp1;
等同于以下指令:
SPersonne sp1=new Spersonne();
创建了一个 (Name,Age) 结构体,而 sp1 的值即为该结构体本身。对于类而言,必须通过 new 运算符显式地创建 (Name,Age) 对象(第 22 行):
CPersonne op1=new CPersonne();
上述语句创建了一个 CPersonne 对象(大致相当于我们的结构体),而 p1 的值即为此对象的地址(即引用)。
总结
- 对于结构体而言,p1 的值即为结构体本身
- 对于类而言,p1 的值是所创建对象的地址
![]() |
当我们在程序中编写第 12 行时:
SPersonne sp2 = sp1;
此时会创建一个新的 sp2(Name,Age) 结构体,并使用 sp1(即该结构体本身)的值对其进行初始化。
![]() |
sp1 的结构在 sp2 中被复制 [1]。这是对值的复制。现在考虑第 27 行中的指令:
CPersonne op2=op1;
对于类而言,op1 的值会被复制到 op2 中,但由于该值实际上是对象地址,因此并未被复制 [2]。
对于结构体 [1],如果我们改变 sp2 的值,sp1 的值也会随之改变,正如程序所示。对于对象 [2],如果我们修改 op2 所指向的对象,op1 所指向的对象也会被修改,因为它们是同一个对象。这一点也通过程序结果得到了验证。
这些解释表明:
- 结构体变量的值即为该结构体本身
- 对象变量的值是该对象的地址
一旦理解了这一根本区别,结构体就与类非常接近,如下面的新示例所示:
using System;
namespace Chap2 {
// structure SPersonne
struct SPersonne {
// private attributes
private string nom;
private int age;
// properties
public string Nom {
get { return nom; }
set { nom = value; }
}//name
public int Age {
get { return age; }
set { age = value; }
}//age
// Manufacturer
public SPersonne(string nom, int age) {
this.nom = nom;
this.age = age;
}//manufacturer
// ToString
public override string ToString() {
return "SPersonne(" + Nom + "," + Age + ")";
}//ToString
}//structure
}//namespace
- 第 8-9 行:两个私有字段
- 第 12-20 行:相关的 public 属性
- 第 23-26 行:定义构造函数。请注意,无参构造函数 SPersonne() 始终存在,无需显式声明。若尝试声明该构造函数,编译器将报错。在第 23-26 行的构造函数中,您可能会想通过其公共属性 Name、Age 来初始化私有字段 name、age,但编译器会拒绝这种做法。在结构体构造过程中,不能使用结构体方法。
- 第 29-31 行:重定义 ToString 方法。
一个测试程序可能如下所示:
using System;
namespace Chap2 {
class Program1 {
static void Main(string[] args) {
// one person p1
SPersonne p1=new SPersonne();
p1.Nom="paul";
p1.Age= 10;
Console.WriteLine("p1={0}",p1);
// one person p2
SPersonne p2 = p1;
Console.WriteLine("p2=" + p2);
// p2 is modified
p2.Nom = "nicole";
p2.Age = 30;
// checking p1 and p2
Console.WriteLine("p1=" + p1);
Console.WriteLine("p2=" + p2);
// one person p3
SPersonne p3 = new SPersonne("amandin", 18);
Console.WriteLine("p3=" + p3);
// one person p4
SPersonne p4 = new SPersonne { Nom = "x", Age = 10 };
Console.WriteLine("p4=" + p4);
}
}
}
- 第 7 行:我们必须显式使用无参构造函数,因为该结构体中还有另一个构造函数。如果该结构体没有构造函数,则该语句
SPersonne p1;
就足以创建一个空结构体。
- 第 8-9 行:通过其公共属性对结构体进行初始化
- 第 10 行:p1.ToString 方法将在 WriteLine 中使用。
- 第 21 行:使用构造函数 SPersonne(string, int) 创建结构体
- 第24行:使用无参构造函数SPersonne()创建结构体,并在花括号内通过其公共属性初始化私有字段。
得到以下结果:
这里结构与类之间唯一的显著区别在于:如果是类,那么在程序结束时,对象 p1 和 p2 将指向同一个对象。
4.6. 接口
接口是一组构成契约的原型方法或属性。决定实现某个接口的类,即承诺提供该接口中定义的所有方法的实现。编译器会验证这一实现。
例如,以下是接口 System.Collections.IEnumerator 的定义:
public interface System.Collections.IEnumerator
{ // Prop
e rties Object Curren
t { get; }
// Methods
bool MoveNe
xt(); void Reset(); }
接口的属性和方法仅通过其签名进行定义。它们并未被实现(没有代码)。具体由实现该接口的类为接口的方法和属性提供代码。
- 第 1 行:类 C 实现了类 IEnumerator。请注意,用于实现接口的冒号与用于派生类的冒号相同。
- 第 3-5 行:实现接口 IEnumerator 的方法和属性。
考虑以下接口:
namespace Chap2 {
public interface IStats {
double Moyenne { get; }
double EcartType();
}
}
IStats 接口提供:
- 一个只读属性 Average:用于计算一组值的平均值
- 一个方法 EcartType:用于计算标准差
请注意,这里并未指定涉及的是哪一组数值。它可能是某个班级的平均成绩、某款产品的月平均销售额、某个地点的平均气温等。这就是接口的原则:我们假设对象中存在方法,但不假设存在具体的数据。
IStats 的首个具体实现可以是一个用于存储某门课程中全班学生成绩的类。学生将由 Student 结构体表示,如下所示:
public struct Elève {
public string Nom { get; set; }
public string Prénom { get; set; }
}//Student
学生将通过姓和名进行识别。第2-3行展示了这两个属性的自动属性。
笔记将通过 Note 结构进行定义,如下所示:
public struct Note {
public Elève Elève { get; set; }
public double Valeur { get; set; }
}//Note
成绩将通过被评分的学生和成绩本身来标识。第2-3行展示了这两个属性的自动属性。
特定科目中所有学生的成绩将在接下来的 TableauDeNotes 类中汇总:
using System;
using System.Text;
namespace Chap2 {
public class TableauDeNotes : IStats {
// attributes
public string Matière { get; set; }
public Note[] Notes { get; set; }
public double Moyenne { get; private set; }
private double ecartType;
// manufacturer
public TableauDeNotes(string matière, Note[] notes) {
// saving via public properties
Matière = matière;
Notes = notes;
// calculating the average score
double somme = 0;
for (int i = 0; i < Notes.Length; i++) {
somme += Notes[i].Valeur;
}
if (Notes.Length != 0) Moyenne = somme / Notes.Length;
else Moyenne = -1;
// standard deviation
double carrés = 0;
for (int i = 0; i < Notes.Length; i++) {
carrés += Math.Pow((Notes[i].Valeur - Moyenne), 2);
}//for
if (Notes.Length != 0)
ecartType = Math.Sqrt(carrés / Notes.Length);
else ecartType = -1;
}//manufacturer
public double EcartType() {
return ecartType;
}
// ToString
public override string ToString() {
StringBuilder valeur = new StringBuilder(String.Format("matière={0}, notes=(", Matière));
int i;
// concatenate all the notes
for (i = 0; i < Notes.Length-1; i++) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("],");
};
//final note
if (Notes.Length != 0) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("]");
}
valeur.Append(")");
// end
return valeur.ToString();
}//ToString
}//class
}
- 第 6 行:类 TableauDeNotes 实现了 IStats 接口。因此,它必须实现 Average 和 EcartType 方法。这些方法分别在第 10 行(Average)和第 35-37 行(EcartType)中实现
- 第 8-10 行:三个自动属性
- 第 8 行:对象存储其笔记的课程
- 第 9 行:学生成绩表(Student, Grade)
- 第 10 行:平均分——实现 IStats 接口 Average 的属性
- 第 11 行:存储分数标准差的字段——第 35-37 行中与 EcartType 关联的 get 方法实现了 IStats 接口中的 EcartType 接口。
- 第9行:成绩存储在表中。当构建类TableauDeNotes时,这些数据会被传递给第14至33行的生成器。
- 第14-33行:构造器。此处假设传递给构造器的分数在未来不会发生变化。因此,我们利用构造器立即计算这些分数的平均值和标准差,并将它们存储在第10-11行的字段中。平均值存储在第10行自动属性Average对应的私有字段中,标准差则存储在第11行的私有字段中。
- 第 10 行:get 方法自动拥有 Average,将呈现其底层的私有字段。
- 第 35-37 行:方法 EcartType 返回第 11 行私有字段的值。
这段代码中存在一些细节:
- 第 23 行:使用 set 属性 Average 方法进行赋值。该方法已在第 10 行声明为私有,因此只有在该方法内部才能对 Average 进行赋值。
- 第 40-54 行:使用 StringBuilder 对象来构建表示 TableauDeNotes 的字符串,以提升性能。但需注意,此举会显著降低代码的可读性。这就是事物的另一面。
在上一节中,笔记存储在表中。一旦创建了 TableauDeNotes,就无法再添加新笔记。现在,我们提出 IStats 的第二个实现方案,名为 ListeDeNotes,这次笔记将保存在列表中,并且在 ListeDeNotes 对象初始化后仍可添加笔记。
ListeDeNotes 类的代码如下:
using System;
using System.Text;
using System.Collections.Generic;
namespace Chap2 {
public class ListeDeNotes : IStats {
// attributes
public string Matière { get; set; }
public List<Note> Notes { get; set; }
public double moyenne = -1;
public double ecartType = -1;
// manufacturer
public ListeDeNotes(string matière, List<Note> notes) {
// saving via public properties
Matière = matière;
Notes = notes;
}//manufacturer
// add a note
public void Ajouter(Note note) {
// add note
Notes.Add(note);
// mean and standard deviation reset
moyenne = -1;
ecartType = -1;
}
// ToString
public override string ToString() {
StringBuilder valeur = new StringBuilder(String.Format("matière={0}, notes=(", Matière));
int i;
// concatenate all the notes
for (i = 0; i < Notes.Count - 1; i++) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("],");
};
//final note
if (Notes.Count != 0) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("]");
}
valeur.Append(")");
// end
return valeur.ToString();
}//ToString
// average score
public double Moyenne {
get {
if (moyenne != -1) return moyenne;
// calculating the average score
double somme = 0;
for (int i = 0; i < Notes.Count; i++) {
somme += Notes[i].Valeur;
}
// we return the average
if (Notes.Count != 0) moyenne = somme / Notes.Count;
return moyenne;
}
}
public double EcartType() {
// standard deviation
if (ecartType != -1) return ecartType;
// average
double moyenne = Moyenne;
double carrés = 0;
for (int i = 0; i < Notes.Count; i++) {
carrés += Math.Pow((Notes[i].Valeur - moyenne), 2);
}//for
// we return the standard deviation
if (Notes.Count != 0)
ecartType = Math.Sqrt(carrés / Notes.Count);
return ecartType;
}
}//class
}
- 第 7 行:类 ListeDeNotes 实现了 IStats 接口
- 第 10 行:笔记现在以列表形式显示,而非表格
- 第 11 行:自动所有权。此处已放弃 TableauDeNotes 平均类,转而使用第 11 行中的私有字段 average,该字段与第 48-60 行中的只读公共所有权 Average 相关联
- 第 22-28 行:现在可以向已存储的注释中添加新注释,而此前这是无法实现的。
- 第 15-19 行:因此,均值和标准差不再在构造函数中计算,而是在接口方法本身中计算:Average(第 48-60 行)和 EcartType(第 62-76 行)。但是,只有当均值和标准差不同于 -1 时,才会重新开始计算(第 50 和 64 行)。
一个测试类可能如下所示:
using System;
using System.Collections.Generic;
namespace Chap2 {
class Program1 {
static void Main(string[] args) {
// some students & english notes
Elève[] élèves1 = { new Elève { Prénom = "Paul", Nom = "Martin" }, new Elève { Prénom = "Maxime", Nom = "Germain" }, new Elève { Prénom = "Berthine", Nom = "Samin" } };
Note[] notes1 = { new Note { Elève = élèves1[0], Valeur = 14 }, new Note { Elève = élèves1[1], Valeur = 16 }, new Note { Elève = élèves1[2], Valeur = 18 } };
// which we save in a TableauDeNotes object
TableauDeNotes anglais = new TableauDeNotes("anglais", notes1);
// average and standard deviation display
Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", anglais.Moyenne, anglais.EcartType(), anglais);
// we put the students and the material in a ListeDeNotes object
ListeDeNotes français = new ListeDeNotes("français", new List<Note>(notes1));
// average and standard deviation display
Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", français.Moyenne, français.EcartType(), français);
// we add a note
français.Ajouter(new Note { Elève = new Elève { Prénom = "Jérôme", Nom = "Jaric" }, Valeur = 10 });
// average and standard deviation display
Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", français.Moyenne, français.EcartType(), français);
}
}
}
- 第 8 行:使用无参构造函数并通过公共属性进行初始化,创建一个学生数组
- 第 9 行:使用相同的方法创建一个笔记表
- 第 11 行:创建 TableauDeNotes 对象,其均值和标准差在第 13 行计算
- 第 15 行:创建一个 ListeDeNotes 对象,其均值和标准差在第 17 行计算。List<Note> 类有一个构造函数,该构造函数接受一个实现 IEnumerable<Note> 接口的对象。表 notes1 实现了该接口,可用于构建 List<Note>。
- 第 19 行:添加了一条新笔记
- 第 21 行:重新计算平均值和标准差
结果如下:
在上一个示例中,有两个类实现了 IStats 接口。不过,该示例并未展示 IStats 接口的实际用途。让我们将测试程序重写如下:
using System;
using System.Collections.Generic;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// some students & english notes
Elève[] élèves1 = { new Elève { Prénom = "Paul", Nom = "Martin" }, new Elève { Prénom = "Maxime", Nom = "Germain" }, new Elève { Prénom = "Berthine", Nom = "Samin" } };
Note[] notes1 = { new Note { Elève = élèves1[0], Valeur = 14 }, new Note { Elève = élèves1[1], Valeur = 16 }, new Note { Elève = élèves1[2], Valeur = 18 } };
// which we save in a TableauDeNotes object
TableauDeNotes anglais = new TableauDeNotes("anglais", notes1);
// average and standard deviation display
AfficheStats(anglais);
// we put the students and the material in a ListeDeNotes object
ListeDeNotes français = new ListeDeNotes("français", new List<Note>(notes1));
// average and standard deviation display
AfficheStats(français);
// we add a note
français.Ajouter(new Note { Elève = new Elève { Prénom = "Jérôme", Nom = "Jaric" }, Valeur = 10 });
// average and standard deviation display
AfficheStats(français);
}
// display mean and standard deviation of a type IStats
static void AfficheStats(IStats valeurs) {
Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", valeurs.Moyenne, valeurs.EcartType(), valeurs);
}
}
}
- 第 25-27 行:静态方法 AfficheStats 接收一个 IStats 接口。这意味着实际参数可以是任何实现 IStats 接口的对象。当使用接口类型的数据时,这意味着你仅使用数据所实现的接口方法。 其余部分将被忽略。这是一种类似于类中多态性的特性。如果一组不通过继承关联的 Ci 类(因此无法使用继承的多态性)提供了一组具有相同签名的方法,那么将这些方法归入一个由所有相关类实现的接口 I 中会很有意义。 这些类 Ci 的实例随后可作为函数的有效参数,这些函数接受形式参数 ,其类型为 I,即仅使用在 I 中定义的 Ci 对象方法,而不使用各个类 Ci 的属性及方法。
- 第 13 行:调用方法 AfficheStats,传入一个实现了 IStats 接口的 TableauDeNotes 对象
- 第 17 行:同上,但使用类型 ListeDeNotes
本次运行的结果与前一次完全相同。
变量可以是接口类型。因此,我们可以这样写:
第 1 行语句表明 stats1 是实现 IStats 接口的类的实例。该语句意味着编译器仅允许访问 stats1 的接口方法:Average 和 EcartType。
最后,需要注意的是,接口可以通过多种方式实现,即可以写成
其中 Ij 表示接口。
4.7. 抽象类
抽象类是指无法被实例化的类。你需要创建可以被实例化的派生类。
抽象类可用于将一组类的代码进行抽象化。请考虑以下情况:
using System;
namespace Chap2 {
abstract class Utilisateur {
// fields
private string login;
private string motDePasse;
private string role;
// manufacturer
public Utilisateur(string login, string motDePasse) {
// information is recorded
this.login = login;
this.motDePasse = motDePasse;
// on identifie l'utilisateur
role=identifie();
// identified?
if (role == null) {
throw new ExceptionUtilisateurInconnu(String.Format("[{0},{1}]", login, motDePasse));
}
}
// toString
public override string ToString() {
return String.Format("Utilisateur[{0},{1},{2}]", login, motDePasse, role);
}
// identifies
abstract public string identifie();
}
}
- 第 11-21 行:类构建器 User。该类存储 Web 应用程序用户的信息。该应用程序包含多种类型的用户,通过用户名/密码进行身份验证(第 6-7 行)。对于部分用户,这两项信息通过 LDAP 服务进行验证;对于其他用户,则通过关系型数据库管理系统(RDBMS)进行验证,等等……
- 第13-14行:认证信息存储在内存中
- 第 16 行:这些信息由 `identifies` 方法进行验证。由于身份验证方法的具体实现未知,因此在第 29 行使用 `abstract` 关键字将其声明为抽象方法。`identifies` 方法返回一个字符串,该字符串指定用户的角色(即用户被允许执行的操作)。如果该字符串为空,则在第 19 行抛出异常。
- 第 4 行:由于包含抽象方法,该类本身使用 abstract 关键字被声明为抽象类。
- 第 29 行:抽象方法 `identifies` 没有具体实现。派生类将为其提供实现。
- 第 24-26 行:ToString 方法用于表示该类的实例。
此处假设开发者希望控制 User 类及其派生类的实例构造,可能是为了确保当用户无法被识别时(第 19 行),会抛出特定类型的异常。派生类可以依赖此构造函数。为此,它们必须提供该构造函数。
ExceptionUtilisateurInconnu 类的定义如下:
using System;
namespace Chap2 {
class ExceptionUtilisateurInconnu : Exception {
public ExceptionUtilisateurInconnu(string message) : base(message){
}
}
}
- 第 3 行:继承自 Exception
- 第4-6行:它有一个构造函数,该构造函数接受一条错误消息作为参数。该参数被传递给父类(第5行),父类具有相同的构造函数。
现在,我们在女生班的 Director 类中派生 User 类:
namespace Chap2 {
class Administrateur : Utilisateur {
// manufacturer
public Administrateur(string login, string motDePasse)
: base(login, motDePasse) {
}
// identifies
public override string identifie() {
// identification LDAP
// ...
return "admin";
}
}
}
- 第 4-6 行:构造函数只是将接收到的参数传递给其父类
- 第9-12行:该方法重写了Director类的identifies方法。假设管理员由LDAP系统进行身份验证。此方法重写了父类的identifies方法。由于重写的是抽象方法,因此无需添加override关键字。
现在,我们将 User 类从 Observer 类派生出来:
namespace Chap2 {
class Observateur : Utilisateur{
// manufacturer
public Observateur(string login, string motDePasse)
: base(login, motDePasse) {
}
//identifies
public override string identifie() {
// identification SGBD
// ...
return "observateur";
}
}
}
- 第 4-6 行:构造函数只是将接收到的参数传递给其父类
- 第9-13行:该方法识别Observer类。假设通过在数据库中核对其身份信息来识别观察者。
最终,Director 和 Observer 对象由与父类 User 相同的构造函数实例化。该构造函数将使用这些类提供的标识信息。
第三个类Unknown也继承自User:
namespace Chap2 {
class Inconnu : Utilisateur{
// manufacturer
public Inconnu(string login, string motDePasse)
: base(login, motDePasse) {
}
//identifies
public override string identifie() {
// unknown user
// ...
return null;
}
}
}
- 第 13 行:该方法将指针设为空,以表示未识别到用户。
一个测试程序可能如下所示:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Console.WriteLine(new Observateur("observer","mdp1"));
Console.WriteLine(new Administrateur("admin", "mdp2"));
try {
Console.WriteLine(new Inconnu("xx", "yy"));
} catch (ExceptionUtilisateurInconnu e) {
Console.WriteLine("Utilisateur non connu : "+ e.Message);
}
}
}
}
请注意,第 6、7 和 9 行使用了 [User].ToString(),该方法将被 WriteLine 调用。
结果如下:
4.8. 类、接口和泛型方法
假设我们要编写一个用于交换两个整数顺序的方法。该方法可以如下所示:
public static void Echanger1(ref int value1, ref int value2){
// on échange les références value1 et value2
int temp = value2;
value2 = value1;
value1 = temp;
}
现在,如果我们要交换两个 Person 对象的引用,我们会这样写:
public static void Echanger2(ref Personne value1, ref Personne value2){
// on échange les références value1 et value2
Personne temp = value2;
value2 = value1;
value1 = temp;
}
这两种方法的区别在于参数的类型 T:Exchange1 中为 int,Exchange2 中为 Person。泛型类和接口满足了仅在某些参数类型上有所不同的方法的需求。
使用泛型类,Exchange 可以重写如下:
namespace Chap2 {
class Generic1<T> {
public static void Echanger(ref T value1, ref T value2){
// exchange the value1 and value2 references
T temp = value2;
value2 = value1;
value1 = temp;
}
}
}
- 第 2 行:类 Generic1 由类型 T 进行泛型化。您可以为其指定任意名称。随后在第 3 行和第 5 行中,该类型 T 被该类重复使用。我们称 Generic1 为泛型类。
- 第 3 行:定义了两个指向 T 类型的引用,用于进行交换
- 第 5 行:临时变量 temp 的类型为 T。
该类的测试程序可以如下所示:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
// int
int i1 = 1, i2 = 2;
Generic1<int>.Echanger(ref i1, ref i2);
Console.WriteLine("i1={0},i2={1}", i1, i2);
// string
string s1 = "s1", s2 = "s2";
Generic1<string>.Echanger(ref s1, ref s2);
Console.WriteLine("s1={0},s2={1}", s1, s2);
// Person
Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55);
Generic1<Personne>.Echanger(ref p1, ref p2);
Console.WriteLine("p1={0},p2={1}", p1, p2);
}
}
}
- 第 8 行:当使用由类型 T1、T2 等参数化的泛型类时,这些类型必须被“实例化”。第 8 行:使用静态方法 Exchange(Generic1<int>) 来表示传递给 Exchange 的引用是 int 类型的。
- 第 12 行:使用静态方法 Exchange 类型 Generic1<string>,以表明传递给 Exchange 的引用是 string 类型的。
- 第 16 行:使用静态方法 Exchange 类型 Generic1<Person>,以表明传递给 Exchange 的引用是 Person 类型的。
结果如下:
Exchange 方法也可以写成如下形式:
namespace Chap2 {
class Generic2 {
public static void Echanger<T>(ref T value1, ref T value2){
// exchange the value1 and value2 references
T temp = value2;
value2 = value1;
value1 = temp;
}
}
}
- 第 2 行:类 Generic2 不再是泛型类
- 第 3 行:静态方法 Exchange 是泛型的
测试程序如下:
using System;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// int
int i1 = 1, i2 = 2;
Generic2.Echanger<int>(ref i1, ref i2);
Console.WriteLine("i1={0},i2={1}", i1, i2);
// string
string s1 = "s1", s2 = "s2";
Generic2.Echanger<string>(ref s1, ref s2);
Console.WriteLine("s1={0},s2={1}", s1, s2);
// Person
Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55);
Generic2.Echanger<Personne>(ref p1, ref p2);
Console.WriteLine("p1={0},p2={1}", p1, p2);
}
}
}
- 第 8、12 和 16 行:通过在 <> 中指定参数类型来调用 Exchange。实际上,编译器能够推断出应使用的 Exchange 变体。因此,以下写法是合法的:
Generic2.Echanger(ref i1, ref i2);
...
Generic2.Echanger(ref s1, ref s2);
...
Generic2.Echanger(ref p1, ref p2);
第 1、3 和 5 行:方法 Exchange 的泛型类型不再显式指定。编译器能够根据所用实际参数的性质推导出该类型。
可以在泛型参数上设置约束:

考虑以下新的泛型方法 Exchange:
namespace Chap2 {
class Generic3 {
public static void Echanger<T>(ref T value1, ref T value2) where T : class {
// exchange the value1 and value2 references
T temp = value2;
value2 = value1;
value1 = temp;
}
}
}
- 第 3 行:类型 T 必须是引用(类、接口)
请看以下测试程序:
using System;
namespace Chap2 {
class Program4 {
static void Main(string[] args) {
// int
int i1 = 1, i2 = 2;
Generic3.Echanger<int>(ref i1, ref i2);
Console.WriteLine("i1={0},i2={1}", i1, i2);
// string
string s1 = "s1", s2 = "s2";
Generic3.Echanger(ref s1, ref s2);
Console.WriteLine("s1={0},s2={1}", s1, s2);
// Person
Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55);
Generic3.Echanger(ref p1, ref p2);
Console.WriteLine("p1={0},p2={1}", p1, p2);
}
}
}
编译器在第 8 行报错,因为类型 int 不是类或接口,而是一个结构体:

接下来考虑新的泛型方法 Exchange:
namespace Chap2 {
class Generic4 {
public static void Echanger<T>(ref T element1, ref T element2) where T : Interface1 {
// retrieve the value of the 2 elements
int value1 = element1.Value();
int value2 = element2.Value();
// if 1st element > 2nd element, exchange elements
if (value1 > value2) {
T temp = element2;
element2 = element1;
element1 = temp;
}
}
}
}
- 第 3 行:类型 T 必须实现 Interface1。该接口有一个名为 Value 的方法,在第 5 行和第 6 行中使用,用于返回类型 T 的对象的值。
- 第 8-12 行:只有当 element1 的值大于 element2 的值时,才会交换 element1 和 element2 这两个引用。
接口 Interface1 定义如下:
namespace Chap2 {
interface Interface1 {
int Value();
}
}
它由 Class1 实现如下:
using System;
using System.Threading;
namespace Chap2 {
class Class1 : Interface1 {
// object value
private int value;
// manufacturer
public Class1() {
// wait 1 ms
Thread.Sleep(1);
// random value between 0 and 99
value = new Random(DateTime.Now.Millisecond).Next(100);
}
// accessor private field value
public int Value() {
return value;
}
// instance status
public override string ToString() {
return value.ToString();
}
}
}
- 第 5 行:Class1 实现了 Interface1
- 第 7 行:Class1 实例的值
- 第 10-14 行:字段 value 被初始化为 0 到 99 之间的随机值
- 第 18-20 行:调用 Interface1 接口的 Value 方法
- 第 23-25 行:ToString 方法,类 Interface1
接口 Interface1 也被 Class2 实现:
using System;
namespace Chap2 {
class Class2 : Interface1 {
// object values
private int value;
private String s;
// manufacturer
public Class2(String s) {
this.s = s;
value = s.Length;
}
// accessor private field value
public int Value() {
return value;
}
// instance status
public override string ToString() {
return s;
}
}
}
- 第 4 行:Class2 实现了 Interface1
- 第 6 行:Class2 实例的值
- 第 10-13 行:字段 value 被初始化为传递给构造函数的字符串的长度
- 第 16-18 行:Value 方法,接口 Interface1
- 第 21-22 行:ToString 方法
一个测试程序可能如下所示:
using System;
namespace Chap2 {
class Program5 {
static void Main(string[] args) {
// exchange instances of type Class1
Class1 c1, c2;
for (int i = 0; i < 5; i++) {
c1 = new Class1();
c2 = new Class1();
Console.WriteLine("Avant échange --> c1={0},c2={1}", c1, c2);
Generic4.Echanger(ref c1, ref c2);
Console.WriteLine("Après échange --> c1={0},c2={1}", c1, c2);
}
// exchange Class2 instances
Class2 c3, c4;
c3 = new Class2("xxxxxxxxxxxxxx");
c4 = new Class2("xx");
Console.WriteLine("Avant échange --> c3={0},c4={1}", c3, c4);
Generic4.Echanger(ref c3, ref c4);
Console.WriteLine("Avant échange --> c3={0},c4={1}", c3, c4);
}
}
}
- 第 8-14 行:交换 Class1 的实例
- 第 16-22 行:交换 Class2 类型的实例
结果如下:
为说明泛型接口( )的概念,我们将对一个人员数组先按姓名排序,再按年龄排序。用于排序数组的方法是Spell类中的静态方法Array:

请记住,调用静态方法时需在方法名前加上类名,而非类实例名。Spell 类具有不同的方法签名(即重载)。我们将使用以下签名:
Spell 是一个泛型方法,其中 T 表示任意类型。该方法接收两个参数:
- T[] 数组:待排序的 T 元素数组
- IComparer<T> comparator:实现 IComparer<T> 接口的对象引用。
IComparer<T> 是一个泛型接口,定义如下:
IComparer<T> 接口仅包含一个方法。该方法 Compare:
- 接收两个类型为 T 的参数 t1 和 t2
- 若 t1>t2 则返回 1,若 t1==t2 则返回 0,若 t1<t2 则返回 -1。<、==、> 运算符的具体含义由开发者定义。例如,若 p1 和 p2 是两个 Person 对象,当 p1 的名字在字母顺序上排在 p2 之前时,可认为 p1>p2。 接下来我们将按名字升序排序。若需按年龄排序,则定义当 p1 的年龄大于 p2 的年龄时,p1>p2。
- 若要按降序排序,只需将 +1 和 -1 的结果互换即可
我们已经掌握了排序人员表所需的知识。程序如下:
using System;
using System.Collections.Generic;
namespace Chap2 {
class Program6 {
static void Main(string[] args) {
// a table of people
Personne[] personnes1 = { new Personne("claude", "pollon", 25), new Personne("valentine", "germain", 35), new Personne("paul", "germain", 32) };
// display
Affiche("Tableau à trier", personnes1);
// sort by name
Array.Sort(personnes1, new CompareNoms());
// display
Affiche("Tableau après le tri selon les nom et prénom", personnes1);
// sorted by age
Array.Sort(personnes1, new CompareAges());
// display
Affiche("Tableau après le tri selon l'âge", personnes1);
}
static void Affiche(string texte, Personne[] personnes) {
Console.WriteLine(texte.PadRight(50, '-'));
foreach (Personne p in personnes) {
Console.WriteLine(p);
}
}
}
// first and last name comparison class
class CompareNoms : IComparer<Personne> {
public int Compare(Personne p1, Personne p2) {
// compare names
int i = p1.Nom.CompareTo(p2.Nom);
if (i != 0)
return i;
// equal names - first names are compared
return p1.Prenom.CompareTo(p2.Prenom);
}
}
// age comparison class
class CompareAges : IComparer<Personne> {
public int Compare(Personne p1, Personne p2) {
// comparing ages
if (p1.Age > p2.Age)
return 1;
else if (p1.Age == p2.Age)
return 0;
else
return -1;
}
}
}
- 第 8 行:人员表
- 第 12 行:按名字和姓氏对人员表进行排序。泛型方法 Spell 的第二个参数是实现泛型接口 IComparer<Person> 的 CompareNoms 类的实例。
- 第 30-39 行:实现泛型 IComparer<Person> 接口的 CompareNoms 类。
- 第 31-38 行:实现泛型方法 int CompareTo(T, T) 接口 IComparer<T>。该方法使用 clipboard 3.3.5.4 中介绍的 String.CompareTo 方法来比较两个字符串。
- 第 16 行:按年龄对人员表进行排序。泛型方法 Spell 的第二个参数是 CompareAges 的一个实例,该类实现了泛型接口 IComparer<Person>,并在第 42-51 行中定义。
结果如下:
4.9. 命名空间
要在屏幕上输出一行,我们使用以下指令
如果我们查看 Console 的定义
Namespace: System
Assembly: Mscorlib (in Mscorlib.dll)
我们会发现它属于 System 命名空间。这意味着 Console 应通过 System.Console 来表示,因此我们实际上应写为:
通过使用 using 语句可以避免这种情况:
我们使用 using 子句导入 System 命名空间。当编译器遇到一个类名(此处为 Console)时,它会尝试在 using 子句导入的各个命名空间中查找该类。在此处,它将在 System 命名空间中找到 Console 类。现在,让我们注意与 Console 类相关联的第二条信息:
这一行指明了类 Console 的定义位于哪个“程序集”中。当在 Visual Studio 外部进行编译,且需要为包含待用类的各个 DLL 提供引用时,此信息会非常有用。要引用编译类所需的 DLL,我们写:
其中 csc 是 C# 编译器。当我们创建一个类时,可以将其置于命名空间内。这些命名空间的目的是避免类在销售时发生名称冲突,例如。 假设两家公司 E1 和 E2 分别发布了打包在 e1.dll 和 e2.dll 中的类。假设客户 C 购买了这两套类,其中两家公司都定义了一个名为 Person 的类。客户 C 编译了一个如下所示的程序:
如果源文件 prog.cs 使用了 Person 类,编译器将无法确定应采用 e1.dll 中的 Person 还是 e2.dll 中的 Person,从而报错。如果 E1 公司将其类定义在名为 E1 的命名空间中,而 E2 公司将其类定义在名为 E2 的命名空间中,那么这两个 Person 类将分别被称为 E1.Person 和 E2.Personne。 客户必须使用 E1.Personne 或 E2.Personne,而不能直接使用 Person。命名空间消除了任何歧义。
要在命名空间中创建类,请编写:
4.10. 示例应用程序 - V2
我们将重复上一章第3.6节中已经学过的税费计算,并使用类和接口来处理它。让我们回顾一下这个问题:
我们建议编写一个程序来计算纳税人的所得税。简化情况是纳税人仅需申报工资收入(2003年收入,采用2004年数据):
- 员工份额数按以下规则计算:若未婚,则 nbParts = nbEnfants / 2 + 1;若已婚,则 nbParts = nbEnfants / 2 + 2,其中 nbEnfants 表示其子女数。
- 若其子女数≥3,则额外增加半份
- 计算应税收入 R=0.72*S,其中 S 为其年薪
- 计算家庭系数 QF=R/nbParts
- 计算您的税额 I。请参考下表:
4262 | 0 | 0 |
8382 | 0.0683 | 291.09 |
14753 | 0.1914 | 1322.92 |
23888 | 0.2826 | 2668.39 |
38868 | 0.3738 | 4846.98 |
47932 | 0.4262 | 6883.66 |
0 | 0.4809 | 9505.54 |
每行有 3 个字段。要计算税款 I,请查找 QF<=champ1 的第一行。例如,如果 QF=5000,则找到该行
因此,Tax I 等于 0.0683*R - 291.09*nbParts。如果 QF 使得关系 QF<=champ1 从未被检查,则使用最后一行中的系数。此处:
由此得出税款 I=0.4809*R - 9505.54*nbParts。
首先,我们定义一个能够封装前述数组中一行数据的结构:
namespace Chap2 {
// a tax bracket
struct TrancheImpot {
public decimal Limite { get; set; }
public decimal CoeffR { get; set; }
public decimal CoeffN { get; set; }
}
}
然后,我们定义一个能够计算税款的接口 IImpot:
namespace Chap2 {
interface IImpot {
int calculer(bool marié, int nbEnfants, int salaire);
}
}
- 第3行:基于三项数据(纳税人是否已婚、子女数量、工资)的税额计算方法
接下来,我们定义一个实现该接口的抽象类:
namespace Chap2 {
abstract class AbstractImpot : IImpot {
// tax brackets required to calculate tax
// come from an external source
protected TrancheImpot[] tranchesImpot;
// tAX CALCULATION
public int calculer(bool marié, int nbEnfants, int salaire) {
// calculating the number of shares
decimal nbParts;
if (marié) nbParts = (decimal)nbEnfants / 2 + 2;
else nbParts = (decimal)nbEnfants / 2 + 1;
if (nbEnfants >= 3) nbParts += 0.5M;
// calculation of taxable income & family quota
decimal revenu = 0.72M * salaire;
decimal QF = revenu / nbParts;
// tAX CALCULATION
tranchesImpot[tranchesImpot.Length - 1].Limite = QF + 1;
int i = 0;
while (QF > tranchesImpot[i].Limite) i++;
// return result
return (int)(revenu * tranchesImpot[i].CoeffR - nbParts * tranchesImpot[i].CoeffN);
}//calculate
}//class
}
- 第 2 行:类 AbstractImpot 实现了 IImpot 接口。
- 第 7 行:以受保护字段形式存储的年度税额计算数据。AbstractImpot 类不知道该字段将如何初始化,而是将此任务留给派生类。这就是为什么它被声明为抽象类(第 2 行),以防止任何实例化。
- 第 10-25 行:实现 IImpot 接口的 calculate 方法。派生类无需重写此方法。AbstractImpot 充当派生类的抽象基类,用于封装所有派生类共有的逻辑。
通过继承 AbstractImpot 类,可以构建一个实现 IImpot 接口的类。这就是我们现在正在做的事情:
using System;
namespace Chap2 {
class HardwiredImpot : AbstractImpot {
// data tables for tax calculations
decimal[] limites = { 4962M, 8382M, 14753M, 23888M, 38868M, 47932M, 0M };
decimal[] coeffR = { 0M, 0.068M, 0.191M, 0.283M, 0.374M, 0.426M, 0.481M };
decimal[] coeffN = { 0M, 291.09M, 1322.92M, 2668.39M, 4846.98M, 6883.66M, 9505.54M };
public HardwiredImpot() {
// creation of tax bracket table
tranchesImpot = new TrancheImpot[limites.Length];
// filling
for (int i = 0; i < tranchesImpot.Length; i++) {
tranchesImpot[i] = new TrancheImpot { Limite = limites[i], CoeffR = coeffR[i], CoeffN = coeffN[i] };
}
}
}// class
}// namespace
类 HardwiredImpot 在第 7-9 行定义了税额计算所需的硬数据。其构造函数(第 11-18 行)使用这些数据来初始化父类 AbstractImpot 的受保护字段 tranchesImpot。
一个测试程序可以如下所示:
using System;
namespace Chap2 {
class Program {
static void Main() {
// interactive Tax calculation program
// l'user types three data into keyboard: married nbEnfants salary
// the program then displays Tax payable
const string syntaxe = "syntaxe : Marié NbEnfants Salaire\n"
+ "Marié : o pour marié, n pour non marié\n"
+ "NbEnfants : nombre d'enfants\n"
+ "Salaire : salaire annuel en F";
// creation of a IImpot object
IImpot impot = new HardwiredImpot();
// infinite loop
while (true) {
// tax calculation parameters are requested
Console.Write("Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :");
string paramètres = Console.ReadLine().Trim();
// anything to do?
if (paramètres == null || paramètres == "") break;
// check number of arguments in the input line
string[] args = paramètres.Split(null);
int nbParamètres = args.Length;
if (nbParamètres != 3) {
Console.WriteLine(syntaxe);
continue;
}//if
// checking the validity of parameters
// married
string marié = args[0].ToLower();
if (marié != "o" && marié != "n") {
Console.WriteLine(syntaxe + "\nArgument marié incorrect : tapez o ou n");
continue;
}//if
// nbEnfants
int nbEnfants = 0;
bool dataOk = false;
try {
nbEnfants = int.Parse(args[1]);
dataOk = nbEnfants >= 0;
} catch {
}//if
// correct data?
if (!dataOk) {
Console.WriteLine(syntaxe + "\nArgument NbEnfants incorrect : tapez un entier positif ou nul");
continue;
}
// salary
int salaire = 0;
dataOk = false;
try {
salaire = int.Parse(args[2]);
dataOk = salaire >= 0;
} catch {
}//try-catch
// correct data?
if (!dataOk) {
Console.WriteLine(syntaxe + "\nArgument salaire incorrect : tapez un entier positif ou nul");
continue;
}
// parameters are correct - Tax is calculated
Console.WriteLine("Impot=" + impot.calculer(marié == "o", nbEnfants, salaire) + " euros");
// next taxpayer
}//while
}
}
}
上述程序允许用户运行多次税务计算模拟。
- 第 16 行:创建实现 IImpot 接口的 tax 对象。该对象通过实例化 HardwiredImpot 类型获得,该类型实现了 IImpot 接口。请注意,我们为变量 tax 赋予的类型是 IImpot,而非 HardwiredImpot。这表明我们仅关注 tax 对象的计算功能,而不涉及其他部分。
- 第19-68行:税费计算模拟循环
- 第 22 行:通过键盘输入一行内容,获取方法 calculate 所需的三项参数。
- 第 26 行:方法 [string].Split(null) 将 [string] 拆分为单词,并将这些单词存储在数组 args 中。
- 第 66 行:调用实现 IImpot 接口的 calculate 对象来计算税额。
以下是运行该程序的示例:









