目 录CONTENT

文章目录

CSharp(二十三)类(Class)访问修饰符和构造函数

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);  // 构造函数替你赋值了!

构造函数的核心规则(三条,记住了就不会错):

  1. 名字和类名一模一样
  2. 没有返回值,连 void 都不能写
  3. 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 最封闭。protectedinternal 各开一道门(子类 / 同项目),两者还能组合。


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 中常见的静态类MathConsoleFilePath——你日常就在用,只是可能没意识到。


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% 算

运行上面的代码,你可以清晰地看到:

  • BankNameInterestRateTotalAccounts——属于"银行"这个整体
  • AccountNumberOwnerNameBalance——属于"每个账户"自己

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
中文名 编译时常量 运行时常量
赋值时机 声明时必须赋值 声明时 构造函数中
可用的类型 stringbool、数值类型、枚举、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 出生证(运行时)

学习建议:每个例子敲一遍,改一改参数,看看有什么不同。编程是"做"会的,不是"看"会的。
.

0
博主关闭了当前页面的评论