目 录CONTENT

文章目录

CSharp(二十二)类(Class)定义与使用详解

CSharp(二十二)类(Class)定义与使用详解

目录

  1. 什么是类
  2. 类的基本语法
  3. 成员变量(字段)
  4. 属性(Property)
  5. 方法(Method)

1. 什么是类

1.1 从生活场景理解"类"

想象你去买车。4S 店里摆着很多车——丰田卡罗拉、本田思域、大众朗逸。每辆车都不一样(颜色、配置、里程数),但它们都属于"汽车"这个品类。

"汽车"这个品类就是一个"类"(Class)。

生活概念 编程概念 说明
汽车设计图纸 类(Class) 定义汽车有什么属性、可以做什么
4S店里的那辆白色卡罗拉 对象(Object) 根据图纸制造出来的具体一辆车
你的车 VIN码=888, 颜色=白 实例(Instance) 对某一个具体对象的称呼

再来一个例子——学生花名册

// "学生"这个类的设计图纸
public class Student
{
    public string Name;      // 姓名 —— 每个学生都有
    public int Age;          // 年龄 —— 每个学生都有
    public string ClassId;   // 班级号 —— 每个学生都有
}

// 根据设计图纸,造出两个"具体的学生"
Student xiaoMing = new Student();   // 第一个具体的学生:小明
xiaoMing.Name = "小明";
xiaoMing.Age = 12;
xiaoMing.ClassId = "六(3)班";

Student xiaoHong = new Student();   // 第二个具体的学生:小红
xiaoHong.Name = "小红";
xiaoHong.Age = 11;
xiaoHong.ClassId = "六(3)班";

小明和小红共用同一份"学生模板",但他们的名字、年龄可以各不相同——这就是类和对象的关系。

1.2 类在计算机中的本质

在 C# 中,类是一种引用类型(Reference Type)。理解"引用类型"这个概念,对于写 C# 代码至关重要。

先对比两种类型:

特性 值类型(Value Type) 引用类型(Reference Type)
常见的 int, double, bool, struct class, string, array
存储位置 栈(Stack) 堆(Heap)上存数据,栈上存地址
赋值行为 复制整个数据 复制引用(地址)
默认值 int = 0, bool = false null
空值 值类型不能为空 引用类型可以为 null
传递参数 传副本,原值不受影响 传地址,方法内修改会影响原对象

用一个实例来说明值类型和引用类型的区别:

// ========== 值类型:复制数据 ==========
int a = 10;
int b = a;      // b 得到 a 的副本
b = 20;         // 修改 b,a 不受影响
Console.WriteLine(a);  // 输出:10  ← a 没变

// ========== 引用类型:复制地址 ==========
Student s1 = new Student();
s1.Name = "张三";
Student s2 = s1;      // s2 指向和 s1 同一个对象!
s2.Name = "李四";      // 通过 s2 修改
Console.WriteLine(s1.Name);  // 输出:李四  ← s1 也变了!

上面这个例子中,s1s2 指向的是内存中的同一个学生对象。改 s2.Name 等于改 s1.Name。这就是引用类型的本质。

你可以这样理解:

值类型(比如 int):
  a = 10     →    ┌────┐
                  │ 10 │  a 的内存空间
                  └────┘
  b = a      →    ┌────┐
                  │ 10 │  b 的内存空间(独立复制)
                  └────┘

引用类型(比如 Student):
  s1 = new Student()  →  s1 ──→ ┌────────────────┐
                                 │ Student 对象    │
  s2 = s1              →  s2 ──→ │ Name = "张三"   │
                                 │ Age = 0         │
                                 └────────────────┘
  s1 和 s2 是同一把钥匙,指向同一个房间

1.3 面向对象编程的四大特性

类是面向对象编程的基石。理解了类,你就理解了面向对象的核心——封装、继承、多态、抽象

特性 含义 举例
封装 把数据和操作包装在类内部,隐藏实现细节 遥控器按"开机"键,你不需要知道电路如何工作
继承 一个类可以从另一个类获得属性和方法 "中学生"继承"学生"的所有属性,再增加"年级"
多态 同一操作对不同对象产生不同行为 "动物.叫()" → 狗:"汪汪",猫:"喵喵"
抽象 提取共同特征,忽略具体细节 "交通工具"是抽象概念,汽车、飞机、轮船是具体实现

1.4 一个完整的"麻雀虽小"示例

在学习具体细节之前,先感受一个完整的类和它的使用:

// ===== 类的定义 =====
public class Dog
{
    // 字段 — 狗狗的数据
    private string name;
    private int age;

    // 构造函数 — 创建对象时自动执行
    public Dog(string dogName, int dogAge)
    {
        name = dogName;
        age = dogAge;
    }

