- A+
一、类的概述
在上一篇文章中(https://www.dotnetprimer.com/csharp/01-types-and-variables-in-csharp/#六用户定义类型),我们看到 C# 提供了 6 种用户定义类型。其中最重要的,也是首先要阐述的是类。因为类在 C# 中是个很大的主题,所以关于它的讨论将会延伸到接下来的几篇文章。
类是一种活动的数据结构
在面向对象的分析和设计出现之前,程序员们仅把程序当作指令的序列,那时的焦点主要放在指令的组合和优化上。随着面向对象的出现,焦点从优化指令转移到组织程序的数据和功能上来。程序的数据和功能被组织为逻辑上相关的数据项和函数的封装集合,并被称为类。
类是一个能存储数据并执行代码的数据结构。它包含数据成员和函数成员。
- 数据成员 它存储与类或类的实例相关的数据。数据成员通常模拟该类所表示的现实世界事物的特性。
- 函数成员 它执行代码,通常会模拟类所表示的现实世界事物的功能和操作。
一个 C# 类可以有任意数目的数据成员和函数成员。成员可以是 9 种成员类型的任意组合。这些成员类型如下表所示。
数据成员存储数据 | 函数成员执行代码 |
---|---|
字段 常量 |
方法 运算符 属性 索引器 构造函数 事件 析构函数 |
说明 类是逻辑相关的数据和函数的封装,通常代表真实世界中或概念上的事物。
二、程序和类:一个简单的示例
一个运行中的 C# 程序是一组相互作用的类型对象,它们中的大部分是类的实例。例如,假设有一个模拟扑克牌游戏的程序。当程序运行时,它有一个名为 Dealer
的类实例,它的工作就是运行游戏。还有几个名为 Player
的类实例,它们代表游戏的玩家。
Dealer
对象保存纸牌的当前状态和玩家数目等信息。它的动作包括洗牌和发牌。
Player
类有很大不同。它保存玩家名称以及用于押注的钱等信息,并实现如分析玩家当前手上的牌和出牌这样的动作。运行中的程序如下图所示。类名显示在方框外面,实例名显示在方框内。
一个真正的程序无疑会包含除 DeaLer
和 Player
之外的许多具他的类,还会包括像 Card
和 Deck
这样的类。每一个类都模拟某种扑克牌游戏中的事物。
说明 运行中的程序是一组相互作用的对象的集合。
三、声明类
或许你能猜到,虽然类型 int
、double
和 char
由 C# 定义,但像 Dealer
和 Player
这样的类不是由语言定义的。如果想在程序中使用它们,你必须通过编写类的声明自己定义它们。
类的声明定义新类的特征和成员。它并不创建类的实例,但创建用于创建实例的模板。类的声明提供下列内容:
- 类的名称;
- 类的成员;
- 类的特征。
下面是一个最简单的类声明语法示例。大括号内包含了成员的声明,它们组成了类主体。类成员可以在类主体内部以任何顺序声明。这意味着一个成员的声明完全可以引用另一个在后面的类声明中才定义的成员。
下面的代码给出了两个类声明的概貌:
class Dealer // 类声明 { ... } class Player // 类声明 { ... }
说明 因为类声明“定义”了一个新类,所以经常会在文献和程序员的日常使用中看到类声明被称为类定义。
四、类成员
字段和方法是最重要的类成员类型。字段是数据成员,方法是函数成员。
4.1 字段
字段是隶属于类的变量。
- 它可以是任何类型,无论是预定义类型还是用户定义类型。
- 和所有变量一样,字段用来保存数据,并具有如下特征:
- 可以被写入;
- 可以被读取。
声明一个字段最简单的语句如下:
例如,下面的类包含字段 MyField
的声明,它可以保存 int
值:
class MyClass { int MyField; }
说明 与 C 和 C++ 不同,C# 在类型的外部不能声明全局变量(也就是变量或字段)。所有的字段都属于类型,而且必须在类型声明内部声明。
1. 显式和隐式字段初始化
因为字段是一种变量,所以字段初始化语句在语法上和上一篇([{{< ref "01-04-类型和变量.md#91-变量声明" >}}]({{< ref "01-04-类型和变量.md#91-变量声明" >}}))所述的变量初始化语句相同。
-
字段初始化语句是字段声明的一部分,由一个等号后面跟着一个求值表达式组成。
-
初始化值必须是编译时可确定的。
class MyClass { int f1 = 17; }
-
如果没有初始化语句,字段的值会被编译器设为默认值,默认值由字段的类型决定。简单类型的默认值见([{{< ref "01-04-类型和变量.md#五预定义类型" >}}]({{< ref "01-04-类型和变量.md#五预定义类型" >}}))。可是总结起来,每种值类型的默认值都是 0,
bool
型的默认值是false
,引用类型的默认值为null
。
例如,下面的代码声明了 4 个字段,前面两个字段被隐式初始化,另外两个字段被初始化语句显示初始化。
class MyClass { int f1; // 初始化为 0 - 值类型 string f2; // 初始化为 null - 引用类型 int f3 = 25; // 初始化为 25 string f4 = "abcd"; // 初始化为 “abcd” }
2. 声明多个字段
可以通过用逗号分隔名称的方式,在同一条语句中声明多个相同类型的字段。但不能在一个声明中混合不同的类型。例如,可以把之前的 4 个字段声明结合成两条语句,语义结果相同。
int f1,f3 = 25; string f2,f4 = "abcd";
4.2 方法
方法是具有名称的可执行代码块,可以从程序的很多不同地方执行,甚至从其他程序中执行。
当方法被调用(call/invoke)时,它执行自己所含的代码,然后返回到调用它的代码并继续执行调用代码。有些方法返回一个值到它们被调用的位置。方法相当于 C++ 中的成员函数。
声明方法的最简语法包括以下组成部分。
- 返回类型 它声明了方法返回值的类型。如果一个方法不返回值,那么返回类型被指定为
void
。 - 名称 这是方法的名称。
- 参数列表 它至少由一对空的圆括号组成。如果有参数([{{< ref "01-06-方法和参数.md" >}}]({{< ref "01-06-方法和参数.md" >}})),将被列在圆括号中间。
- 方法体 它由一对大括号组成,大括号内包含执行代码。
例如,下面的代码声明了一个类,带有一个名为 PrintNums
的简单方法。从这个声明中可以看出下面几点关于 PrintNums
的情况:
- 它不返回值,因此返回类型指定为
void
; - 它有空的参数列表;
- 它的方法体有两行代码,其中第 1 行打印数字 1,第 2 行打印数字 2。
class SimpleClass { void PrintNums() { Console.WriteLine("1"); Console.WriteLine("2"); } }
说明与 C 和 C++ 不同,C# 中没有全局函数(也就是方法或函数)声明在类型声明的外部。同样,和 C/C++ 不同,C# 中方法没有默认的返回类型。所有方法必须包含返回类型或
void
。
五、创建变量和类的实例
类的声明只是用于创建类的实例的蓝图。一旦类被声明,就可以创建类的实例。
- 类是引用类型,正如你从上一篇([{{< ref "01-04-类型和变量.md#八值类型和引用类型" >}}]({{< ref "01-04-类型和变量.md#八值类型和引用类型" >}}))了解到的,这意味着它们要为数据引用和实际数据都申请内存。
- 数据的引用保存在一个类类型的变量中。所以,要创建类的实例,需要从声明一个类类型的变量开始。如果变量没有被初始化,它的值是未定义的。
下图阐明了如何定义保存引用的变量。左边顶端的代码是类 Dealer
的声明,下面是类 Program
的声明,它包含 Main
方法。Main
声明了 Dealer
类型的变量 theDealer
。因为该变量没有初始化,所以它的值是未定义的。
六、为数据分配内存
声明类类型的变量所分配的内存是用来保存引用的,而不是用来保存类对象实际数据的。要为实际数据分配内存,需要使用 new
运算符。
-
new
运算符为任意指定类型的实例分配并初始化内存。它依据类型的不同,从栈或堆里分配。 -
使用
new
运算符组成的一个对象创建表达式,它的组成如下:-
关键字
new
; -
要分配内存的实例的类型名称;
-
成对的括号,可能包括参数也可能没有参数。
-
-
如果将内存分配给一个引用类型,则对象创建表达式返回一个引用,指向在堆中被分配并初始化的对象实例。
要分配和初始化用于保存类实例数据的内存,需要的工作就是这些。下面是使用 new
运算符创建对象创建表达式,并把它的返回值赋给类变量的一个例子:
Dealer theDealer; // 声明引用变量 theDealer = new Dealer(); // 为类对象分配内存并赋值给变量
下图左边的代码展示了用干分配内存并创建类 Dealer
实例的 new
云算符,随后实例被赋值给类变量。右边的图展示了内存的结构。
合并这两个步骤
可以将这两个步骤合并起来,用对象创建表达式来初始化变量。
Dealer theDealer = new Dealer(); // 声明并初始化
七、实例成员
类声明相当于蓝图,通过这个蓝图想创建多少个类的实例都可以。
- 实例成员 类的每个实例都是不同的实体,它们有自己的一组数据成员,不同于同一类的其他实例。因为这些数据成员都和类的实例相关,所以被称为实例成员。
- 静态成员 实例成员是默认类型,但也可以声明与类而不是实例相关的成员,称为静态成员。
下面的代码是实例成员的示例,展示了有 3 个 Player
类实例的扑克牌程序。下图表明每个实例的 Name
字段都有不同的值。
class Dealer {...} // 声明类 class Player // 声明类 { string Name; // 字段 ... } class Program { static void Main() { Dealer theDealer = new Dealer(); Player player1 = new Player(); Player player2 = new Player(); Player player3 = new Player(); ... } }
八、访问修饰符
从类的内部,任何函数成员都可以使用成员的名称访问类中任意的其他成员。
访问修饰符是成员声明的可选部分,指明程序的其他部分如何访问成员。访问修饰符放在简单声明形式之前。下面是字段和方法声明的语法:
字段
访问修饰符 类型 标识符;
方法
访问修饰符 返回类型 方法名() { ... }
5 种成员访问控制如下。本文将阐述前两种。
- 私有的(private);
- 公有的(public);
- 受保护的(protected);
- 内部的(internal);
- 受保护内部的(protected internal)。
私有访问和公有访问
私有成员只能从声明它的类的内部访问,其他的类看不见或无法访问它们。
- 私有访问是默认的访问级别,所以,如果一个成员在声明时不带访问修饰符,那它就是私有成员。
- 还可以使用
private
访问修饰符显式地将一个成员声明为私有。隐式地声明私有成员和显式地声明在语义上没有不同,两种形式是等价的。
例如,下面的两个声明都指定了 private int
成员:
int MyInt1; // 隐式声明为私有 private int MyInt2; // 显示声明为私有
实例的公有成员可以被程序中的其他对象访问。必须使用 public
访问修饰符指定公有访问。
public int MyInt;
1. 公有访问和私有访问图示
本文中的插图把类表示为标签框,如下图所示。
- 类成员为类框中的小标签框。
- 私有成员完全封闭在它们的类框中。
- 公有成员有一部分伸出它们的类框之外。
2. 成员访问示例
类 C1
声明了公有和私有的字段和方法,下图阐明了类 C1
的成员的可见性。
class C1 { int F1; // 隐式私有字段 private int F2; // 显示私有字段 public int F3; // 公有字段 void DoCalc() // 隐式私有方法 { ... } public int GetVal() // 公有方法 { ... } }
九、从类的内部访问成员
如前所述,类的成员仅用其他类成员的名称就可以访问它们。
例如,下面的类声明展示了类的方法对字段和其他方法的访问。即时字段和两个方法被声明为 private
,类的所有成员还是可以被类的任何方法(或任何函数成员)访问。下图阐明了这段代码。
class DaysTemp { // 字段 private int High = 75; private int Low = 45; // 方法 private int GetHigh() { return High; // 访问私有字段 } private int GetLow() { return Low; // 访问私有字段 } public float Average() { return (GetHigh() + GetLow()) / 2; // 访问私有方法 } }
十、从类的外部访问成员
要从类的外部访问实例成员,必须包括变量名称和成员名称,中间用句点(.)分隔。这称为点运算符(dot-syntax notation),后文会详细讨论。
例如,下面代码的第二行展示了一个从类的外部访问方法的示例:
DaysTemp myDt = new DaysTemp(); // 创建类的对象 float fValue = myDt.Average(); // 从外部访问
举个例子,下面的代码声明了两个类:DaysTemp
和 Program
。
DaysTemp
内的两个字段被声明为public
,所以可以从类的外部访问它们。- 方法
Main
是类Program
的成员。它创建了一个变量和类DaysTemp
的对象,并给对象的字段赋值。然后它读取字段的值并打印出来。
class DaysTemp // 声明类 DaysTemp { public int High = 75; public int Low = 45; } class Program // 声明类 Program { static void Main() { DaysTemp temp = new DaysTemp(); // 创建对象 temp.High = 85; // 字段赋值 temp.Low = 60; Console.WriteLine($"High: {temp.High}"); // 读取字段值 Console.WriteLine($"Low: {temp.Low}"); } }
输出:
High: 85 Low: 60
十一、综合应用
下面的代码创建两个实例并把它们的引用保存在名称为 t1
和 t2
的变量中。下图阐明了内存中的 t1
和 t2
。这段代码示范了目前为止讨论的使用类的 3 种行为:
- 声明一个类;
- 创建类的实例;
- 访问类的成员(也就是写入字段和读取字段)。
class DaysTemp // 声明类 { public int High, Low; // 声明实例字段 public int Average() { return (High + Low) / 2; } } class Program { static void Main() { // 创建两个 DaysTemp 实例 DaysTemp t1 = new DaysTemp(); DaysTemp t2 = new DaysTemp(); // 给字段赋值 t1.High = 76; t1.Low = 57; t2.High = 75; t2.Low = 53; // 读取字段值 // 调用实例的方法 Console.WriteLine($"t1: {t1.High}, {t1.Low}, {t1.Average()}"); Console.WriteLine($"t2: {t2.High}, {t2.Low}, {t2.Average()}"); } }
输出:
t1: 76, 57, 66 t2: 75, 53, 64
原文链接:https://www.dotnetprimer.com/csharp/01-classes-in-csharp
(完)