CSharp(二十三)类(Class)访问修饰符和构造函数
1. 构造函数与析构函数
1.1 先讲故事:什么是构造函数?
想象你去买一部新手机。手机从工厂出来的时候,电池已经充了 50% 的电,默认语言是中文,WiFi 是关闭的——这些都是手机出厂时的初始状态。
构造函数就是 C# 里的"出厂设置"。每当你用 new 创建一个对象时,构造函数会自动运行,帮你把对象初始化好。你不用手动去设置每一个字段,构造函数替你做了。
// 没有构造函数的话,你得这样初始化:
Person p = new Person();
p.Name = "张三";
p.Age = 18;
p.IsActive = true;
// ... 如果有 10 个字段,你得写 10 行
// 有了构造函数,一行搞定:
Person p = new Person("张三", 18); // 构造函数替你赋值了!
构造函数的核心规则(三条,记住了就不会错):
- 名字和类名一模一样
- 没有返回值,连
void都不能写 - 用
new创建对象时自动调用,你不能手动调用
1.2 默认构造函数(无参构造函数)
如果你不写任何构造函数,编译器会自动送你一个"隐形的"无参构造函数。它什么都不做,只是让 new 能生效。
public class Car
{
// 你没写构造函数?
// 编译器帮你偷偷加了:
// public Car() { }
public string Brand;
public int Year;
}
// 使用:
Car c = new Car(); // ✅ 能编译,靠的就是编译器送的那个隐形构造函数
Console.WriteLine(c.Brand); // null(引用类型默认值)
Console.WriteLine(c.Year); // 0(值类型默认值)
但如果你自己写了任意一个构造函数,编译器就不再送那个隐形的了:
public class Car
{
public string Brand;
public int Year;
// 你自己写了一个构造函数
public Car(string brand)
{
Brand = brand;
}
}
// Car c = new Car(); // ❌ 编译错误!再也没有无参构造函数了!
Car c = new Car("丰田"); // ✅ 只能用这个了
一句话口诀:写了构造函数,编译器就不再送;想要无参的,自己补一个。
1.3 带参构造函数:用参数初始化对象
这是最常用的构造函数形式:创建对象的同时传入初始值。
public class Student
{
public string Name { get; set; }
public int Age { get; set; }
public string Grade { get; set; }
// 带参构造函数:创建时就设置好名字和年龄
public Student(string name, int age)
{
Name = name;
Age = age;
Grade = "未分配"; // 也可以给默认值
Console.WriteLine($"创建了一个学生:{Name},{Age}岁");
}
}
// 使用——对比一下有构造和没构造的区别:
// ❌ 没有构造函数的写法(繁琐):
Student s1 = new Student();
s1.Name = "小明";
s1.Age = 12;
s1.Grade = "六年级";
// ✅ 有构造函数的写法(简洁):
Student s2 = new Student("小明", 12);
1.4 构造函数重载:多种"出厂设置"
同类的构造函数可以有好几个版本(就像买车,有标配版、豪华版、旗舰版),参数不同就是不同的初始化方式。
public class Order
{
public string OrderId { get; set; }
public DateTime OrderTime { get; set; }
public decimal Amount { get; set; }
public string CustomerName { get; set; }
public bool IsPaid { get; set; }
// 版本1:最简版 — 只给最少的参数
public Order(string orderId)
{
OrderId = orderId;
OrderTime = DateTime.Now; // 自动取当前时间
Amount = 0;
CustomerName = "未知";
IsPaid = false;
}
// 版本2:标准版 — 给订单号和金额
public Order(string orderId, decimal amount)
{
OrderId = orderId;
OrderTime = DateTime.Now;
Amount = amount;
CustomerName = "未知";
IsPaid = false;
}
// 版本3:完整版 — 全部参数
public Order(string orderId, decimal amount, string customerName)
{
OrderId = orderId;
OrderTime = DateTime.Now;
Amount = amount;
CustomerName = customerName;
IsPaid = false;
}
// 打印订单信息
public void Print()
{
Console.WriteLine($"订单号: {OrderId} | 客户: {CustomerName} | 金额: {Amount} | 已付款: {IsPaid}");
}
}
// 使用——根据场景选择不同的构造方式:
Order o1 = new Order("O001"); // 快速创建一个空订单
Order o2 = new Order("O002", 99.99m); // 创建有价格的订单
Order o3 = new Order("O003", 199.99m, "张三"); // 创建完整订单
o1.Print();
o2.Print();
o3.Print();
重载规则回顾:方法名相同(都是类名),参数列表不同(个数、类型、顺序不同)。仅返回值不同不算重载(构造函数根本没返回值,所以更不用担心这个)。
1.5 this 串联调用构造函数(重要技巧)
如果有多个构造函数,很多初始化逻辑是重复的。用 : this(...) 可以链式调用,消除重复代码。
public class Employee
{
public string Name { get; set; }
public int Age { get; set; }
public string Department { get; set; }
public decimal Salary { get; set; }
// 主构造函数:所有参数都在这里
public Employee(string name, int age, string department, decimal salary)
{
Name = name;
Age = age;
Department = department;
Salary = salary;
Console.WriteLine($"完整初始化:{Name}, {Age}岁, {Department}, 月薪{Salary}");
}
// 三参数:年龄用默认值 0
public Employee(string name, string department, decimal salary)
: this(name, 0, department, salary) // 调用上面的四参数版本
{
Console.WriteLine(" → 年龄未指定,默认为 0");
}
// 两参数:部门用默认值
public Employee(string name, decimal salary)
: this(name, 0, "未分配", salary)
{
Console.WriteLine(" → 年龄和部门未指定,使用默认值");
}
// 无参:全是默认值
public Employee()
: this("未知", 0, "未分配", 0)
{
Console.WriteLine(" → 全部使用默认值");
}
}
// 使用效果:
Console.WriteLine("=== 创建员工1 ===");
Employee e1 = new Employee("张三", 28, "研发部", 15000m);
Console.WriteLine("\n=== 创建员工2 ===");
Employee e2 = new Employee("李四", "市场部", 12000m);
Console.WriteLine("\n=== 创建员工3 ===");
Employee e3 = new Employee("王五", 8000m);
Console.WriteLine("\n=== 创建员工4 ===");
Employee e4 = new Employee();
执行顺序(非常重要):
new Employee("李四", "市场部", 12000m)
第一步 → 调用 : this(name, 0, department, salary) → 实际调用四参数版本
四参数版本执行完毕
第二步 → 执行三参数版本自己的代码体
一句话:this 调用的那个构造函数先执行,自己的代码体后执行。
1.6 静态构造函数:类的"一次性初始化"
静态构造函数不是给对象用的,是给类本身用的。整个程序运行期间,它只执行一次,而且你完全不能控制它何时执行——.NET 运行时会自动安排。
public class GameSettings
{
// 静态字段
public static string ServerIp;
public static int MaxPlayers;
public static bool DebugMode;
// 实例字段
public string PlayerName;
// ===== 静态构造函数 =====
// 特点:无参数、无访问修饰符、不能手动调用
static GameSettings()
{
Console.WriteLine("【静态构造函数】加载全局配置...");
// 模拟从配置文件读取
ServerIp = "192.168.1.100";
MaxPlayers = 100;
DebugMode = false;
Console.WriteLine($" 服务器IP: {ServerIp},最大玩家: {MaxPlayers}");
}
// 实例构造函数
public GameSettings(string playerName)
{
PlayerName = playerName;
Console.WriteLine($"【实例构造函数】玩家 {PlayerName} 加入游戏");
}
}
// 使用:
Console.WriteLine("=== 程序开始 ===");
// 注意:静态构造函数什么时候执行?
// 答案:以下任一情况发生时,且只发生一次
// 情况1:第一次创建实例
GameSettings gs1 = new GameSettings("张三");
// 输出:
// 【静态构造函数】加载全局配置... ← 先执行
// 服务器IP: 192.168.1.100,最大玩家: 100
// 【实例构造函数】玩家 张三 加入游戏 ← 后执行
Console.WriteLine("\n--- 创建第二个玩家 ---");
GameSettings gs2 = new GameSettings("李四");
// 输出:
// 【实例构造函数】玩家 李四 加入游戏 ← 静态构造函数不会再执行了!
// 情况2:访问静态成员也会触发
// GameSettings.MaxPlayers // 如果你先访问了这个,静态构造函数也会在这时触发
静态构造函数的三个"铁律":
| 特性 | 说明 |
|---|---|
| 不能有参数 | 因为不是手动调用的,没法传参 |
| 不能有访问修饰符 | 永远是自动调用,public/private 没意义 |
| 只执行一次 | 整个程序生命周期内只跑一次 |
1.7 构造函数的执行顺序(面试重点)
当你有继承关系时,构造函数的调用顺序是从上往下(基类 → 派生类):
public class GrandParent
{
public GrandParent()
{
Console.WriteLine("爷爷的构造函数");
}
}
public class Parent : GrandParent
{
public Parent()
{
Console.WriteLine("爸爸的构造函数");
}
}
public class Child : Parent
{
public Child()
{
Console.WriteLine("孩子的构造函数");
}
}
// 创建孙子对象:
Child c = new Child();
// 输出顺序:
// 爷爷的构造函数
// 爸爸的构造函数
// 孩子的构造函数
// 记忆口诀:老祖宗先"出生",然后一代一代往下传
完整初始化顺序(细节版):
创建 new Child() 时:
1. 基类(GrandParent)的字段初始化
2. 基类(GrandParent)的构造函数执行
3. 父类(Parent)的字段初始化
4. 父类(Parent)的构造函数执行
5. 子类(Child)的字段初始化
6. 子类(Child)的构造函数执行
1.8 析构函数:对象"临终"的清理
析构函数在对象被垃圾回收器回收的时候自动调用。但在现代 C# 中,绝大部分场景都用 IDisposable 接口 + using 语句,析构函数很少手动写。
public class FileLogger
{
private string filePath;
private bool isOpen = false;
public FileLogger(string path)
{
filePath = path;
Console.WriteLine($"[构造] 日志文件已指定: {path}");
}
// 打开文件
public void Open()
{
Console.WriteLine($"[方法] 文件已打开: {filePath}");
isOpen = true;
}
// ===== 析构函数 =====
~FileLogger()
{
// 对象被回收时会执行这里
// 确保文件被关闭(即使程序员忘记了)
if (isOpen)
{
Console.WriteLine($"[析构] 紧急关闭文件: {filePath}");
isOpen = false;
}
else
{
Console.WriteLine($"[析构] 文件已经是关闭的: {filePath}");
}
}
}
// 使用:
void DoWork()
{
FileLogger logger = new FileLogger("log.txt");
logger.Open();
// ... 做了一些操作 ...
// 忘记关闭文件了!
} // 离开作用域,logger 变成"垃圾"
// 垃圾回收器在未来某个时间点会调用析构函数
// 输出(GC 运行时):
// [构造] 日志文件已指定: log.txt
// [方法] 文件已打开: log.txt
// ... 一段时间后 ...
// [析构] 紧急关闭文件: log.txt
实际开发中不推荐依赖析构函数。推荐使用
IDisposable+using:
public class BetterLogger : IDisposable
{
private string filePath;
public BetterLogger(string path) => filePath = path;
public void Open() => Console.WriteLine($"打开文件: {filePath}");
// IDisposable 要求实现的方法
public void Dispose()
{
Console.WriteLine($"[Dispose] 清理资源: {filePath}");
// 这里做清理工作
}
}
// ✅ 推荐用法:using 语句,离开作用域自动调用 Dispose
using (var logger = new BetterLogger("log.txt"))
{
logger.Open();
} // ← 这里自动调用 Dispose(),无需手动清理
1.9 本节总结
| 概念 | 一句话解释 |
|---|---|
| 默认构造函数 | 你不写,编译器送;你写了任意一个,编译器就不送了 |
| 带参构造函数 | 创建对象同时赋值,省去逐行赋值的麻烦 |
| 构造函数重载 | 同一个类提供多种初始化方式 |
this(...) 串联 |
复用构造函数,消除重复代码 |
| 静态构造函数 | 类级别的初始化,只执行一次,自动触发 |
| 析构函数 | 对象回收时自动调用,但更推荐 IDisposable + using |
2. 访问修饰符
2.1 先讲故事:什么是访问修饰符?
你家有客厅、卧室和保险柜。客厅谁都能进(访客、快递员),卧室只有家人能进,保险柜只有你能打开。
访问修饰符就是用来划分"谁可以碰这些东西"的边界线。
public——客厅:谁都能访问private——保险柜:只有你自己能访问protected——卧室:你自己和你的孩子(派生类)能访问internal——你家小区内部:同一个项目的人都能访问
2.2 六种访问修饰符全家福
C# 提供了六种访问修饰符,按开放程度从大到小排列:
| 修饰符 | 含义 | 同一类内 | 派生类(子类) | 同一项目 | 其他项目 |
|---|---|---|---|---|---|
public |
完全公开 | ✅ | ✅ | ✅ | ✅ |
protected internal |
同一项目 或 派生类 | ✅ | ✅ | ✅ | ❌ |
internal |
同一项目内可见 | ✅ | ❌ | ✅ | ❌ |
protected |
只对派生类开放 | ✅ | ✅ | ❌ | ❌ |
private protected |
同一项目内的派生类 | ✅ | ✅(同项目) | ❌ | ❌ |
private |
只有自己能看 | ✅ | ❌ | ❌ | ❌ |
一句话记忆法:
public最开放,private最封闭。protected和internal各开一道门(子类 / 同项目),两者还能组合。
2.3 用代码把每种修饰符跑一遍
我们用一个银行的例子来理解(这个例子覆盖了所有修饰符):
// ========== 在一个项目(程序集)里定义银行账户类 ==========
public class BankAccount
{
// public:任何人都能访问
public string AccountNumber;
// private:只有本类内部能访问 — 密码绝不能外泄!
private string Password;
// protected:本类和子类能访问 — 余额可以让子类看到但不让外人看
protected decimal Balance;
// internal:同一个项目(程序集)内能访问 — 银行内部审计信息
internal string InternalNote;
// protected internal:同一项目 或 派生类(满足任一即可)
protected internal DateTime LastTransactionTime;
// private protected:同一项目内的派生类才可访问
private protected decimal BackupBalance;
// 不加修饰符的字段 = private(类的成员默认是 private)
string _defaultPrivate = "默认就是私有";
public BankAccount(string accountNumber, string password)
{
AccountNumber = accountNumber;
Password = password;
Balance = 0;
LastTransactionTime = DateTime.Now;
}
// public 方法:谁都能调用
public void Deposit(decimal amount)
{
Balance += amount; // ✅ 类内可以访问 protected 成员
LastTransactionTime = DateTime.Now;
Console.WriteLine($"存入 {amount} 元,当前余额: {Balance} 元");
}
// private 方法:只有本类内部能调用
private void LogTransaction(string message)
{
Console.WriteLine($"[内部日志] {DateTime.Now}: {message}");
}
// protected 方法:本类和子类能调用
protected void AdjustBalance(decimal newBalance)
{
Balance = newBalance;
}
// internal 方法:同项目内能调用
internal void SetInternalNote(string note)
{
InternalNote = note;
}
// 验证密码(对外提供安全的密码校验方式)
public bool VerifyPassword(string password)
{
return Password == password; // ✅ 类内可以访问 private 成员
}
}
// ========== 派生类(子类)==========
public class SavingsAccount : BankAccount
{
public decimal InterestRate { get; set; }
public SavingsAccount(string accountNumber, string password, decimal rate)
: base(accountNumber, password)
{
InterestRate = rate;
}
public void CalculateInterest()
{
// ✅ 可以访问 protected 成员 Balance
decimal interest = Balance * InterestRate;
Balance += interest; // ✅ protected 成员,子类可访问
Console.WriteLine($"计算利息: {interest} 元,新余额: {Balance} 元");
// ✅ 可以访问 protected internal 成员
Console.WriteLine($"上次交易: {LastTransactionTime}");
// ✅ 可以访问 private protected 成员(同一项目内的子类)
Console.WriteLine($"备用余额: {BackupBalance}");
// ✅ 可以访问 public 成员
Console.WriteLine($"账号: {AccountNumber}");
// ❌ 不能访问 private 成员
// Console.WriteLine(Password); // 编译错误!
}
}
// ========== 同一个项目内的另一个类 ==========
public class BankAuditor
{
public void Audit(BankAccount account)
{
// ✅ public 成员 — 任何地方都能访问
Console.WriteLine($"审计账号: {account.AccountNumber}");
// ✅ internal 成员 — 同项目能访问
Console.WriteLine($"内部备注: {account.InternalNote}");
// ❌ private — 同项目也不行
// Console.WriteLine(account.Password); // 编译错误!
// ❌ protected — 非同类的非派生类不能访问
// Console.WriteLine(account.Balance); // 编译错误!
}
}
同一个项目内的权限总结(用代码说话):
// 在同一个项目内创建一个测试类
public class AccessTest
{
public void Test()
{
BankAccount account = new BankAccount("6222-1234", "mypwd");
// ✅ 以下都可以
account.AccountNumber = "6222-5678"; // public
account.LastTransactionTime = DateTime.Now; // protected internal (同项目满足)
account.InternalNote = "测试备注"; // internal
account.Deposit(100); // public 方法
// ❌ 以下都不行
// account.Password = "123"; // private — 只有类内部能碰
// account.Balance = 1000; // protected — 非派生类不能碰
// account.BackupBalance = 0; // private protected — 非同类的非派生类不行
}
}
2.4 跨项目访问:internal 的边界
假设你有两个项目:BankSystem(类库)和 ExternalApp(外部应用)。
在 BankSystem 中:
// BankSystem 项目
public class AccountService
{
internal string InternalKey = "key-123"; // 只在本项目内可见
public string PublicKey = "pub-456"; // 任何项目都可见
}
public class VIPAccount : AccountService
{
void Test()
{
Console.WriteLine(InternalKey); // ✅ 同项目内,子类可以
}
}
在 ExternalApp 项目中(引用了 BankSystem):
// ExternalApp 项目
public class ExternalClass
{
void Test()
{
AccountService service = new AccountService();
Console.WriteLine(service.PublicKey); // ✅ public,任何项目都能访问
// Console.WriteLine(service.InternalKey); // ❌ internal,不同项目不能访问!
}
}
2.5 封装原则:为什么不要用 public 字段?
核心思想:把数据藏起来,只暴露你需要别人用的。
// ❌ 错误示范:字段全部 public
public class BadStudent
{
public int Age; // 外人可以直接改成 -100,谁来管?
}
BadStudent bad = new BadStudent();
bad.Age = -100; // 非法值进来了,没有任何防范!
// ✅ 正确做法:private 字段 + public 属性(带验证)
public class GoodStudent
{
private int age; // 字段藏起来
public int Age // 属性对外暴露
{
get { return age; }
set
{
if (value < 0 || value > 150)
{
Console.WriteLine($"错误:年龄 {value} 不合法!");
return; // 直接拒绝
}
age = value;
}
}
}
GoodStudent good = new GoodStudent();
good.Age = 25; // ✅ 正常赋值
good.Age = -100; // ❌ 被拒之门外,控制台输出"年龄不合法"
封装口诀:字段用
private,需要外部访问的用public属性,对外只开放一个"窗口",数据怎么被操作自己说了算。
2.6 快速查阅:什么场景用什么修饰符
| 场景 | 推荐修饰符 | 原因 |
|---|---|---|
| 类的成员字段 | private |
封装原则,数据不外泄 |
| 对外提供的属性 | public + get/set |
安全可控的数据访问 |
| 只在项目内使用的工具方法 | internal |
不给外部项目用 |
| 子类需要的方法 | protected |
自己不用暴露,只给"后代" |
| 跨项目公开的 API | public |
对外接口 |
| 需要被子类或同一项目使用的 | protected internal |
灵活组合 |
2.7 本节总结
public → 天安门广场,谁都能去
internal → 小区内部,业主和住户能进
protected → 你家的卧室,只有家人(子类)能进
private → 你的日记本,只有你自己能看
protected internal → 小区的人 或 家人(满足一个就能进)
private protected → 住在小区里的家人(两个条件都要满足)
3. this 关键字
3.1 什么是 this?—— 三个字:"我自己"
this 代表当前这个对象本身。你在类里面写代码的时候,用 this 来指代"正在被操作的那个实例"。
public class Dog
{
public string Name;
public void WhoAmI()
{
Console.WriteLine($"我的名字是 {Name}");
Console.WriteLine($"this 就是我: {this}");
// this 是一个 Dog 类型的引用,指向调用 WhoAmI 的那只狗
}
}
Dog dog1 = new Dog { Name = "大黄" };
dog1.WhoAmI();
// 此时 this 就是 dog1
Dog dog2 = new Dog { Name = "小黑" };
dog2.WhoAmI();
// 此时 this 就是 dog2(不同对象,this 指向不同)
3.2 this 的五大用途
用途 1:区分字段和参数(最常用)
当方法参数和字段同名时,this 来区分谁是谁。
public class Person
{
private string name; // 这是字段
private int age; // 这是字段
// 参数也叫 name 和 age
public Person(string name, int age)
{
this.name = name; // this.name = 字段;name = 参数
this.age = age; // 同理
}
public void Rename(string name)
{
this.name = name; // 字段 = 参数
}
}
看
this就知是字段,没this就是参数(或在方法内声明的局部变量)。
用途 2:串联调用构造函数(见 1.5 节详细讲解)
public Person(string name) : this(name, 0) // 调用两参数的那一个
{
// this(..) 必须先于方法体执行
}
用途 3:把"我自己"传给别的方法
有时候你需要把当前对象作为参数传出去。
public class Student
{
public string Name { get; set; }
public string ClassName { get; set; }
// 报名参加班级
public void EnrollInClass(Classroom classroom)
{
classroom.AddStudent(this); // 把"我自己"传过去
Console.WriteLine($"{Name} 加入了 {classroom.ClassName}");
}
}
public class Classroom
{
public string ClassName { get; set; }
private List<Student> students = new List<Student>();
public Classroom(string name) => ClassName = name;
public void AddStudent(Student student)
{
students.Add(student);
Console.WriteLine($"班级 {ClassName} 添加了学生 {student.Name}");
}
public void ListStudents()
{
Console.WriteLine($"=== {ClassName} 的学生列表 ===");
foreach (var s in students)
{
Console.WriteLine($" - {s.Name}");
}
}
}
// 使用:
Classroom class1 = new Classroom("三年一班");
Student s1 = new Student { Name = "小明", ClassName = "三年一班" };
Student s2 = new Student { Name = "小红", ClassName = "三年一班" };
s1.EnrollInClass(class1); // 这里面 class1.AddStudent(this),this 就是 s1
s2.EnrollInClass(class1);
class1.ListStudents();
用途 4:链式调用(Fluent API 风格)
通过 return this,一个方法调用完可以继续在同一行调下一个方法——像搭积木一样。
public class Bulider
{
private string title;
private string content;
private string author;
private List<string> tags = new List<string>();
// 每个方法返回 this,支持链式调用
public Bulider SetTitle(string title)
{
this.title = title;
return this; // 返回自己
}
public Bulider SetContent(string content)
{
this.content = content;
return this;
}
public Bulider SetAuthor(string author)
{
this.author = author;
return this;
}
public Bulider AddTag(string tag)
{
tags.Add(tag);
return this;
}
// 最终构建
public void Print()
{
Console.WriteLine($"标题: {title}");
Console.WriteLine($"内容: {content}");
Console.WriteLine($"作者: {author}");
Console.WriteLine($"标签: {string.Join(", ", tags)}");
}
}
// 使用:链式调用,一气呵成
new Bulider()
.SetTitle("我的第一篇文章")
.SetContent("这是一段很长的内容...")
.SetAuthor("张三")
.AddTag("C#")
.AddTag("入门")
.AddTag("教程")
.Print();
// 输出:
// 标题: 我的第一篇文章
// 内容: 这是一段很长的内容...
// 作者: 张三
// 标签: C#, 入门, 教程
每个方法返回
this,下一步就能继续 "." 调用,这就是链式调用(Fluent API)。
用途 5:在扩展方法中标记类型
// 静态类 + 静态方法 + 第一个参数前加 this
public static class StringHelper
{
// 为 string 添加一个扩展方法:反转字符串
// this string 这里表示:"我要给 string 类型扩展新功能"
public static string Reverse(this string str)
{
if (string.IsNullOrEmpty(str)) return str;
char[] chars = str.ToCharArray();
Array.Reverse(chars);
return new string(chars);
}
// 再扩展一个:判断是否为邮箱格式
public static bool IsEmail(this string str)
{
return str != null && str.Contains("@") && str.Contains(".");
}
}
// 使用:和调用实例方法一模一样
string name = "Hello World";
Console.WriteLine(name.Reverse()); // "dlroW olleH"
Console.WriteLine("test@qq.com".IsEmail()); // True
Console.WriteLine("not-email".IsEmail()); // False
3.3 本节总结:this 的五个身份
| 用途 | 代码 | 场景 |
|---|---|---|
| 区分字段和参数 | this.name = name |
参数与字段同名时 |
| 串联构造函数 | : this(name, 0) |
复用构造逻辑 |
| 传递自身引用 | SomeMethod(this) |
把当前对象当参数 |
| 链式调用 | return this |
构建 Fluent API |
| 扩展方法标记 | this string str |
给已有类加新方法 |
4. 静态成员与静态类
4.1 先讲故事:实例 vs 静态
- 实例成员:每个对象自己有一份。比如每个学生有自己的名字、自己的分数——互不影响。
- 静态成员:全班只有一份,共享的。比如班级名称"三年一班"——不管你问哪个学生,班级名都一样。
public class Student
{
// 实例字段:每个学生自己的
public string Name;
public int Score;
// 静态字段:全班共享的
public static string ClassName = "三年一班";
public static int TotalStudentCount = 0;
public Student(string name, int score)
{
Name = name;
Score = score;
TotalStudentCount++; // 每创建一个学生,总数+1
}
}
// 使用:
Student s1 = new Student("小明", 90);
Student s2 = new Student("小红", 85);
Console.WriteLine(s1.Name); // 小明 ← 实例,自己的
Console.WriteLine(s2.Name); // 小红 ← 实例,自己的
Console.WriteLine(Student.ClassName); // 三年一班 ← 静态,共用的
Console.WriteLine(Student.TotalStudentCount); // 2 ← 静态,共用的
关键区分:实例成员用
对象.成员访问,静态成员用类名.成员访问。
4.2 静态字段与静态属性
public class AppConfig
{
// 静态字段(通常用 private 保护,通过属性暴露)
private static string _connectionString;
private static int _maxConnections = 100;
private static bool _isInitialized = false;
// 静态属性
public static string ConnectionString
{
get { return _connectionString; }
set
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("连接字符串不能为空!");
}
_connectionString = value;
}
}
public static int MaxConnections
{
get { return _maxConnections; }
set
{
if (value < 1 || value > 1000)
{
Console.WriteLine($"最大连接数 {value} 不合法,已保持原值");
return;
}
_maxConnections = value;
}
}
// 静态只读属性(计算属性)
public static string ConfigSummary
{
get
{
return $"连接串: {_connectionString?.Substring(0, Math.Min(20, _connectionString?.Length ?? 0))}... | 最大连接: {_maxConnections}";
}
}
// 初始化方法
public static void Initialize(string connectionString, int maxConn = 100)
{
ConnectionString = connectionString;
MaxConnections = maxConn;
_isInitialized = true;
Console.WriteLine("配置已初始化");
}
}
// 使用:通过类名直接访问
AppConfig.Initialize("Server=localhost;Database=MyDB;", 200);
Console.WriteLine(AppConfig.ConfigSummary);
Console.WriteLine($"最大连接: {AppConfig.MaxConnections}");
4.3 静态方法:不需要对象就能调用的方法
public class MathUtils
{
// 静态方法:求三个数中的最大值
public static double MaxOfThree(double a, double b, double c)
{
double max = a;
if (b > max) max = b;
if (c > max) max = c;
return max;
}
// 静态方法:判断是否是质数
public static bool IsPrime(int n)
{
if (n <= 1) return false;
if (n == 2) return true;
if (n % 2 == 0) return false;
for (int i = 3; i <= Math.Sqrt(n); i += 2)
{
if (n % i == 0) return false;
}
return true;
}
// 静态方法:格式化文件大小(字节 → 可读格式)
public static string FormatFileSize(long bytes)
{
string[] sizes = { "B", "KB", "MB", "GB", "TB" };
int order = 0;
double size = bytes;
while (size >= 1024 && order < sizes.Length - 1)
{
order++;
size /= 1024;
}
return $"{size:0.#} {sizes[order]}";
}
// ===== 重要限制 =====
// public static void Test()
// {
// // ❌ 静态方法里不能直接访问实例成员!
// // Console.WriteLine(this.SomeField); // 静态方法没有 this!
// }
}
// 使用:直接通过类名调用,不需要 new
Console.WriteLine(MathUtils.MaxOfThree(5, 12, 7)); // 12
Console.WriteLine(MathUtils.IsPrime(17)); // True
Console.WriteLine(MathUtils.FormatFileSize(1048576)); // 1 MB
静态方法的重要规则:
public class Demo
{
public int InstanceValue = 10; // 实例字段
public static int StaticValue = 20; // 静态字段
public void InstanceMethod()
{
// ✅ 实例方法:可以访问实例成员,也可以访问静态成员
Console.WriteLine(InstanceValue); // ✅ 实例字段
Console.WriteLine(StaticValue); // ✅ 静态字段
}
public static void StaticMethod()
{
// ✅ 静态方法:可以访问静态成员
Console.WriteLine(StaticValue); // ✅ 静态字段
// ❌ 但绝不能访问实例成员!
// Console.WriteLine(InstanceValue); // 编译错误!
// 原因:静态方法不属于任何对象,不知道 InstanceValue 是哪个对象的
}
}
口诀:静态方法很"孤独"——它不绑定任何对象,所以没有
this,也摸不到实例的字段。
4.4 静态类:全是静态成员的类
静态类不能创建实例(new 不了),专门用来放工具函数和常量。
public static class StringTools
{
// ⚠️ 静态类里所有成员都必须是 static 的
// 方法
public static string Truncate(string input, int maxLength)
{
if (string.IsNullOrEmpty(input)) return input;
if (input.Length <= maxLength) return input;
return input.Substring(0, maxLength) + "...";
}
public static string ToTitleCase(string input)
{
if (string.IsNullOrEmpty(input)) return input;
// 每个单词首字母大写
var words = input.Split(' ');
for (int i = 0; i < words.Length; i++)
{
if (words[i].Length > 0)
{
words[i] = char.ToUpper(words[i][0]) + words[i].Substring(1).ToLower();
}
}
return string.Join(" ", words);
}
// 静态常量
public static readonly string[] StopWords = { "的", "了", "是", "在", "和" };
// 可以有静态构造函数
static StringTools()
{
Console.WriteLine("StringTools 静态类已就绪");
}
}
// 使用:直接类名调用,不能 new
string longText = "这是一个非常非常非常长的文本内容";
Console.WriteLine(StringTools.Truncate(longText, 8)); // "这是一个非常非常..."
Console.WriteLine(StringTools.ToTitleCase("hello world")); // "Hello World"
// StringTools tool = new StringTools(); // ❌ 编译错误!静态类不能实例化
静态类的四个限制:
| 限制 | 原因 |
|---|---|
不能 new |
静态类不属于实例 |
| 只能有静态成员 | 没有实例,就没有实例成员 |
| 不能被继承 | 没有意义,静态类不参与多态 |
| 不能作为基类 | 同上 |
.NET 中常见的静态类:
Math、Console、File、Path——你日常就在用,只是可能没意识到。
4.5 实例 vs 静态:一个完整的对比例子
public class Bank
{
// ===== 静态成员:银行层面的信息 =====
public static string BankName = "中国人民银行";
public static decimal InterestRate = 0.03m; // 统一利率
private static int totalAccounts = 0;
public static int TotalAccounts
{
get { return totalAccounts; }
}
// ===== 实例成员:每个账户自己的信息 =====
public string AccountNumber { get; }
public string OwnerName { get; }
public decimal Balance { get; private set; }
public Bank(string ownerName, string accountNumber)
{
OwnerName = ownerName;
AccountNumber = accountNumber;
Balance = 0;
totalAccounts++; // 每开一个账户,总数+1
}
public void Deposit(decimal amount)
{
Balance += amount;
Console.WriteLine($"{OwnerName} 存入 {amount} 元,余额: {Balance} 元");
}
public void AddInterest()
{
// 实例方法可以访问静态成员
decimal interest = Balance * InterestRate; // ✅ 访问静态利率
Balance += interest;
Console.WriteLine($"{OwnerName} 获得利息 {interest} 元");
}
// 静态方法:调整全局利率
public static void AdjustInterestRate(decimal newRate)
{
InterestRate = newRate;
Console.WriteLine($"全局利率调整为: {InterestRate:P}");
}
}
// ===== 使用演示 =====
Console.WriteLine($"银行: {Bank.BankName}"); // 静态成员,用类名
Bank account1 = new Bank("张三", "622201");
Bank account2 = new Bank("李四", "622202");
Bank account3 = new Bank("王五", "622203");
Console.WriteLine($"总共开了 {Bank.TotalAccounts} 个账户"); // 3
account1.Deposit(10000); // 实例方法,用自己的余额
account2.Deposit(20000);
account3.Deposit(5000);
// 调整利率(静态方法,影响全局)
Bank.AdjustInterestRate(0.035m);
// 所有账户都按新利率计算利息
account1.AddInterest(); // 按 3.5% 算
account2.AddInterest(); // 按 3.5% 算
account3.AddInterest(); // 按 3.5% 算
运行上面的代码,你可以清晰地看到:
BankName、InterestRate、TotalAccounts——属于"银行"这个整体AccountNumber、OwnerName、Balance——属于"每个账户"自己
4.6 常见陷阱与误区
陷阱 1:静态字段是全局共享的,要注意线程安全
public static class Counter
{
public static int Count = 0;
public static void Increment()
{
Count++; // ⚠️ 多线程下这不是原子操作,可能出问题
}
// ✅ 正确的线程安全写法:
// private static int _count = 0;
// public static int Count => _count;
// public static void Increment() => Interlocked.Increment(ref _count);
}
陷阱 2:静态构造函数抛异常 = 这个类型"废了"
public static class BrokenConfig
{
static BrokenConfig()
{
throw new Exception("初始化失败!");
}
public static void DoSomething() { }
}
// 一旦访问 BrokenConfig 的任何成员,都会收到 TypeInitializationException
// 而且之后再也无法恢复,这个类就"废了"
陷阱 3:别在静态方法里干实例的事
// 这不会编译!
// public static void PrintName()
// {
// Console.WriteLine(Name); // ❌ Name 是实例成员,静态方法不认
// }
4.7 本节总结
| 对比维度 | 实例(Instance) | 静态(Static) |
|---|---|---|
| 属于谁 | 每个对象 | 类本身 |
| 有多少份 | 每个对象各有一份 | 整个程序只有一份 |
| 怎么访问 | 对象.成员 |
类名.成员 |
| 能访问实例成员吗 | ✅ | ❌(没有 this) |
| 能访问静态成员吗 | ✅ | ✅ |
有 this 吗 |
✅ | ❌ |
| 典型例子 | 学生的名字、年龄 | 班级名、学校名 |
5. 常量与只读字段
5.1 先讲故事:const 和 readonly 的区别
const(常量):就像刻在石头上的字——写死的,编译的时候就确定了,之后永远不变。readonly(只读字段):就像出生证明上的信息——出生那一刻确定,之后不能改,但每家孩子出生日期不同。
// const = 刻在石头上,大家看到都一样
// 比如圆周率 π,永远是 3.14159...,所有计算都共享这一个值
public const double PI = 3.14159;
// readonly = 出生证明,创建时确定,之后不变,但每个对象可以不同
// 比如身份证号,每个人不同,但一旦领了就不能改
public readonly string IdCardNumber;
5.2 const(编译时常量)详解
核心特征:值在编译时就写死了,程序运行时直接把值"嵌"到使用的地方。
public class MathConstants
{
// const 必须在声明时赋值,不能留到后面
public const double PI = 3.14159265358979;
public const double E = 2.71828182845905;
// 只能用于:string、bool、数值类型、枚举、null引用
public const string AppName = "超级计算器";
public const int MaxRetryCount = 3;
public const bool EnableDebug = false;
// ❌ 下面这些都不能用 const:
// public const DateTime Now = DateTime.Now; // DateTime 不是基础类型
// public const int[] Numbers = { 1, 2, 3 }; // 数组不是基础类型
// public const Point Point = new Point(1, 2); // 自定义类型不行
}
public class CircleCalculator
{
public static double GetArea(double radius)
{
// 编译时,MathConstants.PI 直接被替换成 3.14159265358979
return MathConstants.PI * radius * radius;
}
public static double GetCircumference(double radius)
{
return 2 * MathConstants.PI * radius;
}
}
// 使用:
Console.WriteLine(MathConstants.PI); // 3.14159265358979
Console.WriteLine(MathConstants.AppName); // 超级计算器
Console.WriteLine(CircleCalculator.GetArea(5)); // 78.5398163397448
const 的一个重要陷阱 — 跨程序集问题:
// ===== 项目 A(类库):定义了一个 const =====
public class LibConfig
{
public const int Version = 1;
}
// ===== 项目 B(应用程序):引用了项目 A =====
// 编译时,Version 的值 1 被直接"嵌入"到项目 B 的代码里
Console.WriteLine(LibConfig.Version); // 输出: 1
// ===== 如果你改了项目 A 的版本 =====
// public const int Version = 2;
// 但只重新编译了项目 A,没重新编译项目 B
// 项目 B 里嵌入的还是旧的 1!
// 这就是 const 的"值嵌入"带来的风险
// 解决方案:用 static readonly 代替
// public static readonly int Version = 1;
// readonly 在运行时取值,不会"嵌入"
5.3 readonly(运行时常量)详解
核心特征:值在运行时确定,一旦赋值就不能再改,但每个对象可以有自己的 readonly 值。
public class Document
{
// readonly 字段
public readonly string DocumentId; // 创建时确定,之后不变
public readonly DateTime CreatedTime; // 运行时的当前时间
public readonly string Author; // 创建时确定
// 普通字段(可改)
public string Content;
// 静态 readonly
public static readonly string AppVersion = "2.0.1";
public Document(string author)
{
// readonly 字段可以在构造函数中赋值
DocumentId = Guid.NewGuid().ToString("N").Substring(0, 8);
CreatedTime = DateTime.Now;
Author = author;
}
public void TryToModify()
{
Content = "可以修改普通字段";
// ❌ 以下全部编译错误!readonly 不能改
// DocumentId = "new-id"; // 报错!readonly 字段不可修改
// CreatedTime = DateTime.Now; // 报错!
// Author = "别人"; // 报错!
}
public void Print()
{
Console.WriteLine($"文档ID: {DocumentId}");
Console.WriteLine($"创建者: {Author}");
Console.WriteLine($"创建时间: {CreatedTime:yyyy-MM-dd HH:mm:ss}");
Console.WriteLine($"版本: {AppVersion}");
Console.WriteLine($"内容: {Content}");
}
}
// 使用:
Document doc1 = new Document("张三");
Document doc2 = new Document("李四");
doc1.Content = "这是张三的文档内容";
doc2.Content = "这是李四的文档内容";
doc1.Print();
// 文档ID: a1b2c3d4
// 创建者: 张三
// 创建时间: 2026-06-26 15:30:00
// 版本: 2.0.1
Console.WriteLine("---");
doc2.Print();
// 文档ID: e5f6g7h8 ← 和 doc1 不同
// 创建者: 李四 ← 和 doc1 不同
// 创建时间: 2026-06-26 15:30:01 ← 和 doc1 不同(晚了一秒)
// 版本: 2.0.1 ← 和 doc1 相同(static,共享的)
5.4 const vs readonly 终极对比
public class CompareDemo
{
// const:编译时常量
public const int ConstValue = 100;
// public const DateTime ConstTime = DateTime.Now; // ❌ 不能!
// readonly:运行时常量
public readonly int ReadonlyValue;
public static readonly DateTime ReadonlyTime = DateTime.Now;
public CompareDemo()
{
// const 不能在构造函数里赋值(编译时就定死了)
// ConstValue = 200; // ❌ 错误!
// readonly 可以在构造函数里赋值
ReadonlyValue = 200; // ✅ 没问题
}
public void Show()
{
Console.WriteLine($"ConstValue: {ConstValue} (编译时确定)");
Console.WriteLine($"ReadonlyValue: {ReadonlyValue} (构造函数赋值)");
Console.WriteLine($"ReadonlyTime: {ReadonlyTime} (运行时确定)");
}
}
一张表看清所有区别:
| 特性 | const |
readonly |
|---|---|---|
| 中文名 | 编译时常量 | 运行时常量 |
| 赋值时机 | 声明时必须赋值 | 声明时或 构造函数中 |
| 可用的类型 | 仅 string、bool、数值类型、枚举、null |
任意类型 |
| 是静态还是实例 | 隐式静态(永远属于类) | 可以是实例,也可以是静态 |
| 内存方式 | 值直接"嵌入"使用它的代码 | 正常的字段,在堆上 |
| 跨程序集风险 | ⚠️ 改了不重新编译全项目会出 bug | ✅ 运行时取值,没问题 |
| 适合存什么 | 真正的"永恒不变"的值:π、数学常数 | "创建后不变"的值:ID、创建时间 |
5.5 实际开发中的选择指南
public class AppSettings
{
// ✅ 用 const 的典型场景:
// 数学常量、固定配置值、永不改变的字符串
public const double TAX_RATE = 0.13; // 税率(法律规定的,基本不变)
public const int DEFAULT_PAGE_SIZE = 20; // 默认每页条数
public const string DEFAULT_LANGUAGE = "zh-CN"; // 默认语言
public const bool ENABLE_AUTO_SAVE = true; // 是否自动保存
// ✅ 用 readonly 的典型场景:
// 运行时才能确定的值,或者可能因环境而变化的值
public static readonly string ConnectionString = LoadConnectionString();
public static readonly DateTime AppStartTime = DateTime.Now;
public static readonly Version AppVersion = new Version(2, 0, 1);
// 实例 readonly:每个对象不同的"只读标识"
public readonly string InstanceId = GenerateId();
public readonly DateTime CreatedAt = DateTime.Now;
private static string LoadConnectionString()
{
// 模拟从配置文件读取(编译时不知道值)
return "Server=prod-server;Database=MainDB;";
}
private static string GenerateId()
{
return Guid.NewGuid().ToString("N").Substring(0, 12);
}
}
// 使用演示:
Console.WriteLine($"税率: {AppSettings.TAX_RATE:P}"); // const
Console.WriteLine($"连接串: {AppSettings.ConnectionString}"); // readonly
Console.WriteLine($"启动时间: {AppSettings.AppStartTime}"); // readonly
var setting1 = new AppSettings();
var setting2 = new AppSettings();
Console.WriteLine($"实例1 ID: {setting1.InstanceId}"); // 不同
Console.WriteLine($"实例2 ID: {setting2.InstanceId}"); // 不同
// 但每个对象的 InstanceId 一旦创建就不能改
5.6 本节总结
选择口诀:
- 值永远不变、编译时就知道 →
const(如数学常数 π、固定字符串)- 值创建后不变、但运行时才知道 →
readonly(如创建时间、配置文件读取的值)- 不确定会不会变、或者需要验证 → 用属性(
{ get; set; }),不要用常量- 跨项目共享的常量 → 优先用
static readonly,避免const的跨程序集陷阱
全文总结
这五节内容涵盖了 C# 类的基础核心:
| 章节 | 核心知识点 | 记忆口诀 |
|---|---|---|
| 1. 构造函数 | 对象的"出厂设置",用 new 自动调用 |
类名当方法,new 时调用 |
| 2. 访问修饰符 | 控制谁能访问什么 | public 开放 / private 封闭 / protected 给后代 / internal 给邻居 |
| 3. this 关键字 | 指代"当前对象" | this 就是我,字段参数同名时不迷路 |
| 4. 静态成员 | 属于类本身,不属于对象 | 静态用类名,实例用对象 |
| 5. 常量与只读 | 不可变的值 | const 刻石头(编译时),readonly 出生证(运行时) |
学习建议:每个例子敲一遍,改一改参数,看看有什么不同。编程是"做"会的,不是"看"会的。
.