    // 属性 — 对外暴露数据的桥梁
    public string Name
    {
        get { return name; }
    }

    public int Age
    {
        get { return age; }
    }

    // 方法 — 狗狗的行为
    public void Bark()
    {
        Console.WriteLine($"{name}:汪汪!");
    }

    public string GetInfo()
    {
        return $"{name},{age}岁";
    }
}

// ===== 类的使用 =====
class Program
{
    static void Main(string[] args)
    {
        Dog myDog = new Dog("大黄", 3);
        myDog.Bark();                              // 输出:大黄:汪汪!
        Console.WriteLine(myDog.GetInfo());         // 输出:大黄,3岁
        Console.WriteLine($"名字:{myDog.Name}");    // 通过属性读取名字
    }
}

从这个例子可以看到:

  1. 类用 class 关键字声明
  2. 类里面有 字段属性构造函数方法
  3. 使用 new 关键字来创建一个对象
  4. 创建出来的对象拥有类里定义的所有能力

下面我们就从最简单的语法开始,逐步深入。


2. 类的基本语法

2.1 最简类定义

一个最简单的类,只需要 class 关键字 + 类名 + 一对大括号:

// 最精简的类 —— 有字段、有方法
public class Student
{
    // 字段(成员变量)
    public string Name;
    public int Age;

    // 方法
    public void SayHello()
    {
        Console.WriteLine($"大家好,我叫{Name},今年{Age}岁。");
    }
}

以上三要素:

  • public:访问修饰符,public 表示"公开的",任何人都能访问
  • class:关键字,告诉编译器要定义一个类
  • Student:类名,你自己起的名字(命名规范见下文)

2.2 使用类的三步走

// 第一步:声明一个引用变量(此时还不指向任何对象)
Student stu;

// 第二步:用 new 关键字创建对象,让变量指向它
stu = new Student();

// 第三步:通过变量操作对象
stu.Name = "小明";
stu.Age = 15;
stu.SayHello();

通常我们会把第一步和第二步合成一行:

Student stu = new Student();
//  ↑          ↑
//  声明变量    创建对象

为什么要用 new new 关键字在内存中开辟一块空间,用来存放对象的数据。不写 new,你的变量就只是一个空标签,不能用。

2.3 命名规范

在 C# 里,命名不只是"随便起个名字",它直接影响代码的可读性:

项目 规范 示例
类名 Pascal 命名法(每个单词首字母大写) Student, OrderManager, UserProfile
方法名 Pascal 命名法 GetName, CalculateTotal, PrintReport
属性名 Pascal 命名法 Name, TotalPrice, IsStudent
字段名 Camel 命名法(首字母小写) name, totalCount, isRunning
私有字段 Camel 命名法,通常加 _ 前缀 _name, _totalCount
局部变量 Camel 命名法 studentName, index
常量 Pascal 命名法 MaxSize, DefaultTimeout

什么是 Pascal 和 Camel?

Pascal(帕斯卡):每个单词首字母大写
  → StudentName、OrderManager、UserService

Camel(驼峰):第一个单词全小写,后面单词首字母大写
  → studentName、orderManager、userService

注意:类名要用名词名词短语。比如 Student(学生)、DataManager(数据管理器),不用动词。因为类描述的是"是什么",不是"做什么"。

2.4 类在项目中的位置

在一个标准的 C# 项目中,通常一个 .cs 文件放一个类:

MyProject/
├── Program.cs           ← 程序入口
├── Models/
│   ├── Student.cs       ← Student 类
│   ├── Teacher.cs       ← Teacher 类
│   └── Course.cs        ← Course 类
├── Services/
│   └── GradeService.cs  ← 成绩处理服务
└── Utils/
    └── StringHelper.cs  ← 字符串工具类
// Student.cs 文件
namespace MyProject.Models       // 命名空间,管理类的位置
{
    public class Student
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }
}

2.5 同一文件中多个类

一个 .cs 文件里可以写多个类,但不推荐这样做(除非是小程序或嵌套类):

// 这种方式可以运行,但不推荐
public class Person { }
public class Address { }
public class Phone { }

最佳实践:一个文件只放一个类,文件名等于类名。

2.6 特殊类形式:记录类型(Record)

C# 9.0 引入了 record 类型,它是一种特殊的引用类型,专为"不可变数据"设计:

// record:简单数据承载(C# 9.0+)
public record PersonRecord(string Name, int Age);

// 使用
var p1 = new PersonRecord("张三", 20);
var p2 = new PersonRecord("张三", 20);

Console.WriteLine(p1 == p2);  // 输出:True ← record 自动按值比较
// 如果用 class,== 会比较引用地址,结果会是 False

初学者建议:现阶段专注于 classrecord 是进阶内容,先知道有这个东西就行。

