C#基础07-类与对象
1、类和对象
(1)基本定义
| 概念 |
说明 |
核心特性 |
| 类 (Class) |
用户自定义的数据类型,描述具有相同属性和行为的实体模板 |
- 封装状态(字段)和行为(方法) - 支持继承、多态 |
| 对象 (Object) |
类的具体实例,占用独立内存空间 |
- 通过 new 关键字实例化 - 每个对象拥有独立的字段值 |
- 类比:
- 类 = 汽车设计蓝图(定义颜色、型号、引擎等属性)
- 对象 = 根据蓝图生产的实际汽车(如车牌 A123 的红色轿车)
(2)类的组成成员
| 成员 |
作用 |
示例 |
| 字段 (Field) |
存储对象状态数据 |
private string _name; |
| 属性 (Property) |
封装字段访问逻辑(get/set) |
public string Name { get => _name; set => _name = value; } |
| 方法 (Method) |
定义对象行为 |
public void StartEngine() { ... } |
| 构造函数 |
初始化对象状态 |
public Car(string model) { Model = model; } |
| 事件 (Event) |
实现对象间通信 |
public event Action EngineStarted; |
| 索引器 |
使对象支持数组式访问 |
public string this[int index] { get; set; } |
public const double PI = 3.14; // 类级别常量
- 静态成员 (static):类级别共享(无需实例化)
public static int CarCount; // 统计所有实例数量
️(3)对象的生命周期
- 实例化:使用
new 关键字分配堆内存并调用构造函数:
Car myCar = new Car("Tesla Model 3");
public class Car {
public Car(string model) => Model = model; // 构造初始化
}
- 销毁:依赖垃圾回收器(GC)自动管理内存,可通过析构函数释放资源:
~Car() {
Console.WriteLine("Car object destroyed");
}
2、构造函数
(1)本质与特性
- 定义:构造函数是与类同名、无返回类型的特殊方法,用于初始化对象状态。在
new 实例化时自动调用。
class Person {
public string Name;
// 构造函数
public Person(string name) {
this.Name = name; // 初始化成员
}
}
- 核心特性
- 强制命名规则:必须与类名完全相同。
- 无返回值:不能使用
void 或其他返回类型。
- 自动调用:仅通过
new 触发,不可显式调用。
- 隐式默认构造:未定义时编译器自动生成无参构造;若自定义任意构造,则默认构造失效。
(2)类型与使用场景
- 实例构造函数
- 作用:初始化实例成员变量。
- 重载支持:参数列表不同的多个构造方法。
class Car {
public string Model;
public int Year;
// 无参构造
public Car() { Model = "Unknown"; }
// 带参构造
public Car(string model, int year) {
Model = model;
Year = year;
}
}
- 静态构造函数
- 作用:初始化静态成员,在类首次加载时执行(首次实例化或访问静态成员前)。
- 特性:
- 用
static 修饰,无访问修饰符(隐式私有)。
- 每个类仅允许一个,且无参数。
class Logger {
public static string LogPath;
static Logger() {
LogPath = @"C:\logs\app.log"; // 初始化静态变量
}
}
- 私有构造函数
- 作用:禁止类实例化,适用于工具类或单例模式。
- 示例:
public class Utility {
private Utility() { } // 外部无法 new
public static void Print() { ... }
}
(3)调用链与继承
- 调用同级构造:
this(),复用当前类的其他构造逻辑
class Rectangle {
public int Width, Height;
public Rectangle() : this(10, 5) { } // 调用带参构造
public Rectangle(int w, int h) {
Width = w; Height = h;
}
}
- 调用基类构造:
base(),子类必须通过 base 显式调用父类构造(若父类无默认构造):
class Vehicle {
public int Speed;
public Vehicle(int speed) { Speed = speed; }
}
class Car : Vehicle {
public Car(int speed) : base(speed) { } // 必须传递参数
}
(4)关键注意事项
- 子类构造规则
- 未显式调用
base 时,编译器自动尝试调用父类无参构造;若无则报错。
- 执行顺序:父类构造 → 子类构造。
- 避免常见错误
- 自定义带参构造后,需显式添加无参构造(否则子类可能报错)。
- 静态构造中避免耗时操作(影响首次加载性能)。
- 性能优化
- 减少构造函数的重复初始化逻辑,用
this() 复用代码。
- 值类型(如
struct)的默认构造由编译器自动生成,无需定义。
(5)应用场景示例
// 单例模式(私有构造 + 静态实例)
public class AppConfig {
private static AppConfig _instance;
public static AppConfig Instance => _instance ??= new AppConfig();
private AppConfig() { } // 禁止外部实例化
}
// 带继承链的构造调用
class Animal {
public string Type;
public Animal(string type) => Type = type;
}
class Dog : Animal {
public string Breed;
public Dog(string breed) : base("Mammal") {
Breed = breed;
}
}
(6)最佳实践
- 工具类设计 → 私有构造
- 全局配置初始化 → 静态构造
- 对象复用 → 构造链
this()
- 跨层初始化 → 显式
base()
3、析构函数
(1)本质与作用
- 析构函数是类的特殊成员函数,用于对象销毁前执行资源清理(如关闭文件、释放非托管资源)。
- 语法:
~类名(),无参数与返回类型(如 ~MyClass() { ... })。
(2)与构造函数的对比
| 特性 |
构造函数 |
析构函数 |
| 执行时机 |
对象创建时自动调用 |
对象销毁时自动调用 |
| 命名 |
与类名相同 |
类名前加 ~ |
| 数量限制 |
可重载(多个) |
仅允许一个 |
| 调用控制 |
可通过 new 触发 |
完全由垃圾回收器(GC)控制 |
(3)关键限制与特性
- 使用约束
- 仅适用于类(不适用于结构体)。
- 不可继承或重载,且不能被显式调用。
- 禁止添加访问修饰符(如
public)或参数。
- 执行机制
- 由垃圾回收器(GC)在对象不可达时自动触发,具体时机不可预测。
- 程序退出时,所有未析构的对象会按继承链反向顺序调用析构函数(子类→父类)。
- 示例:继承链析构顺序
class A { ~A() => Console.WriteLine("A destroyed"); }
class B : A { ~B() => Console.WriteLine("B destroyed"); }
// 输出:B destroyed → A destroyed
(4)适用场景与替代方案
- 核心用途
- 非托管资源清理(如文件句柄、网络连接),作为资源释放的保底机制。
- 需与
IDisposable 接口配合使用,避免资源泄露。
- 替代方案:
IDisposable 模式
- 优先通过
Dispose() 方法主动释放资源,而非依赖析构函数。
- 典型实现:
class Resource : IDisposable {
private bool disposed = false;
public void Dispose() {
Dispose(true);
GC.SuppressFinalize(this); // 阻止析构函数重复调用
}
protected virtual void Dispose(bool disposing) {
if (!disposed) {
if (disposing) { /* 释放托管资源 */ }
/* 释放非托管资源 */
disposed = true;
}
}
~Resource() => Dispose(false); // 析构函数兜底
}
(5)性能注意事项
- 避免空析构函数
- 空析构函数会导致 GC 额外开销(将对象加入终止队列),降低性能。
- 慎用强制 GC
- 调用
GC.Collect() 强制回收可能引发性能问题,仅在必要时使用。
(6)最佳实践
| 场景 |
推荐方案 |
原因 |
| 非托管资源释放 |
IDisposable + 析构函数兜底 |
确保资源安全释放 |
| 纯托管资源 |
无需析构函数 |
GC 自动管理,析构函数反而降低效率 |
| 需要确定性释放 |
实现 Dispose() |
主动控制释放时机 |
| 资源释放逻辑复杂 |
析构函数仅调用 Dispose(false) |
分离托管/非托管资源处理 |
- 关键原则:析构函数应作为资源清理的最后防线,而非主要手段。优先使用
IDisposable 模式确保及时释放资源。
(7)进阶说明
- 与
Finalize() 的关系:C# 析构函数编译后会转换为 Finalize() 方法,由 GC 调用。
- 结构体限制:结构体不支持析构函数,因其为值类型且由栈管理,无需终结。
4、字段
(1)定义与本质
- 基本概念
- 字段是类或结构体内部的数据存储单元,本质是成员变量。
- 声明语法:
访问修饰符 数据类型 字段名;
private int _age; // 私有整型字段
public string Name; // 公有字符串字段
- 核心特性
- 存储位置:值类型字段存储于栈内存,引用类型字段存储堆内存地址。
- 生命周期:与所属对象实例绑定(实例字段)或与类型共存(静态字段)。
- 默认值:未显式初始化时,数值类型为
0,引用类型为 null。
(2)字段的分类与作用
| 分类 |
修饰符 |
作用 |
示例 |
| 实例字段 |
无 |
存储对象状态数据 |
private int _id; |
| 静态字段 |
static |
全局共享数据,类级别存储 |
public static int Count; |
| 只读字段 |
readonly |
仅允许在声明或构造函数中赋值 |
private readonly string _key; |
| 常量字段 |
const |
编译时确定值,不可修改 |
public const float PI = 3.14f; |
readonly 与 const:
const 必须在声明时赋值,且仅支持基本类型。
readonly 可在构造函数赋值,支持任意类型。
public class Config {
public const string Env = "Prod"; // 正确
public readonly string DbPath;
public Config() { DbPath = "C:/Data"; } // 构造函数赋值
}
(3)关键注意事项
- 初始化要求
- 常量字段 (
const) 必须声明时初始化。
- 只读字段 (
readonly) 必须在声明或构造函数中初始化 。
- 访问安全
- 避免将字段设为
public,防止外部直接篡改内部状态。
- 静态字段需注意线程安全(如用
lock 同步)。
- 性能优化
- 频繁访问的字段可标记为
readonly 减少意外修改风险 。
- 大型对象避免过多实例字段,防止内存碎片。
(4)典型应用场景
// 场景1:存储对象状态
public class Person {
private int _age; // 私有字段
public string Name { get; } // 只读属性(通过构造函数赋值)
public Person(string name) => Name = name;
}
// 场景2:全局配置(静态只读字段)
public static class AppConfig {
public static readonly string ConnectionString = "Server=...";
}
// 场景3:枚举替代方案(常量字段分组)
public class ErrorCodes {
public const int NotFound = 404;
public const int Forbidden = 403;
}
(5)开发建议
- 优先使用属性封装字段,增强代码健壮性。
- 对敏感数据(如密钥)使用
readonly + 私有字段组合。
5、属性
(1)基本概念与作用
- 属性(Property) 是类中封装字段的成员,提供对私有字段的安全访问机制。核心作用:
- 封装性:隐藏字段实现细节,仅暴露安全访问接口。
- 数据验证:在
set 访问器中添加逻辑(如非空检查、范围验证)。
- 计算属性:动态生成值(如全名 = 名 + 姓)。
- 示例:基础属性定义
public class Person
{
private string _name; // 私有字段
public string Name // 属性
{
get { return _name; }
set {
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("姓名不能为空");
_name = value;
}
}
}
(2)分类与语法
- 读写属性
- 同时包含
get(读取)和 set(写入)访问器。
- 自动属性:编译器自动生成私有字段(简化代码):
public string Name { get; set; } = "Unknown"; // 默认值初始化
- 只读属性:仅含
get 访问器,初始化后不可修改:
public int Age { get; } = 18; // 构造函数或初始化器赋值
- 访问器权限控制:为
get/set 单独设置访问级别:
public string Id { get; private set; } // 外部只读,类内可写
- 高级特性:
init 访问器(C# 9+):仅对象初始化时可赋值:
public required string Email { get; init; } // 必需属性
- 表达式体属性:单行逻辑简化:
public string FullName => $"{FirstName} {LastName}";
(3)关键注意事项
- 性能优化:
- 避免在属性访问器中执行耗时操作(如数据库查询)。
- 优先使用自动属性,除非需额外逻辑。
- 数据绑定支持:
- 实现
INotifyPropertyChanged 接口,通知界面更新:
public event PropertyChangedEventHandler? PropertyChanged;
public string Name
{
set {
_name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
}
}
(4)与字段的区别
| 特性 |
字段(Field) |
属性(Property) |
| 数据验证 |
不支持 |
支持(通过 set 访问器) |
| 计算逻辑 |
不支持 |
支持(动态返回值) |
| 访问控制 |
仅通过访问修饰符 |
可单独控制 get/set 权限 |
| 接口实现 |
不可用于接口 |
可用于接口定义 |
- 封装原则:字段通常设为
private,通过属性对外提供受控访问:
private string _name; // 私有字段
public string Name { // 封装属性
get => _name;
set {
if (!string.IsNullOrEmpty(value))
_name = value;
}
}
(5)实际应用场景
private int _age;
public int Age
{
get => _age;
set => _age = value >= 0 ? value : throw new ArgumentException("年龄不能为负");
}
private List<string>? _data;
public List<string> Data
{
get => _data ??= LoadData(); // 首次访问时加载
}
// 旧字段升级为属性(保持API兼容)
public string LegacyField { get; set; } // 替换原 public 字段
(6)最佳实践总结
- 优先使用属性而非公共字段:确保封装性和扩展性。
- 为必需属性添加
required 修饰符:强制调用方初始化(C# 11+)。
- 避免过度复杂逻辑:保持属性简洁,复杂操作移至方法中。
- 单元测试验证:确保
set 验证逻辑和 get 计算正确性。
6、索引器
(1)核心概念
- 本质与作用
- 索引器是特殊属性,允许类或结构体的实例像数组一样通过索引访问元素。
- 核心价值:简化集合类操作,提供直观的数据访问语法(如
obj[index])。
- 基本语法结构
public 返回值类型 this[参数类型 参数名]
{
get { /* 返回索引对应的值 */ }
set { /* 设置索引对应的值(value关键字) */ }
}
(2)实现步骤与示例
- 基础实现(封装数组)
- 索引器使用
this 关键字定义。
get/set 访问器控制读写逻辑(可只实现其一)。
public class IntCollection
{
private int[] _data = new int[10];
public int this[int index]
{
get => _data[index];
set => _data[index] = value;
}
}
// 使用示例
var collection = new IntCollection();
collection[0] = 100; // 调用set
Console.WriteLine(collection[0]); // 输出100(调用get)
- 多维索引器(如矩阵):支持多参数(如
row, col)实现多维访问
public class Matrix
{
private int[,] _grid = new int[3, 3];
public int this[int row, int col]
{
get => _grid[row, col];
set => _grid[row, col] = value;
}
}
// 使用示例
var matrix = new Matrix();
matrix[1, 2] = 5; // 设置第2行第3列的值
- 非整数索引(如字典式访问):索引参数可为任意类型(字符串、枚举等)
public class ConfigLoader
{
private Dictionary<string, string> _configs = new();
public string this[string key]
{
get => _configs[key];
set => _configs[key] = value;
}
}
// 使用示例
var config = new ConfigLoader();
config["Timeout"] = "30"; // 字符串作为索引
(3)关键特性与限制
- 重载支持 :同一类中可定义多个索引器(参数类型或数量不同)
public string this[int id] { ... }
public string this[string name] { ... } // 重载
- 使用限制
- 必须是实例成员(不能声明为
static)。
- 无法定义可选参数或使用
params 关键字。
- 访问性需与类一致(如公共类的索引器必须为
public)。
- 异常处理 :需手动检查索引有效性(如防止数组越界):
get
{
if (index < 0 || index >= _data.Length)
throw new IndexOutOfRangeException();
return _data[index];
}
(4)典型应用场景
- 自定义集合类:封装列表、字典等,提供类似原生数组的访问体验。
- 封装复杂数据结构:如矩阵、树形结构,通过索引简化节点操作。
- 配置文件/资源访问:通过字符串索引快速读写配置项。
(5)索引器 vs. 属性
| 特性 |
索引器 |
属性 |
| 访问语法 |
obj[index] |
obj.PropertyName |
| 参数支持 |
必须带参数 |
无参数 |
| 重载能力 |
支持多参数重载 |
不支持重载 |
| 命名 |
固定为 this |
自定义名称 |
- 索引器本质是带参数的属性,两者均依赖
get/set 访问器。
(6)最佳实践与陷阱规避
- 优先场景:需类支持集合式访问时使用(如自定义容器)。
- 避免滥用:非集合类不建议使用索引器,避免逻辑混淆。
- 性能优化:在
get/set 中避免复杂计算(如数据库查询)。
7、成员重载
(1)定义与核心规则
- 定义:在 同一作用域内(如类、结构体),声明多个 同名方法/构造函数/运算符,但参数列表(参数类型、数量或顺序)不同。
- 参数差异:必须通过参数区分(类型、数量或顺序),仅返回值不同不构成重载(编译错误)。
// 合法重载
void Print(int a) { }
void Print(string a) { } // 参数类型不同
void Print(int a, double b) { } // 参数数量不同
// 非法重载(返回值不同)
int GetValue() { return 0; }
string GetValue() { return ""; } // 编译报错
- 作用域限制:重载必须在同一类或派生类中(派生类需用
new 关键字隐藏基类方法)。
(2)方法重载
- 参数列表必须不同(满足以下任一):
- 参数类型不同:
void Print(int x) 与 void Print(string x)
- 参数数量不同:
void Print(int a) 与 void Print(int a, int b)
- 参数顺序不同:
void Print(int a, string b) 与 void Print(string a, int b)
- 返回值类型和参数名不影响重载:
int GetValue() 与 string GetValue() 不合法(编译错误)
void Process(int num) 与 void Process(int count) 不合法(参数名不同无效)
- 示例:
class Calculator {
// 参数数量不同
public int Add(int a, int b) => a + b;
public int Add(int a, int b, int c) => a + b + c;
// 参数类型不同
public double Add(double a, double b) => a + b;
}
(3)构造函数重载
- 作用:提供多种对象初始化方式。
- 规则:与方法重载一致,通过
this 关键字复用代码。
class Person {
public string Name;
public int Age;
// 无参构造
public Person() : this("Unknown", 0) {}
// 全参构造
public Person(string name, int age) {
Name = name;
Age = age;
}
}
(4)运算符重载
- 规则:
- 使用
operator 关键字定义静态方法(如 +, ==)。
- 至少一个参数为当前类类型。
- 示例(重载
+ 运算符):
class Vector {
public int X, Y;
public Vector(int x, int y) { X = x; Y = y; }
public static Vector operator +(Vector v1, Vector v2) {
return new Vector(v1.X + v2.X, v1.Y + v2.Y);
}
}
// 使用:Vector v3 = v1 + v2;
(5)设计规范与注意事项
- 避免歧义:确保参数差异足够明确,避免因隐式转换导致调用歧义:
void Process(float num) { }
void Process(double num) { }
Process(10); // 编译错误:int 可隐式转 float 或 double,无法确定调用哪个
- 优先使用泛型:若重载仅因类型不同(如
Add(int)、Add(double)),改用泛型方法更简洁:
public T Add<T>(T a, T b) where T : INumber<T> => a + b; // .NET 7+ 支持
- 慎用
params 参数:params 数组参数可能掩盖重载冲突,需测试边界情况:
void Log(string format) { }
void Log(params object[] args) { }
Log("test"); // 优先调用第一个重载
- 慎用运算符重载:确保语义符合直觉(如
+ 不应执行减法)。
(6)重载决策机制
- 精确匹配 > 隐式转换 > 参数数组展开
- 示例分析:
void Execute(int a) { } // 候选1
void Execute(double a) { } // 候选2
Execute(5); // 选择候选1(int 精确匹配)
Execute(5.0); // 选择候选2(double 精确匹配)
Execute(5.0f); // 选择候选2(float 隐式转 double)
(7)应用场景
- 简化 API 设计:
Console.WriteLine 支持不同参数类型(int, string, object)。
- 灵活初始化:类提供多种构造函数(如
FileStream 支持路径或句柄初始化)。
- 自定义类型行为:为结构体重载运算符实现向量运算。