【C#零基础从入门到精通】(二十二)——C#类和对象详解
1. 类和对象的基本概念
1.1 类(Class)
类是一种用户自定义的数据类型,它是对现实世界中一类具有相同属性和行为的事物的抽象描述。可以把类看作是创建对象的模板或蓝图。类定义了对象所具有的属性(数据成员)和行为(方法成员)。
例如,我们要描述 “汽车” 这个概念,汽车有颜色、品牌、型号等属性,还有启动、行驶、刹车等行为。在 C# 中可以这样定义一个 Car
类:
class Car
{
// 属性
public string Color;
public string Brand;
public string Model;
// 方法
public void Start()
{
Console.WriteLine($"{Brand} {Model} 已启动");
}
public void Drive()
{
Console.WriteLine($"{Brand} {Model} 正在行驶");
}
public void Brake()
{
Console.WriteLine($"{Brand} {Model} 已刹车");
}
}
在上述代码中,Car
类包含了三个属性(Color
、Brand
、Model
)和三个方法(Start
、Drive
、Brake
)。
1.2 对象(Object)
对象是类的实例。类定义了对象的通用结构和行为,而对象则是具体的实体,具有类所定义的属性和行为。可以通过类来创建多个不同的对象,每个对象都有自己独立的属性值。
例如,我们可以根据 Car
类创建两辆不同的汽车对象:
class Program
{
static void Main()
{
// 创建第一个汽车对象
Car car1 = new Car();
car1.Color = "红色";
car1.Brand = "宝马";
car1.Model = "X5";
// 创建第二个汽车对象
Car car2 = new Car();
car2.Color = "蓝色";
car2.Brand = "奔驰";
car2.Model = "C级";
// 调用对象的方法
car1.Start();
car2.Drive();
}
}
在这个例子中,car1
和 car2
是 Car
类的两个不同对象,它们各自有不同的属性值,并且可以独立地调用类中定义的方法。
2. 类的定义和成员
2.1 类的定义语法
在 C# 中,类的定义使用 class
关键字,语法如下:
[访问修饰符] class 类名
{
// 类的成员(属性、方法等)
}
其中,访问修饰符是可选的,常见的访问修饰符有 public
、private
、protected
等,用于控制类或类成员的访问权限。
2.2 类的成员
- 字段(Field):字段是类中用于存储数据的变量,也称为类的属性。例如,在
Car
类中的Color
、Brand
、Model
就是字段。字段可以有不同的数据类型,并且可以使用访问修饰符来控制其访问权限。
class Car
{
public string Color; // 公共字段
private int speed; // 私有字段
}
- 属性(Property):属性是一种特殊的成员,它提供了对字段的访问和修改的控制。属性可以包含
get
和set
访问器,用于获取和设置字段的值。
class Car
{
private int speed;
public int Speed
{
get { return speed; }
set
{
if (value >= 0)
{
speed = value;
}
else
{
Console.WriteLine("速度不能为负数");
}
}
}
}
在上述代码中,Speed
是一个属性,通过 get
访问器可以获取 speed
字段的值,通过 set
访问器可以设置 speed
字段的值,并且在设置时进行了合法性检查。
- 方法(Method):方法是类中用于执行特定操作的代码块,它定义了类的行为。方法可以有参数和返回值,也可以没有。
class Car
{
public void Accelerate(int increment)
{
// 假设这里有一个 Speed 属性
Speed += increment;
Console.WriteLine($"汽车加速到 {Speed} km/h");
}
}
- 构造函数(Constructor):构造函数是一种特殊的方法,用于在创建对象时初始化对象的状态。构造函数的名称与类名相同,并且没有返回类型。
class Car
{
public string Color;
public string Brand;
public string Model;
public Car(string color, string brand, string model)
{
Color = color;
Brand = brand;
Model = model;
}
}
在上述代码中,定义了一个带有三个参数的构造函数,在创建 Car
对象时需要传入相应的参数来初始化对象的属性。
3. 对象的创建和使用
3.1 创建对象
使用 new
关键字来创建类的对象,语法如下:
类名 对象名 = new 类名([参数列表]);
如果类有构造函数,需要在 new
后面的括号中传入相应的参数。例如:
Car car = new Car("白色", "奥迪", "A6");
3.2 访问对象的成员
使用点号(.
)来访问对象的属性和调用对象的方法。例如:
class Program
{
static void Main()
{
Car car = new Car("白色", "奥迪", "A6");
Console.WriteLine($"汽车颜色: {car.Color}");
car.Accelerate(20);
}
}
4. 类和对象的高级特性
4.1 继承(Inheritance)
继承是面向对象编程的一个重要特性,它允许一个类(子类)继承另一个类(父类)的属性和方法。子类可以扩展或重写父类的功能。
// 父类
class Vehicle
{
public string Brand;
public void Move()
{
Console.WriteLine($"{Brand} 正在移动");
}
}
// 子类
class Car : Vehicle
{
public int Wheels = 4;
public new void Move()
{
Console.WriteLine($"{Brand} 汽车正在行驶");
}
}
在上述代码中,Car
类继承自 Vehicle
类,它不仅拥有 Vehicle
类的 Brand
属性和 Move
方法,还添加了自己的 Wheels
属性,并且重写了 Move
方法。
4.2 多态(Polymorphism)
多态允许不同的对象对同一消息做出不同的响应。通过继承和方法重写可以实现多态性。
class Program
{
static void Main()
{
Vehicle vehicle = new Car();
vehicle.Brand = "宝马";
vehicle.Move(); // 输出 "宝马 汽车正在行驶"
}
}
在这个例子中,虽然 vehicle
变量的类型是 Vehicle
,但它实际引用的是 Car
对象,调用 Move
方法时会执行 Car
类中重写的 Move
方法。
4.3 封装(Encapsulation)
封装是指将数据和操作数据的方法捆绑在一起,并隐藏对象的内部实现细节,只对外提供必要的接口。通过使用访问修饰符(如 private
、public
等)可以实现封装。
class BankAccount
{
private double balance;
public void Deposit(double amount)
{
if (amount > 0)
{
balance += amount;
Console.WriteLine($"存入 {amount} 元,当前余额: {balance} 元");
}
else
{
Console.WriteLine("存入金额必须大于 0");
}
}
public void Withdraw(double amount)
{
if (amount > 0 && amount <= balance)
{
balance -= amount;
Console.WriteLine($"取出 {amount} 元,当前余额: {balance} 元");
}
else
{
Console.WriteLine("取出金额无效或余额不足");
}
}
}
在上述代码中,balance
字段被声明为 private
,外部代码无法直接访问,只能通过 Deposit
和 Withdraw
方法来操作余额,这样就实现了对数据的封装。
5. 类和对象的应用场景
5.1 模拟现实世界的实体
类和对象可以用来模拟现实世界中的各种实体,如人、动物、车辆等。通过定义类的属性和方法,可以准确地描述这些实体的特征和行为。
5.2 构建软件系统的模块
在软件开发中,类和对象可以作为软件系统的基本模块,通过组合和交互来实现复杂的功能。例如,在一个电商系统中,可以定义 Product
类表示商品,Order
类表示订单,Customer
类表示顾客等。
5.3 实现代码的复用和可维护性
通过继承和多态等特性,类和对象可以提高代码的复用性和可维护性。当需要扩展或修改系统功能时,可以通过创建新的子类或重写方法来实现,而不需要修改大量的现有代码。
类的构造函数和析构函数详解
在 C# 中,类的构造函数和析构函数是两个非常重要的概念,它们分别用于对象的初始化和资源清理工作。以下是对它们的详细介绍。
构造函数
定义与作用
构造函数是类中的特殊方法,其名称与类名相同,主要用于在创建对象时初始化对象的状态,为对象的成员变量赋初值。构造函数在使用 new
关键字创建对象时自动调用。
语法
[访问修饰符] 类名([参数列表])
{
// 构造函数的主体,用于初始化对象
}
访问修饰符通常为 public
,这样外部代码才能创建该类的对象。参数列表是可选的,根据需要可以有零个或多个参数。
1、实例构造函数
class Person
{
public string Name;
public int Age;
// 无参构造函数
public Person()
{
Name = "未知";
Age = 0;
}
// 带参构造函数
public Person(string name, int age)
{
Name = name;
Age = age;
}
}
class Program
{
static void Main()
{
// 使用无参构造函数创建对象
Person person1 = new Person();
Console.WriteLine($"姓名: {person1.Name}, 年龄: {person1.Age}");
// 使用带参构造函数创建对象
Person person2 = new Person("张三", 25);
Console.WriteLine($"姓名: {person2.Name}, 年龄: {person2.Age}");
}
}
在上述示例中,Person
类有两个构造函数:一个是无参构造函数,用于将 Name
初始化为 “未知”,Age
初始化为 0;另一个是带参构造函数,接受 name
和 age
作为参数,并将它们赋值给对象的成员变量。
2、静态构造函数
静态构造函数用于初始化类中的静态数据或执行仅需执行一次的特定操作。静态构造函数将在创建第一个实例或引用类中的静态成员之前自动调用。
静态构造函数具有以下特性:
- 静态构造函数不使用访问权限修饰符修饰或不具有参数;
- 类或结构体中只能具有一个静态构造函数;
- 静态构造函数不能继承或重载;
- 静态构造函数不能直接调用,仅可以由公共语言运行时 (CLR) 调用;
- 用户无法控制程序中静态构造函数的执行时间;
- 在创建第一个实例或引用任何静态成员之前,将自动调用静态构造函数以初始化类;
- 静态构造函数会在实例构造函数之前运行。
【实例】下面通过一个示例来演示实例构造函数与静态构造函数:
using System;
namespace c.biancheng.net
{
class Demo
{
public static int num = 0;
// 构造函数
Demo(){
num = 1;
}
// 静态构造函数
static Demo(){
num = 2;
}
static void Main(string[] args)
{
Console.WriteLine("num = {0}", num);
Demo Obj = new Demo();
Console.WriteLine("num = {0}", num);
Console.Read();
}
}
}
当执行上面程序时,会首先执行public static int num = 0,接着执行类中的静态构造函数,此时 num = 2,然后执行 Main 函数里面的内容,此时打印 num 的值为 2,接着初始化 Demo 类,这时会执行类中的构造函数,此时 num 会重新赋值为 1,所以上例的运行结果如下所示:
num = 2
num = 1
3、私有构造函数
私有构造函数是一种特殊的实例构造函数,通常用在只包含静态成员的类中。如果一个类中具有一个或多个私有构造函数而没有公共构造函数的话,那么其他类(除嵌套类外)则无法创建该类的实例。 例如:
class NLog
{
// 私有构造函数
private NLog() { }
public static double e = Math.E; //2.71828...
}
上例中定义了一个空的私有构造函数,这么做的好处就是空构造函数可阻止自动生成无参数构造函数。需要注意的是,如果不对构造函数使用访问权限修饰符,则默认它为私有构造函数。
【示例】下面通过一个示例来演示私有构造函数的使用:
using System;
namespace c.biancheng.net
{
class Demo
{
static void Main(string[] args)
{
// Student stu = new Student();
Student.id = 101;
Student.name = "张三";
Student.Display();
Console.Read();
}
}
public class Student
{
private Student() { }
public static int id;
public static string name;
public static void Display()
{
Console.WriteLine("姓名:"+name+" 编号:"+id);
}
}
}
运行结果如下:
姓名:张三 编号:101
注意,上述代码中,如果取消 Main 函数中注释的Student stu = new Student();,程序就会出错,因为 Student 类的构造函数是私有静态函数,受其保护级别的限制不能访问。
构造函数的特点
- 名称与类名相同:这是构造函数的显著特征,编译器通过名称来识别构造函数。
- 无返回类型:构造函数不声明返回类型,包括
void
也不能声明。 - 可重载:一个类可以有多个构造函数,只要它们的参数列表不同,这就是构造函数的重载。重载构造函数提供了多种初始化对象的方式。
默认构造函数
如果一个类没有显式定义任何构造函数,编译器会自动为该类提供一个无参的默认构造函数,该构造函数会将对象的成员变量初始化为默认值(例如,数值类型初始化为 0,引用类型初始化为 null
)。但一旦类中显式定义了构造函数,编译器就不会再提供默认构造函数。
析构函数
定义与作用
析构函数也是类中的特殊方法,用于在对象被垃圾回收之前执行一些清理工作,主要用于释放对象占用的非托管资源,如文件句柄、数据库连接、网络连接等。
语法
~类名()
{
// 析构函数的主体,用于释放资源
}
析构函数的名称是类名前加波浪号 ~
,它没有参数,也没有访问修饰符。
示例
class FileHandler
{
private System.IO.FileStream fileStream;
public FileHandler(string filePath)
{
fileStream = new System.IO.FileStream(filePath, System.IO.FileMode.Open);
Console.WriteLine("文件已打开");
}
~FileHandler()
{
if (fileStream != null)
{
fileStream.Close();
Console.WriteLine("文件已关闭");
}
}
}
class Program
{
static void Main()
{
FileHandler fileHandler = new FileHandler("test.txt");
// 模拟一些操作
}
}
在上述示例中,FileHandler
类的构造函数打开一个文件流,析构函数在对象被垃圾回收时关闭该文件流。
析构函数的特点
- 调用时机不确定:析构函数由垃圾回收器(GC)自动调用,垃圾回收器的工作机制是基于内存管理的,它会在系统需要回收内存时自动运行,因此析构函数的调用时机是不确定的,不能保证在对象不再使用后立即调用。
- 不能被继承:每个类的析构函数都是独立的,子类不会继承父类的析构函数。
- 不能有参数和访问修饰符:析构函数的签名是固定的,不能添加参数或指定访问修饰符。
与 IDisposable
接口结合使用
由于析构函数调用时机不确定,为了更及时地释放资源,C# 引入了 IDisposable
接口。该接口定义了一个 Dispose
方法,程序员可以在该方法中编写释放资源的代码,然后使用 using
语句或显式调用 Dispose
方法来确保资源在使用完毕后立即释放。
using System;
class FileHandler : IDisposable
{
private System.IO.FileStream fileStream;
private bool disposed = false;
public FileHandler(string filePath)
{
fileStream = new System.IO.FileStream(filePath, System.IO.FileMode.Open);
Console.WriteLine("文件已打开");
}
public void Dispose()
{
Dispose(true);
// 告诉垃圾回收器不再调用析构函数
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
// 释放托管资源
}
if (fileStream != null)
{
fileStream.Close();
Console.WriteLine("文件已关闭");
}
disposed = true;
}
}
~FileHandler()
{
Dispose(false);
}
}
class Program
{
static void Main()
{
using (FileHandler fileHandler = new FileHandler("test.txt"))
{
// 使用文件资源
}
// 文件资源已在 using 块结束时释放
}
}
在上述示例中,FileHandler
类实现了 IDisposable
接口,在 Dispose
方法中调用 Dispose(bool disposing)
方法进行资源释放,析构函数调用 Dispose(false)
只释放非托管资源,GC.SuppressFinalize(this)
方法告诉垃圾回收器不再调用析构函数。
综上所述,构造函数用于对象的初始化,析构函数用于对象的资源清理,在实际开发中要合理使用它们来确保对象的正确创建和资源的有效管理。
C#镶嵌类和密封类
在 C# 中,嵌套类和密封类是两种不同的类类型,它们分别有着独特的定义、特性和使用场景,下面将为你详细介绍。
嵌套类
定义
嵌套类是指在一个类的内部定义另一个类。外部的类称为外部类,内部定义的类称为嵌套类。嵌套类可以访问其外部类的所有成员(包括私有成员),但外部类需要通过嵌套类的实例来访问嵌套类的成员。
语法示例
class OuterClass
{
private int outerField = 10;
// 嵌套类
public class NestedClass
{
public void DisplayOuterField(OuterClass outer)
{
// 嵌套类可以访问外部类的私有成员
Console.WriteLine($"Outer class field value: {outer.outerField}");
}
}
}
class Program
{
static void Main()
{
OuterClass outer = new OuterClass();
OuterClass.NestedClass nested = new OuterClass.NestedClass();
nested.DisplayOuterField(outer);
}
}
特性
- 访问权限:嵌套类可以具有自己的访问修饰符(如
public
、private
、protected
等),用于控制它在外部的可见性。如果嵌套类被声明为private
,则只有外部类可以访问它。 - 作用域:嵌套类的作用域限定在外部类内部,它可以直接使用外部类的成员,因为它隐式地包含了对外部类实例的引用(如果需要)。
- 逻辑封装:嵌套类可以将相关的功能和数据封装在外部类内部,提高代码的内聚性和可维护性。例如,在一个表示数据库连接的类中,可以嵌套一个表示数据库命令的类。
使用场景
- 实现细节隐藏:当某个类的实现细节只与另一个类相关时,可以将其定义为嵌套类,这样可以避免这些细节暴露给外部。例如,在一个图形绘制类中,将具体的绘制算法类定义为嵌套类。
- 紧密关联的功能:如果两个类之间有非常紧密的关联,一个类的功能依赖于另一个类的内部状态,那么可以将其中一个类定义为另一个类的嵌套类。
密封类
定义
密封类是使用 sealed
关键字修饰的类,它不能被其他类继承。密封类的主要目的是防止其他类对其进行扩展,确保类的实现细节不被修改。
语法示例
sealed class SealedClass
{
public void DoSomething()
{
Console.WriteLine("Doing something...");
}
}
// 以下代码会编译错误,因为 SealedClass 是密封类,不能被继承
// class DerivedClass : SealedClass
// {
// }
class Program
{
static void Main()
{
SealedClass sealedObj = new SealedClass();
sealedObj.DoSomething();
}
}
特性
- 防止继承:密封类不能作为基类被其他类继承,这有助于保持类的完整性和安全性,避免派生类对其行为进行意外修改。
- 性能优化:在某些情况下,密封类可以提供一定的性能优化。因为编译器知道密封类不会有派生类,所以可以进行一些额外的优化。
使用场景
- 安全考虑:当一个类的实现不希望被其他类修改或扩展时,可以将其定义为密封类。例如,一些系统级的类,为了保证系统的稳定性和安全性,可能会被设计为密封类。
- 性能优化:对于一些频繁使用且不需要继承的类,可以将其定义为密封类,以提高性能。例如,
System.String
类就是一个密封类,这样可以避免派生类对字符串处理逻辑的干扰,同时也便于编译器进行优化。
综上所述,嵌套类主要用于代码的组织和封装,而密封类主要用于限制类的继承,它们在不同的场景下发挥着重要的作用。