2.7 引用类型的空值处理

引用类型变量可以设为 null(空引用),但 null 访问对象成员会引发运行时异常:

Student s = null;

// s.Name = "测试";    // ❌ 运行时错误:NullReferenceException
// s.SayHello();       // ❌ 运行时错误:NullReferenceException

// ========== 安全写法:先判空 ==========
if (s != null)
{
    s.SayHello();
}

// ========== 简写:C# 6.0+ 空条件运算符 ==========
s?.SayHello();   // 如果 s 是 null,什么都不做

3. 成员变量(字段)

3.1 什么是字段

字段(Field) 就是类里面直接声明的变量。它负责存储对象的状态数据。

public class Person
{
    // 下面是三个字段
    public string Name;     // 姓名(公开)
    private int age;        // 年龄(私有)
    public double Height;   // 身高(公开)
}

如果把类比作一张表格:

字段名 类型 作用
Name string 存储人的姓名
age int 存储人的年龄
Height double 存储人的身高

每个对象都拥有一份独立的字段副本

Person p1 = new Person();
p1.Name = "张三";
p1.Height = 175.5;

Person p2 = new Person();
p2.Name = "李四";
p2.Height = 160.0;

// p1 和 p2 的字段互不影响
Console.WriteLine($"{p1.Name} 身高 {p1.Height}");   // 张三 身高 175.5
Console.WriteLine($"{p2.Name} 身高 {p2.Height}");   // 李四 身高 160

3.2 字段分类

根据修饰符不同,字段有不同类型:

public class GameSettings
{
    // ===========================
    // 一、实例字段(Instance Field)
    // ===========================
    // 属于每个对象,每 new 一个对象就有一份
    public string PlayerName;
    private int score;
    public int Level;

    // ===========================
    // 二、静态字段(Static Field)
    // ===========================
    // 属于类本身,不管有多少对象,都只有一份
    // 所有实例共享同一份数据
    public static int TotalPlayers = 0;
    public static string GameTitle = "超级冒险";

    // ===========================
    // 三、只读字段(Readonly Field)
    // ===========================
    // 只能在声明时或构造函数中赋值,之后不能改
    public readonly string PlayerId;
    public readonly DateTime CreateTime;

    // ===========================
    // 四、常量(Const)
    // ===========================
    // 编译时常量,必须在声明时赋值,之后不能改
    // 隐含 static
    public const int MaxLevel = 100;
    public const double PI = 3.14159265358979;

    // ===========================
    // 五、volatile 字段
    // ===========================
    // 告诉编译器该字段可能被多个线程同时修改
    // 确保每次读取都从内存获取最新值
    private volatile bool isRunning;

    // 构造函数中初始化只读字段
    public GameSettings(string playerName, string playerId)
    {
        PlayerName = playerName;
        PlayerId = playerId;
        CreateTime = DateTime.Now;
        TotalPlayers++;  // 静态字段:每次创建玩家,总数+1
    }
}

3.3 访问权限:public 和 private

public class BankAccount
{
    // public 字段:谁都能看、谁能改
    public string AccountNumber;

    // private 字段:只有类自己的代码能访问
    private decimal balance;

    public void Deposit(decimal amount)
    {
        // 类内部可以访问 private 字段
        balance += amount;
        Console.WriteLine($"存入 {amount} 元,余额 {balance} 元");
    }
}

// 使用
BankAccount account = new BankAccount();
account.AccountNumber = "622200123456";   // ✅ public 字段可以
// account.balance = 1000;                // ❌ private 字段,外部不能访问
account.Deposit(1000);                    // ✅ 通过公开方法间接操作

核心原则:永远不要把字段设为 public 让外部直接改。应该用 private 字段 + public 方法/属性来封装。这样你可以在方法里加入检查逻辑(比如余额不能为负数)。

3.4 静态字段深入理解

静态字段最容易让初学者困惑。看这个例子:

public class Counter
{
    // 实例字段 — 它是"李四的计数"或者"张三的计数"
    private int instanceValue;

    // 静态字段 — 它是"全班共用的计数",不属于张三也不属于李四
    private static int sharedValue;

    public Counter()
    {
        instanceValue = 0;
        sharedValue++;  // 每 new 一个,共享计数 +1
    }

    public void Increment()
    {
        instanceValue++;  // 只增加这个对象的计数
    }

    public void Display()
    {
        Console.WriteLine($"实例值 = {instanceValue},共享值 = {sharedValue}");
    }
}

// 使用
Counter c1 = new Counter();  // sharedValue = 1
Counter c2 = new Counter();  // sharedValue = 2
Counter c3 = new Counter();  // sharedValue = 3

