CSharp(二十二)类(Class)定义与使用详解
目录
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 也变了!
上面这个例子中,s1 和 s2 指向的是内存中的同一个学生对象。改 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}"); // 通过属性读取名字
}
}
从这个例子可以看到:
- 类用
class关键字声明 - 类里面有 字段、属性、构造函数、方法
- 使用
new关键字来创建一个对象 - 创建出来的对象拥有类里定义的所有能力
下面我们就从最简单的语法开始,逐步深入。
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
初学者建议:现阶段专注于
class,record是进阶内容,先知道有这个东西就行。
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 属性初始化后不能修改
initvsprivate set:init更严格,只能在一开始设置,之后永远不能改;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
递归的三要素:
- 终止条件:什么时候不再继续调用自己(极其重要!否则死循环爆栈)
- 缩小规模:每次递归都让问题变小一点
- 递归调用:自己调用自己
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、递归、局部函数、元组 | 方法封装行为,提高复用性 |
学习路径建议:
- 先手敲每个示例代码,运行看效果
- 改一改参数和逻辑,观察变化
- 用一个小项目(比如学生成绩管理)综合运用所学知识
- 遇到问题回头看对应章节,加深理解