文章目录
C# 类(Class)
当你定义一个类时,你定义了一个数据类型的蓝图:类的对象由什么组成及在这个对象上可执行什么操作。对象是类的实例,构成类的方法和变量称为类的成员。
1、类的定义
类的定义是以关键字 class 开始,后跟类的名称,类的主体包含在一对花括号内。下面是类定义的一般形式:
<access specifier> class class_name
{
// member variables
<access specifier> <data type> variable1;
<access specifier> <data type> variable2;
...
<access specifier> <data type> variableN;
// member methods
<access specifier> <return type> method1(parameter_list)
{
// method body
}
<access specifier> <return type> method2(parameter_list)
{
// method body
}
...
<access specifier> <return type> methodN(parameter_list)
{
// method body
}
}
请注意:
- 访问标识符
<access specifier>
指定了对类及其成员的访问规则。如果没有指定,则使用默认的访问标识符。类的默认访问标识符是 internal,成员的默认访问标识符是 private。- 数据类型
<data type>
指定了变量的类型,返回类型<return type>
指定了方法返回的数据类型。- 如果要访问类的成员,你要使用点
(.)
运算符。点运算符链接了对象的名称和成员的名称。
下面的实例直观展示了上述的概念:
using System;
namespace BoxApplication
{
class Box
{
public double length; // 长度
public double breadth; // 宽度
public double height; // 高度
}
class Boxtester
{
static void Main(string[] args)
{
Box Box1 = new Box(); // 声明 Box1,类型为 Box
Box Box2 = new Box(); // 声明 Box2,类型为 Box
double volume = 0.0; // 体积
// Box1 详述
Box1.height = 5.0;
Box1.length = 6.0;
Box1.breadth = 7.0;
// Box2 详述
Box2.height = 10.0;
Box2.length = 12.0;
Box2.breadth = 13.0;
// Box1 的体积
volume = Box1.height * Box1.length * Box1.breadth;
Console.WriteLine("Box1 的体积: {0}", volume);
// Box2 的体积
volume = Box2.height * Box2.length * Box2.breadth;
Console.WriteLine("Box2 的体积: {0}", volume);
Console.ReadKey();
}
}
}
当上面的代码被编译和执行时,它会产生下列结果:
Box1 的体积: 210
Box2 的体积: 1560
2、成员函数和封装
类的成员函数是一个在类定义中有它的定义或原型的函数,就像其他变量一样,通常我们也称之为 “方法”。作为类的一个成员,它能在类的任何对象上操作(即类的所有实例对象都能调用成员函数),且能访问该对象的类的所有成员(即在类内部定义的变量、方法等都可以被成员函数访问)。
- 成员变量被称作 “字段”(从设计角度),通过让它们保持私有来实现封装,这些私有变量只能借由内部定义的公共成员函数来访问。
结合上面的概念,改进代码实现私有变量的外部间接访问:
using System;
namespace BoxApplication
{
class Box
{
private double length; // 长度
private double breadth; // 宽度
private double height; // 高度
public void setLength( double len )
{
length = len; //公共成员函数(方法)可以被外部调用传参,再将数据赋给私有变量
}
public void setBreadth( double bre )
{
breadth = bre; //公共成员函数(方法)可以被外部调用传参,再将数据赋给私有变量
}
public void setHeight( double hei )
{
height = hei; //公共成员函数(方法)可以被外部调用传参,再将数据赋给私有变量
}
public double getVolume()
{
return length * breadth * height; //再通过公共成员函数(方法),返回私有变量的计算结果
}
}
class Boxtester
{
static void Main(string[] args)
{
Box Box1 = new Box(); // 声明 Box1,类型为 Box
Box Box2 = new Box(); // 声明 Box2,类型为 Box
double volume; // 体积
// Box1 详述,调用公共方法初始化
Box1.setLength(6.0);
Box1.setBreadth(7.0);
Box1.setHeight(5.0);
// Box2 详述,调用公共方法初始化
Box2.setLength(12.0);
Box2.setBreadth(13.0);
Box2.setHeight(10.0);
// Box1 的体积
volume = Box1.getVolume();
Console.WriteLine("Box1 的体积: {0}" ,volume);
// Box2 的体积
volume = Box2.getVolume();
Console.WriteLine("Box2 的体积: {0}", volume);
Console.ReadKey();
}
}
}
当上面的代码被编译和执行时,它会产生下列结果:
Box1 的体积: 210
Box2 的体积: 1560
3、构造函数
类的构造函数是类的一个特殊的成员函数,当创建类的新实例对象时自动执行。
- 构造函数的名称与类的名称完全相同;
- 它没有任何返回类型。
下面的实例说明了构造函数的概念:
using System;
namespace LineApplication
{
class Line
{
private double length; // 线条的长度
public Line() //构造函数
{
Console.WriteLine("对象已创建");
}
public void setLength( double len )
{
length = len;
}
public double getLength()
{
return length;
}
static void Main(string[] args)
{
Line line = new Line();
// 设置线条长度
line.setLength(6.0);
Console.WriteLine("线条的长度: {0}", line.getLength());
Console.ReadKey();
}
}
}
当上面的代码被编译和执行时,它会产生下列结果:
对象已创建
线条的长度: 6
默认的构造函数没有任何参数。但是如果你需要一个带有参数的构造函数可以有参数,这种构造函数叫做参数化构造函数。这种技术可以帮助你在创建对象的同时给对象赋初始值,具体请看下面实例:
using System;
namespace LineApplication
{
class Line
{
private double length; // 线条的长度
public Line(double len) // 参数化构造函数
{
Console.WriteLine("对象已创建,length = {0}", len);
length = len;
}
public void setLength( double len )
{
length = len;
}
public double getLength()
{
return length;
}
static void Main(string[] args)
{
Line line = new Line(10.0);
Console.WriteLine("线条的长度: {0}", line.getLength());
// 设置线条长度
line.setLength(6.0);
Console.WriteLine("线条的长度: {0}", line.getLength());
Console.ReadKey();
}
}
}
当上面的代码被编译和执行时,它会产生下列结果:
对象已创建,length = 10
线条的长度: 10
线条的长度: 6
4、析构函数
类的析构函数同样是类的一个特殊的成员函数,当类的对象超出范围时执行,例如该对象被删除或者离开了它的作用域。
- 析构函数的名称是在类的名称前加上一个波浪形(~)作为前缀,它不返回值,也不带任何参数。
- 析构函数用于在结束程序(比如关闭文件、释放内存等)之前释放资源。
- 析构函数不能继承或重载。
下面的实例说明了析构函数的概念:
using System;
namespace LineApplication
{
class Line
{
private double length; // 线条的长度
public Line() // 构造函数
{
Console.WriteLine("对象已创建");
}
~Line() // 析构函数
{
Console.WriteLine("对象已删除");
}
public void setLength( double len )
{
length = len;
}
public double getLength()
{
return length;
}
static void Main(string[] args)
{
Line line = new Line();
// 设置线条长度
line.setLength(6.0);
Console.WriteLine("线条的长度: {0}", line.getLength());
}
}
}
当上面的代码被编译和执行时,它会产生下列结果:
对象已创建
线条的长度: 6
对象已删除
5、静态成员
我们可以使用 static 关键字
把类成员定义为静态的。当我们声明一个类成员为静态时,意味着无论有多少个类的对象被创建,只会有一个该静态成员的副本。
- 关键字 static 意味着类中只有一个该成员的实例。静态变量用于定义常量,因为它们的值可以通过类直接调用而不需要创建类的实例来获取。
- 静态变量可在成员函数或类定义的外部进行初始化。
- 你也可以在类的定义内部初始化静态变量。
下面的实例演示了静态变量的用法:
using System;
namespace StaticVarApplication
{
class StaticVar
{
public static int num; //静态公共变量,默认值为0
public void count()
{
num++; //类的内部进行初始化
}
public int getNum()
{
return num;
}
}
class StaticTester
{
static void Main(string[] args)
{
StaticVar s1 = new StaticVar();
StaticVar s2 = new StaticVar();
s1.count();
s1.count();
s1.count();
s2.count();
s2.count();
s2.count();
Console.WriteLine("s1 的变量 num: {0}", s1.getNum());
Console.WriteLine("s2 的变量 num: {0}", s2.getNum());
Console.ReadKey();
}
}
}
当上面的代码被编译和执行时,它会产生下列结果:
s1 的变量 num: 6
s2 的变量 num: 6
你也可以把一个成员函数声明为 static
,这样的函数只能访问静态变量。静态函数在对象被创建之前就已经存在。下面的实例演示了静态函数的用法:
using System;
namespace StaticVarApplication
{
class StaticVar
{
public static int num;
public void count()
{
num++;
}
public static int getNum() //定义静态成员函数(方法),只能访问静态变量
{
return num;
}
}
class StaticTester
{
static void Main(string[] args)
{
StaticVar s = new StaticVar();
s.count();
s.count();
s.count();
Console.WriteLine("变量 num: {0}", StaticVar.getNum());
Console.ReadKey();
}
}
}
当上面的代码被编译和执行时,它会产生下列结果:
变量 num: 3
静态方法通常用于初始化操作,因为静态方法之间是无法传递数据的。把成员函数(方法)比作工具,当我们不是只需要经过一个工具加工,而是需要在几个工具之间传递数据然后去处理,这个时候就需要用到非静态成员,即动态成员。
6、继承
继承是面向对象程序设计中最重要的概念之一。继承允许我们根据一个类来定义另一个类,这使得创建和维护应用程序变得更容易,同时也有利于重用代码和节省开发时间。
- 继承的语法:
class 子类类名 : class 父类类名{ //子类类体 }
- 继承的特点:子类拥有所有父类中所有的字段、属性和方法
- 一个类可以有多个子类,但是父类只能有一个
- 一个类在继承另一个类的同时,还可以被其他类继承
- 在 C# 中,所有的类都直接或者间接的继承自 Object 类
- 当创建一个类时,程序员不需要完全重新编写新的数据成员和成员函数,只需要设计一个新的类,继承了已有的类的成员即可。这个已有的类被称为的基类,这个新的类被称为派生类。
- 继承的思想实现了属于(IS-A)关系。例如,哺乳动物属于(IS-A)动物,而狗属于(IS-A) 哺乳动物,因此狗属于(IS-A)动物。
6.1 基类和派生类
一个类可以派生自多个类或接口(即有多个父类),这意味着它可以从多个基类或接口继承数据和函数。
C# 中创建派生类的语法如下:
<访问修饰符> class <基类>
{
...
}
class <派生类> : <基类>
{
...
}
假设,有一个基类 Shape,它的派生类是 Rectangle:
using System;
namespace InheritanceApplication
{
class Shape
{
public void setWidth(int w)
{
width = w;
}
public void setHeight(int h)
{
height = h;
}
protected int width; //变量可以被当前类及其子类访问
protected int height; //变量可以被当前类及其子类访问
}
// 派生类
class Rectangle: Shape
{
public int getArea()
{
return (width * height); //继承自父类(基类)的变量
}
}
class RectangleTester
{
static void Main(string[] args)
{
Rectangle Rect = new Rectangle(); //实例化一个派生类Rectangle对象
Rect.setWidth(5); //该对象可以调用基类Shape的实例方法
Rect.setHeight(7); //该对象可以调用基类Shape的实例方法
// 打印对象的面积
Console.WriteLine("总面积: {0}", Rect.getArea()); //该对象可以调用自身的实例方法
Console.ReadKey();
}
}
}
当上面的代码被编译和执行时,它会产生下列结果:
总面积: 35
6.2 基类的初始化
派生类继承了基类的成员变量和成员方法,因此父类对象应在子类对象创建之前被创建。我们可以在子类的成员初始化列表中进行父类的初始化。
下面的程序演示了这点:
using System;
namespace RectangleApplication
{
// 定义矩形类
class Rectangle
{
// 成员变量:矩形的长度和宽度,被保护修饰符修饰
protected double length;
protected double width;
// 构造函数,用于初始化矩形的长度和宽度
public Rectangle(double l, double w)
{
length = l;
width = w;
}
// 成员方法:计算矩形的面积
public double GetArea()
{
return length * width;
}
// 成员方法:显示矩形的信息,包括长度、宽度、面积
public void Display()
{
Console.WriteLine("长度: {0}", length);
Console.WriteLine("宽度: {0}", width);
Console.WriteLine("面积: {0}", GetArea());
}
}//end class Rectangle
// 定义继承自矩形类的桌面类
class Tabletop : Rectangle
{
// 成员变量:桌面的成本
private double cost;
// 构造函数,用于初始化桌面的长度和宽度,继承了基类构造函数来初始化矩形的长度和宽度
public Tabletop(double l, double w) : base(l, w)
{ }
// 成员方法:计算桌面的成本
public double GetCost()
{
double cost;
cost = GetArea() * 70;
return cost;
}
// 成员方法:显示桌面的信息,包括长度、宽度、面积和成本
public void Display()
{
base.Display(); // 调用基类的Display方法,显示矩形的信息
Console.WriteLine("成本: {0}", GetCost());
}
}
// 执行类,包含Main方法
class ExecuteRectangle
{
static void Main(string[] args)
{
// 创建桌面对象,传入长度和宽度
Tabletop t = new Tabletop(4.5, 7.5);
// 显示桌面的信息,包括长度、宽度、面积和成本
t.Display();
Console.ReadLine();
}
}
}
因为构造函数与类时同名的,所以在派生类的构造函数处通过base(l, w)
这一接口就可以继承基类的构造函数。当上面的代码被编译和执行时,它会产生下列结果:
长度: 4.5
宽度: 7.5
面积: 33.75
成本: 2362.5
6.3 多重继承
多重继承指的是一个类别可以同时从多于一个父类继承行为与特征的功能。与单一继承相对,单一继承指一个类别只可以继承自一个父类。
C# 不支持多重继承,但是我们可以使用接口来实现多重继承,定义接口需要使用到关键字interface
。下面的程序演示了这点:
using System;
// 命名空间
namespace InheritanceApplication
{
// 定义 Shape 类
class Shape
{
// 公共方法,用于设置宽度
public void setWidth(int w)
{
width = w;
}
// 公共方法,用于设置高度
public void setHeight(int h)
{
height = h;
}
// 成员变量,用于存储宽度和高度
protected int width;
protected int height;
}
// 定义 PaintCost 接口
public interface PaintCost
{
// 规定要实现一个方法,用于计算涂漆成本
int getCost(int area);
}
// 定义 Rectangle 类,继承自 Shape 类,并实现 PaintCost 接口
class Rectangle : Shape, PaintCost
{
// 计算矩形的面积
public int getArea()
{
return (width * height);
}
// 计算涂漆成本
public int getCost(int area)
{
return area * 70;
}
}
// 定义执行类 RectangleTester
class RectangleTester
{
// 主函数
static void Main(string[] args)
{
// 创建 Rectangle 对象
Rectangle Rect = new Rectangle();
// 定义变量 area
int area;
// 设置宽度和高度
Rect.setWidth(5);
Rect.setHeight(7);
// 计算矩形的面积
area = Rect.getArea();
// 输出矩形的面积和涂漆成本
Console.WriteLine("总面积: {0}", Rect.getArea());
Console.WriteLine("油漆总成本: ${0}", Rect.getCost(area)); //调用继承自接口的方法
// 等待用户按下一个键
Console.ReadKey();
}
}
}
当上面的代码被编译和执行时,它会产生下列结果:
总面积: 35
油漆总成本: $2450
C# 接口(Interface)
接口定义了所有类继承接口时应遵循的语法合同。接口定义了语法合同 “是什么” 部分,派生类定义了语法合同 “怎么做” 部分。
- 接口定义了属性、方法和事件,这些都是接口的成员。
- 接口只包含了成员的声明,而成员的定义是派生类的责任,接口只是提供了派生类应遵循的标准结构。
- 接口使得实现接口的类或结构在形式上保持一致。
抽象类在某种程度上与接口类似,都是用于描述行为或功能的概念,但它们在定义上存在一些区别:
- 成员的实现方式:抽象类可以定义成员的实现,也可以定义抽象成员,而接口只能定义抽象成员。
- 实现的自由度:子类继承抽象类时,可以选择性地实现抽象类中的成员;而实现接口时,必须实现接口中定义的所有成员。
- 构造函数:抽象类可以有构造函数,而接口没有。
- 多重继承:类只能继承一个抽象类,但可以实现多个接口。
- 关系类型:抽象类和子类之间属于继承关系,而接口和实现类之间属于实现关系。
- 功能定位:抽象类主要用于将相关的类分组,并提供一个通用的抽象类定义,而接口主要用于实现多态性和解耦。
1、定义接口
接口使用 interface
关键字声明,它与类的声明类似。接口声明默认是 public
的,接口成员可以不指定访问修饰符,但是不能指定 public
以外的访问修饰符。下面是一个接口声明的实例:
interface IMyInterface
{
void MethodToImplement();
}
以上代码定义了接口 IMyInterface
。通常接口命令以 I 字母开头,这个接口只有一个方法MethodToImplement()
,没有参数和返回值,当然我们可以按照需求设置参数和返回值。
2、实现接口
接口函数和抽象类一样,是不能进行实例化的,需要使用派生类继承然后实现。上面我们定义的接口是还没有具体实现的,接下来我们进一步实现这个接口:
using System;
interface IMyInterface
{
// 接口成员
void MethodToImplement();
}
class InterfaceImplementer : IMyInterface // 继承接口,注意这里虽然叫继承,但本质是实现关系
{
static void Main()
{
InterfaceImplementer iImp = new InterfaceImplementer();
iImp.MethodToImplement();
}
// 接口的实现
public void MethodToImplement()
{
Console.WriteLine("MethodToImplement() called.");
}
}
InterfaceImplementer
类实现了 IMyInterface
接口,接口的实现与类的继承语法格式类似:class InterfaceImplementer : IMyInterface
。继承接口后,我们需要实现接口的方法 MethodToImplement()
,派生类中的方法名必须与接口定义的方法名一致。
3、接口间继承
这里说的继承不是派生类或结构体继承接口,而是指一个接口继承其他接口。此时负责实现接口的类或结构就需要实现所有接口的成员。
以下实例中,IMyInterface
接口继承了 IParentInterface
接口,因此接口实现类 InterfaceImplementer
必须实现 MethodToImplement()
和 ParentInterfaceMethod()
方法:
using System;
// 基类接口
interface IParentInterface
{
void ParentInterfaceMethod();
}
// 派生接口
interface IMyInterface : IParentInterface
{
void MethodToImplement();
}
// 派生类相当于继承了全部的接口“协议”,两个接口的成员都要实现
class InterfaceImplementer : IMyInterface
{
static void Main()
{
InterfaceImplementer iImp = new InterfaceImplementer();
iImp.MethodToImplement();
iImp.ParentInterfaceMethod();
}
// 实习基类接口成员
public void MethodToImplement()
{
Console.WriteLine("MethodToImplement() called.");
}
// 实习派生类接口成员
public void ParentInterfaceMethod()
{
Console.WriteLine("ParentInterfaceMethod() called.");
}
}
实例输出结果为:
MethodToImplement() called.
ParentInterfaceMethod() called.