c1.Increment();
c1.Increment();
c1.Display();  // 实例值 = 2,共享值 = 3
c2.Display();  // 实例值 = 0,共享值 = 3
c3.Display();  // 实例值 = 0,共享值 = 3

规律

  • 实例字段:每个对象各有各的,互不影响
  • 静态字段:大家共用一份,改了一处等于改了全部

3.5 只读字段 vs 常量

这是面试和考试中经常出现的问题:

public class Demo
{
    // const:编译时就确定了值,直接"烧"进代码
    public const string AppTitle = "我的应用程序";
    public const int MaxUsers = 100;

    // readonly:运行时确定的值,可以在构造函数中赋值
    public readonly DateTime CreateTime;
    public readonly string InstanceId;

    public Demo()
    {
        // readonly 字段可以在构造函数中赋值
        CreateTime = DateTime.Now;  // 程序运行的那一刻确定
        InstanceId = Guid.NewGuid().ToString();  // 随机生成
    }
}
对比维度 const readonly
赋值时机 必须在声明时立即赋值 声明时或构造函数中赋值
运行时 值在编译时就固定了 值在运行时确定
类型限制 只能是简单类型 + string 任何类型都可以
静态/实例 隐式 static 可以是实例或 static
更新影响 改值后,所有引用处必须重新编译 改值后只需重新编译本程序集

一个通俗的理解:

const     →  像刻在石头上的字,造石头的时候就固定了
readonly  →  像写在本子上的字,本子造好之后你还有一次写的机会

3.6 字段默认值

C# 中,字段会自动赋予默认值:

public class DefaultValues
{
    public int IntValue;           // 默认 = 0
    public float FloatValue;       // 默认 = 0.0f
    public double DoubleValue;     // 默认 = 0.0
    public bool BoolValue;         // 默认 = false
    public char CharValue;         // 默认 = '\0'
    public string StringValue;     // 默认 = null
    public object ObjectValue;     // 默认 = null
    public DateTime DateValue;     // 默认 = 0001-01-01
}

注意:字段有默认值,但局部变量(方法内声明的变量)没有默认值,必须手动赋初值才能使用。

public void Test()
{
    int localVar;                // 声明但没有赋值
    // Console.WriteLine(localVar);  // ❌ 编译错误!必须赋值后才能用
    localVar = 10;
    Console.WriteLine(localVar); // ✅ 赋值之后可以用
}

3.7 字段初始化器

可以在声明字段时直接赋初值(字段初始化器):

public class Player
{
    // 声明时可以赋初始值
    public string Name = "无名玩家";
    public int Level = 1;
    public int Health = 100;
    public DateTime JoinDate = DateTime.Now;
    public List<string> Inventory = new List<string>();  // 初始化集合字段!

    // ⚠️ 注意:不能引用另一个实例字段!
    // public string DisplayName = Name + Level.ToString();  // ❌ 编译错误
}

重要习惯:给集合类型的字段初始化空列表,否则它是 null,调用 .Add() 之类的操作会报 NullReferenceException


4. 属性(Property)

4.1 为什么需要属性?字段不够用吗?

假设有这样一段代码:

public class Student
{
    public int Age;  // 公开字段
}

// 外部代码
Student stu = new Student();
stu.Age = -5;  // ❌ 年龄可以是负数?这不对!

直接用 public 字段的缺点:

  • 外部代码可以随意改成非法值(比如负的年龄)
  • 改了实现细节之后,所有调用的地方都要跟着改
  • 无法在"取值"或"赋值"时执行额外逻辑

属性(Property) 就是为了解决这些问题而生的。

4.2 属性的本质

属性看起来像字段,但本质是方法

// ===== 属性写法 =====
private int age;

public int Age
{
    get { return age; }    // 读 — 相当于 int GetAge()
    set { age = value; }   // 写 — 相当于 void SetAge(int value)
}

// ===== 等同于以下两个方法 =====
public int GetAge()
{
    return age;
}

public void SetAge(int value)
{
    age = value;
}

使用属性时,语法就像使用字段一样简单:

student.Age = 20;          // 看起来像给字段赋值
int currentAge = student.Age;  // 看起来像读字段
// 但实际上调用的是 set 和 get 方法!

4.3 五种属性写法

public class Person
{
    // ==========================================
    // 写法一:完整属性(有后备字段)
    // ==========================================
    private string name;

    public string Name
    {
        get { return name; }
        set { name = value; }     // value 是 C# 关键字,表示传入的值
    }

    // ==========================================
    // 写法二:带验证逻辑的属性
    // ==========================================
    private int age;

    public int Age
    {
        get { return age; }
        set
        {
            // 在赋值前做检查
            if (value < 0 || value > 150)
            {
                Console.WriteLine($"警告:年龄 {value} 不合法,已忽略");
                return;
            }
            age = value;
        }
    }

    // ==========================================
    // 写法三:自动属性(最常用)
    // ==========================================
    // 编译后会自动生成隐藏的后备字段,无需手写
    public string PhoneNumber { get; set; }
    public string Address { get; set; }

    // ==========================================
    // 写法四:只读 / 只写属性
    // ==========================================
    private string idCard;

    // 只读属性 — 只有 get
    public string IdCard
    {
        get { return idCard; }
    }

    // 计算属性 — 没有后备字段,每次实时计算
    public string DisplayInfo
    {
        get { return $"{Name},{Age}岁"; }
    }

    // 只写属性(很少用)— 只有 set
    private string password;

    public string Password
    {
        set
        {
            password = Encrypt(value);
        }
    }

    // ==========================================
    // 写法五:表达式体属性(C# 6.0+)
    // ==========================================
    public bool IsAdult => Age >= 18;

    public string FullInfo => $"姓名:{Name},年龄:{Age},成人:{(IsAdult ? "是" : "否")}";

    private string Encrypt(string pwd) { /* 加密 */ return pwd; }
}

4.4 自动属性的访问修饰符

public class Product
{
    // 属性本身是 public(谁都能读)
    // 但 set 是 private(只有内部能改)
    public decimal Price { get; private set; }

    // 属性本身是 public
    // get 是 internal(同一程序集内能读)
    // set 是 private(只有内部能改)
    public decimal Cost { internal get; private set; }

    // C# 6.0+:自动属性可以直接赋初始值
    public string Name { get; set; } = "未命名商品";
    public int Stock { get; private set; } = 100;

    public Product(decimal price)
    {
        Price = price;  // 内部可以 set
    }
}

// 使用
Product p = new Product(99.9m);
Console.WriteLine(p.Price);  // ✅ 可以读
// p.Price = 50;             // ❌ 编译错误!set 是 private

4.5 init 访问器(C# 9.0+)

init 是一种特殊的只读属性:只能在对象初始化时赋值一次,之后就锁死了:

public class Student
{
    public string Name { get; init; }     // init = 对象初始化后就不能改
    public int Age { get; init; }
    public string StudentId { get; init; }
}

// 使用
Student stu = new Student
{
    Name = "张三",        // ✅ 初始化时赋值 OK
    Age = 20,            // ✅ 初始化时赋值 OK
    StudentId = "S001"   // ✅ 初始化时赋值 OK
};

// stu.Name = "李四";     // ❌ 编译错误!init 属性初始化后不能修改

init vs private setinit 更严格,只能在一开始设置,之后永远不能改;private set 类内部随时可以改。

4.6 required 属性(C# 11.0+)

required 强制调用者必须赋值,否则编译不通过:

public class User
{
    public required string UserName { get; set; }    // 必须赋值
    public required string Email { get; set; }       // 必须赋值
    public string Phone { get; set; }                // 可选
}

// 使用
User user = new User
{
    UserName = "zhangsan",  // ✅ 必须写
    Email = "zs@test.com"   // ✅ 必须写
    // Phone 可以不写
};
// new User { UserName = "ls" };  // ❌ 编译错误!Email 没写

4.7 属性 vs 字段:选择指南

场景 用什么 为什么
简单数据,无验证逻辑 自动属性 { get; set; } 简洁,编译器帮你生成后备字段
需要验证、转换、通知 完整属性 可以在 set 里写任何逻辑
只内部使用 私有字段 private 属性对外、字段对内
动态计算值 只读属性 => 表达式 没有后备字段,省内存
对外暴露的数据 属性 不破坏封装,可以自由改内部实现

5. 方法(Method)

5.1 什么是方法

方法就是封装了一段可重复使用的代码,用来执行某个操作。把方法理解为"给一段代码起个名字,需要的时候喊一声"。

public class Calculator
{
    // 加法功能,起个名字叫 Add
    public int Add(int a, int b)
    {
        int result = a + b;
        return result;            // return 把结果送回去
    }
}

// 使用
Calculator calc = new Calculator();
int sum = calc.Add(3, 5);        // 喊一声"Add",传入 3 和 5
Console.WriteLine(sum);          // 输出:8

5.2 方法的组成部分

public int Add(int a, int b)      // 方法签名(方法头)
{                                  // ← 方法体开始
    int result = a + b;            // 方法体 — 具体执行的代码
    return result;                 // 返回值
}                                  // ← 方法体结束

逐部分解释:

部分 代码 含义
访问修饰符 public 谁可以调用这个方法
返回类型 int 这个方法执行完返回什么类型的数据
方法名 Add 方法的名字,通过名字来调用
参数列表 (int a, int b) 调用时必须传入的数据
方法体 { ... return result; } 实际执行的代码
返回值 return result 把计算结果送给调用方

5.3 各种形式的方法

public class DemoMethods
{
    // ==========================================
    // 形式一:无参数,无返回值
    // ==========================================
    public void SayHello()
    {
        Console.WriteLine("你好,世界!");
    }
    // void 表示"什么都不返回"
    // 调用:demo.SayHello();

    // ==========================================
    // 形式二:有参数,无返回值
    // ==========================================
    public void Greet(string name)
    {
        Console.WriteLine($"你好,{name}!");
    }
    // 调用:demo.Greet("小明");

    // ==========================================
    // 形式三:有参数,有返回值
    // ==========================================
    public int Square(int n)
    {
        return n * n;
    }
    // 调用:int result = demo.Square(5);  // result = 25

    // ==========================================
    // 形式四:无参数,有返回值
    // ==========================================
    public string GetCurrentTime()
    {
        return DateTime.Now.ToString("HH:mm:ss");
    }
    // 调用:string time = demo.GetCurrentTime();

    // ==========================================
    // 形式五:表达式体方法(C# 6.0+)
    // ==========================================
    // 只有一行代码的方法可以简写成 =>
    public int Double(int x) => x * 2;
    public bool IsEven(int x) => x % 2 == 0;
    // 优点:代码更简洁,一目了然
}

5.4 return 关键字详解

return 用于结束方法并返回结果。一旦执行到 return,方法就结束,后面的代码不会执行。

public string GetGrade(int score)
{
    if (score >= 90)
    {
        return "A";   // 到这里方法就结束了
    }

    if (score >= 80)
    {
        return "B";   // 到这里方法就结束了
    }

    if (score >= 70)
    {
        return "C";
    }

    // 所有分支都要有 return
    return "D";
}

规则:有返回值的方法(非 void),必须在所有可能的代码路径上都有 return 语句。

5.5 方法重载(Overload)

方法重载:同一个类里可以有多个同名方法,只要它们的参数列表不同

public class Logger
{
    // 重载1:只记录消息
    public void Log(string message)
    {
        Console.WriteLine($"[信息] {message}");
    }

    // 重载2:记录消息 + 错误级别
    public void Log(string message, string level)
    {
        Console.WriteLine($"[{level}] {message}");
    }

    // 重载3:记录消息 + 错误级别 + 时间戳
    public void Log(string message, string level, DateTime time)
    {
        Console.WriteLine($"[{time:HH:mm:ss}] [{level}] {message}");
    }

    // 重载4:不同类型参数
    public void Log(int errorCode)
    {
        Console.WriteLine($"[错误码] {errorCode}");
    }

    // 重载5:参数顺序不同
    public void Log(int code, string message)
    {
        Console.WriteLine($"[错误{code}] {message}");
    }

    public void Log(string message, int code)
    {
        Console.WriteLine($"[{message}] 错误码:{code}");
    }
}

// 编译器会根据你传入的参数自动选择正确的版本
Logger log = new Logger();
log.Log("系统启动");                              // 调用重载1
log.Log("文件丢失", "ERROR");                      // 调用重载2
log.Log("任务完成", "INFO", DateTime.Now);         // 调用重载3
log.Log(404);                                     // 调用重载4
log.Log(500, "服务器内部错误");                      // 调用重载5
log.Log("用户不存在", 401);                         // 调用重载5另一个版本

重载的规则

✅ 参数数量不同         → 算重载
✅ 参数类型不同         → 算重载
✅ 参数类型顺序不同     → 算重载
❌ 仅返回值不同         → 不算重载!(编译错误)
❌ 仅参数名不同         → 不算重载!
❌ 仅 ref/out 不同      → 不算重载(C# 规定)
// 错误示例:仅返回值不同,不是重载!
public int GetValue() { return 1; }
// public string GetValue() { return "a"; }   // ❌ 编译错误!

// 错误示例:仅参数名不同,不是重载!
public void Do(int age) { }
// public void Do(int height) { }   // ❌ 编译错误!

5.6 可选参数

有些参数调用时可以省略(因为有默认值):

public class EmailSender
{
    // subject 和 cc 有默认值,调用时可省略
    public void Send(
        string to,
        string body,
        string subject = "无主题",
        string cc = "",
        bool isHtml = false)
    {
        Console.WriteLine($"发送邮件给:{to}");
        Console.WriteLine($"主题:{subject}");
        Console.WriteLine($"内容:{body}");
        if (!string.IsNullOrEmpty(cc))
            Console.WriteLine($"抄送:{cc}");
        Console.WriteLine($"格式:{(isHtml ? "HTML" : "纯文本")}");
        Console.WriteLine("-----");
    }
}

// 使用
EmailSender sender = new EmailSender();

// 全部使用默认值
sender.Send("a@test.com", "你好");

// 覆盖前两个默认值
sender.Send("b@test.com", "开会通知", "紧急会议");

// 全部显式指定
sender.Send("c@test.com", "报告", "周报", "manager@test.com", true);

注意:可选参数必须放在必选参数的后面,不能 Send(string subject = "默认", string to) 这样写。

5.7 命名参数

调用时可以不按顺序,直接指定参数名:

public class Builder
{
    public void CreateUser(string name, int age, string email, string phone = "")
    {
        Console.WriteLine($"创建用户:{name}, {age}岁, {email}, {phone}");
    }
}

// 使用
Builder b = new Builder();

// 传统方式(按顺序)
b.CreateUser("张三", 25, "zs@test.com", "123456");

// 命名参数(不按顺序!)
b.CreateUser(age: 30, name: "李四", email: "ls@test.com");

// 混合:前面的按顺序,后面的命名
b.CreateUser("王五", 22, phone: "654321", email: "ww@test.com");

5.8 ref、out、in 参数详解

这三个关键字本质上改变了参数的传递方式

5.8.1 正常传递(按值传递)

// 默认:按值传递 — 方法内修改不影响外部
public void NormalAdd(int x)
{
    x = x + 10;
    Console.WriteLine($"方法内部:x = {x}");
}

// 调用
int a = 5;
NormalAdd(a);
Console.WriteLine($"方法外部:a = {a}");  // 输出:5  ← 没变!

5.8.2 ref — 双向传递

// ref:按引用传递 — 方法内外是同一个变量
public void RefAdd(ref int x)
{
    x = x + 10;
    Console.WriteLine($"方法内部:x = {x}");
}

// 调用
int b = 5;
RefAdd(ref b);                              // 注意:调用时也要写 ref
Console.WriteLine($"方法外部:b = {b}");    // 输出:15  ← 变了!

5.8.3 out — 只出不进

// out:纯输出 — 不用先初始化,方法负责赋值
public bool TryDivide(int a, int b, out int result)
{
    if (b == 0)
    {
        result = 0;
        return false;     // 除数为0,失败
    }

    result = a / b;
    return true;
}

// 调用
int answer;  // 不需要初始化!
bool success = TryDivide(10, 3, out answer);
if (success)
{
    Console.WriteLine(answer);  // 3
}
else
{
    Console.WriteLine("除法失败");
}

// C# 7.0+:可以在调用时直接声明变量
TryDivide(20, 4, out int directResult);
Console.WriteLine(directResult);  // 5

5.8.4 in — 只读传入

// in:只读引用 — 不能修改,但传的是引用(避免大结构体的拷贝开销)
public void PrintValue(in int value)
{
    Console.WriteLine(value);
    // value = 100;   // ❌ 编译错误!in 参数不能修改
}

// 调用
int c = 42;
PrintValue(c);       // 可以省略 in
PrintValue(in c);    // 显式写 in 也行

5.8.5 三兄弟对比

关键字 方向 传入前需初始化 方法内必须赋值 方法内能修改 调用时写关键字
默认 ✅(不影响外部) 不写
ref 进出 ✅(影响外部) 必须写
out ✅(影响外部) 必须写
in ❌(不能改) 可省略

5.9 params 关键字(可变数量参数)

当参数数量不确定时,用 params

public class MathTool
{
    // params:可以传入任意数量的 int
    public double Average(params int[] numbers)
    {
        if (numbers.Length == 0)
            return 0;

        int sum = 0;
        foreach (int n in numbers)
        {
            sum += n;
        }
        return (double)sum / numbers.Length;
    }

    // 也可以组合:普通参数 + params(params 必须在最后)
    public void PrintScores(string subject, params int[] scores)
    {
        Console.Write($"{subject}:");
        foreach (int s in scores)
        {
            Console.Write($"{s} ");
        }
        Console.WriteLine($"(平均:{scores.Average():F1})");
    }
}

// 使用
MathTool tool = new MathTool();

// 传入3个数
double avg1 = tool.Average(80, 90, 85);  // 85.0

// 传入5个数
double avg2 = tool.Average(60, 70, 80, 90, 100);  // 80.0

// 不传参数
double avg3 = tool.Average();  // 0

// 直接传数组也行
int[] data = new int[] { 75, 85, 95 };
double avg4 = tool.Average(data);

// 普通参数 + params
tool.PrintScores("数学", 90, 85, 78, 92);
// 输出:数学:90 85 78 92 (平均:86.3)

5.10 递归方法

方法自己调用自己,就是递归。适合解决"自相似"的问题。

public class RecursionDemo
{
    // ========== 例1:计算阶乘 ==========
    // n! = n × (n-1) × (n-2) × ... × 1
    // 5! = 5 × 4 × 3 × 2 × 1 = 120
    public long Factorial(int n)
    {
        if (n <= 1)
            return 1;                    // 终止条件:1! = 1

        return n * Factorial(n - 1);     // 递归调用自己
    }
    // 执行过程:
    //   Factorial(5)
    //   = 5 * Factorial(4)
    //   = 5 * (4 * Factorial(3))
    //   = 5 * (4 * (3 * Factorial(2)))
    //   = 5 * (4 * (3 * (2 * 1)))
    //   = 120

    // ========== 例2:计算斐波那契数列 ==========
    // 数列:1, 1, 2, 3, 5, 8, 13, 21, ...
    // 规律:每个数 = 前两个数之和
    public int Fibonacci(int n)
    {
        if (n <= 1)
            return n;                          // 终止条件

        return Fibonacci(n - 1) + Fibonacci(n - 2); // 递归调用两次
    }

    // ========== 例3:遍历文件夹(伪代码)==========
    // 递归也常用于操作树形结构
    public void ListFiles(string path)
    {
        Console.WriteLine($"进入文件夹:{path}");
        // 假设用伪代码表示:对每个子文件夹调用 ListFiles(子文件夹)
        // 对每个文件,打印文件名
    }
}

// 使用
RecursionDemo demo = new RecursionDemo();
Console.WriteLine(demo.Factorial(5));    // 120
Console.WriteLine(demo.Fibonacci(6));    // 8

递归的三要素

  1. 终止条件:什么时候不再继续调用自己(极其重要!否则死循环爆栈)
  2. 缩小规模:每次递归都让问题变小一点
  3. 递归调用:自己调用自己

5.11 局部函数(Local Function,C# 7.0+)

方法里面可以再声明方法(仅在当前方法内部可用):

public class MathProcessor
{
    public int Process(int a, int b)
    {
        // 局部函数:只在这个方法内可见
        int Add(int x, int y) => x + y;

        int Multiply(int x, int y) => x * y;

        int Square(int x) => x * x;

        // 使用局部函数
        int sum = Add(a, b);
        int product = Multiply(a, b);
        int aSquare = Square(a);
        int bSquare = Square(b);

        return sum + product + aSquare + bSquare;
    }
}

// 使用局部函数的典型场景:复杂的验证/转换逻辑
public string FormatAddress(string street, string city, string province)
{
    // 局部函数:复用验证逻辑
    bool IsValid(string value)
    {
        return !string.IsNullOrWhiteSpace(value) && value.Length > 1;
    }

    // 局部函数:复用格式化逻辑
    string Format(string value) => value?.Trim() ?? "未知";

    if (!IsValid(street) && !IsValid(city))
    {
        return "地址无效";
    }

    return $"{Format(province)}{Format(city)}{Format(street)}";
}

局部函数的优点

  • 把只在当前方法中使用的辅助逻辑封装起来
  • 比私有方法更"就近",更容易阅读
  • 可以访问外层方法的局部变量

5.12 方法返回多个值:元组(Tuple,C# 7.0+)

public class Analyzer
{
    // 返回元组:同时返回最大值、最小值、平均值
    public (int Max, int Min, double Average) Analyze(params int[] numbers)
    {
        if (numbers.Length == 0)
        {
            return (0, 0, 0);  // 返回元组字面量
        }

        int max = numbers.Max();
        int min = numbers.Min();
        double avg = numbers.Average();

        return (max, min, avg);
    }
}

// 使用
Analyzer analyzer = new Analyzer();
var result = analyzer.Analyze(85, 92, 78, 95, 88);

Console.WriteLine($"最高分:{result.Max}");      // 95
Console.WriteLine($"最低分:{result.Min}");      // 78
Console.WriteLine($"平均分:{result.Average}");   // 87.6

// 也可以用解构语法
(int highest, int lowest, double average) = analyzer.Analyze(60, 70, 80);
Console.WriteLine($"最高:{highest},最低:{lowest},平均:{average}");

总结

以上五章涵盖了 C# 类的基础核心:

章节 核心内容 一句话
什么是类 类的概念、引用类型、面向对象四大特性 类是对象的模板
类的基本语法 定义、创建、命名、项目组织 class 定义,new 创建
字段 实例字段、静态字段、readonly、const、默认值 字段存储数据,私有字段最安全
属性 get/set、自动属性、init、required、表达式体 属性封装字段,外观像字段本质是方法
方法 重载、可选参数、ref/out/in、递归、局部函数、元组 方法封装行为,提高复用性

学习路径建议

  1. 先手敲每个示例代码,运行看效果
  2. 改一改参数和逻辑,观察变化
  3. 用一个小项目(比如学生成绩管理)综合运用所学知识
  4. 遇到问题回头看对应章节,加深理解
0
博主关闭了当前页面的评论