CSharp(二十八)分部类(Partial Class)详解
1. 什么是分部类
一句话解释
分部类(Partial Class) 允许你把一个类的定义拆分到多个文件中书写。编译器在编译时会把所有部分自动合并成一个完整的类。
生活类比
想象你在写一本很厚的小说:
| 方式 | 类比 | 说明 |
|---|---|---|
| 普通类 | 整本书写在一个大本子上 | 一个文件搞定 |
| 分部类(Partial) | 把书分成上、中、下三册 | 多个文件,最终还是一本书 |
关键是:读者(编译器)看完上中下三册后,得到的是一个完整的故事,而不是三个独立的故事。
图示理解
编译前(源代码) 编译后(IL代码)
┌─────────────────┐
│ Person1.cs │ ──┐
│ partial class │ │
│ { │ │ 合并 ┌─────────────────┐
│ 字段和属性 │ │ ───────→ │ class Person │
│ } │ │ │ { │
├─────────────────┤ │ │ 所有字段 │
│ Person2.cs │ │ │ 所有属性 │
│ partial class │ ──┤ │ 所有方法 │
│ { │ │ │ 构造函数 │
│ 方法 │ │ │ } │
│ } │ │ └─────────────────┘
├─────────────────┤ │
│ Person3.cs │ │
│ partial class │ ──┘
│ { │
│ 构造函数 │
│ } │
└─────────────────┘
2. 为什么需要分部类
问题场景
假如你要写一个"学生管理系统"的 Student 类,它需要包含:
- 10 个字段和属性(学号、姓名、性别、年龄、地址、电话...)
- 15 个方法(注册、选课、考试、毕业...)
- 5 个构造函数
- 3 个事件
如果全部写在一个文件里,这个文件可能轻松超过 500 行甚至上千行,阅读和维护都非常困难。
分部类的价值
| 问题 | 不用分部类 | 用分部类 |
|---|---|---|
| 文件太大 | 一个文件几百上千行,难找代码 | 按功能拆分到多个小文件,每个文件几十行 |
| 多人协作 | 多人修改同一文件,容易冲突 | 每人负责不同的文件,互不干扰 |
| 自动生成代码 | 手写代码和生成代码混在一起 | 生成代码放一个文件,手写代码放另一个文件 |
| 代码组织 | 不同功能的代码混在一起 | 字段一个文件、方法一个文件、逻辑分组清晰 |
3. 分部类的基本语法
核心关键字:partial
只需要在 class 关键字前面加一个 partial,就声明了一个分部类。
// 语法格式
// 访问修饰符 partial class 类名
// {
// 类成员
// }
最简单的例子
假设我们要定义一个 Calculator(计算器)类,把它拆成两个文件:
文件1:Calculator.Basic.cs —— 基础运算
public partial class Calculator
{
// 字段
private double _result;
// 加法
public double Add(double a, double b)
{
_result = a + b;
return _result;
}
// 减法
public double Subtract(double a, double b)
{
_result = a - b;
return _result;
}
}
文件2:Calculator.Advanced.cs —— 高级运算
public partial class Calculator
{
// 乘法
public double Multiply(double a, double b)
{
_result = a * b;
return _result;
}
// 除法
public double Divide(double a, double b)
{
if (b == 0)
{
throw new DivideByZeroException("除数不能为零!");
}
_result = a / b;
return _result;
}
// 获取当前结果
public double GetResult()
{
return _result;
}
}
使用这个类(和普通类完全一样):
class Program
{
static void Main(string[] args)
{
Calculator calc = new Calculator();
Console.WriteLine($"10 + 5 = {calc.Add(10, 5)}"); // 输出:10 + 5 = 15
Console.WriteLine($"10 - 3 = {calc.Subtract(10, 3)}"); // 输出:10 - 3 = 7
Console.WriteLine($"6 * 4 = {calc.Multiply(6, 4)}"); // 输出:6 * 4 = 24
Console.WriteLine($"20 / 4 = {calc.Divide(20, 4)}"); // 输出:20 / 4 = 5
Console.WriteLine($"当前结果:{calc.GetResult()}"); // 输出:当前结果:5
}
}
注意:使用
Calculator时,你完全感觉不到它是分部类。Add 方法虽然在文件1里定义的,Divide 方法虽然在文件2里定义的,但它们都属于同一个对象。你可以像调用普通类一样调用所有方法。
4. 代码是如何合并的
合并过程详解
当编译器编译你的项目时,它会这样处理分部类:
- 收集:找到所有带
partial class 类名的文件 - 合并:把所有部分的成员(字段、属性、方法等)合并到一起
- 编译:把合并后的完整类编译成 IL(中间语言)代码
动手验证
你可以通过下面的完整例子来感受合并的效果:
文件1:Person.Fields.cs
public partial class Person
{
// 基本信息字段
public string Name;
public int Age;
public string Address;
}
文件2:Person.Methods.cs
// 注意:这里需要 using System; 因为用了 Console
using System;
public partial class Person
{
// 自我介绍方法 —— 可以直接使用文件1中定义的 Name、Age
public void Introduce()
{
Console.WriteLine($"大家好,我叫{Name},今年{Age}岁,住在{Address}");
}
// 判断是否成年
public bool IsAdult()
{
return Age >= 18;
}
}
文件3:Person.Constructor.cs
public partial class Person
{
// 无参构造函数
public Person()
{
Name = "未知";
Age = 0;
Address = "未知";
}
// 带参构造函数
public Person(string name, int age, string address)
{
Name = name;
Age = age;
Address = address;
}
}
测试代码:
using System;
class Program
{
static void Main(string[] args)
{
// 使用带参构造函数(定义在文件3中)
Person p1 = new Person("张三", 25, "北京市朝阳区");
p1.Introduce(); // 输出:大家好,我叫张三,今年25岁,住在北京市朝阳区
Console.WriteLine($"是否成年:{p1.IsAdult()}"); // 输出:是否成年:True
// 使用无参构造函数(定义在文件3中)
Person p2 = new Person();
p2.Name = "李四";
p2.Age = 16;
p2.Address = "上海市浦东新区";
p2.Introduce(); // 输出:大家好,我叫李四,今年16岁,住在上海市浦东新区
Console.WriteLine($"是否成年:{p2.IsAdult()}"); // 输出:是否成年:False
}
}
关键理解:虽然三个文件中都写了
public partial class Person,但它们不是三个类,而是同一个类的三个部分。因此,方法Introduce()可以直接访问字段Name(即使它们写在不同的文件中)。
5. 分部类的核心规则
使用分部类时必须遵守以下规则:
规则1:所有部分都必须加 partial 关键字
// ✅ 正确:两个文件都用了 partial
// 文件A
public partial class MyClass { }
// 文件B
public partial class MyClass { }
// ❌ 错误:文件B 忘了写 partial
// 文件A
public partial class MyClass { }
// 文件B
public class MyClass { } // 编译错误!
规则2:访问修饰符必须一致
// ✅ 正确:都是 public
// 文件A
public partial class MyClass { }
// 文件B
public partial class MyClass { }
// ❌ 错误:访问修饰符不一致
// 文件A
public partial class MyClass { }
// 文件B
internal partial class MyClass { } // 编译错误!
注意:如果所有部分都不写访问修饰符,则默认都是
internal,这也算一致。
规则3:不能把同一个成员声明两次
// ❌ 错误:Name 字段在两个文件中都声明了
// 文件A
public partial class Person
{
public string Name; // 第一次声明
}
// 文件B
public partial class Person
{
public string Name; // 编译错误!重复声明
}
规则4:基类只需在一个部分中指定
// ✅ 正确:只在文件A中指定继承关系
// 文件A
public partial class Dog : Animal { }
// 文件B
public partial class Dog { } // 不需要再写 : Animal
// ❌ 错误:两个文件指定了不同的基类
// 文件A
public partial class Dog : Animal { }
// 文件B
public partial class Dog : Creature { } // 编译错误!
规则5:所有部分必须在同一个命名空间和同一个程序集中
// ✅ 正确:都在同一个命名空间中
namespace MyApp.Models
{
public partial class Student { }
}
namespace MyApp.Models // 同样的命名空间
{
public partial class Student { }
}
// ❌ 错误:不同命名空间
namespace MyApp.Models
{
public partial class Student { }
}
namespace MyApp.Services // 不同的命名空间
{
public partial class Student { } // 编译错误!这是另一个类了
}
规则速查表
| 项目 | 要求 |
|---|---|
partial 关键字 |
每个部分都必须有 |
| 访问修饰符 | 所有部分必须一致 |
| 类名 | 完全相同(包括大小写) |
| 命名空间 | 相同 |
| 程序集 | 同一个程序集(不能跨 DLL 拆分) |
| 基类 | 只在一个部分声明即可,多了会冲突 |
| 成员声明 | 同一个成员不能出现在两个部分中 |
| 接口实现 | 可以在不同部分中实现同一个接口的不同成员 |
6. 分部方法(Partial Method)
什么是分部方法
分部方法是分部类中的一个特殊函数。它允许你在一个部分中声明方法签名,在另一个部分中可选地实现它。
为什么需要分部方法
分部方法最主要的使用场景是:代码生成工具生成的钩子方法。
比如 Visual Studio 自动生成的 WinForms 代码中有很多类似 OnLoad()、OnClick() 这样的钩子,允许你在另一个文件中"挂接"自定义逻辑。如果你不需要,这些方法就完全不存在(编译时会被移除,零性能开销)。
基本语法
// 文件A(通常是自动生成的代码)
public partial class Order
{
// 声明分部方法(只有签名,没有方法体)
partial void OnOrderCreated();
public void CreateOrder()
{
Console.WriteLine("订单创建中...");
// 调用分部方法 —— 如果没实现,这行代码编译时会被移除
OnOrderCreated();
Console.WriteLine("订单创建完成!");
}
}
// 文件B(手写代码 —— 选择性实现)
public partial class Order
{
// 实现分部方法(可选!不实现也完全没问题)
partial void OnOrderCreated()
{
Console.WriteLine(">>> 日志:新订单已创建,时间:" + DateTime.Now);
}
}
测试:
Order order = new Order();
order.CreateOrder();
// 输出:
// 订单创建中...
// >>> 日志:新订单已创建,时间:2026-06-26 10:30:00
// 订单创建完成!
分部方法的规则
| 规则 | 说明 |
|---|---|
| 返回值 | 必须是 void(不能返回值) |
| 访问修饰符 | 不能写(隐式为 private) |
| 参数 | 可以有参数,但不能有 out 参数 |
| 是否必须实现 | 不必须。如果不实现,编译时声明和调用都会被完全移除 |
| 声明与实现 | 声明和实现都必须在分部类的不同部分中 |
| 关键字 | 声明和实现都用 partial 关键字 |
带参数的分部方法
// 声明(文件A)
public partial class Logger
{
partial void Log(string message, int level);
public void DoSomething()
{
// 调用
Log("开始执行任务", 1);
// ... 执行任务 ...
Log("任务执行完毕", 1);
}
}
// 实现(文件B)
public partial class Logger
{
partial void Log(string message, int level)
{
string levelText = level switch
{
1 => "信息",
2 => "警告",
3 => "错误",
_ => "未知"
};
Console.WriteLine($"[{levelText}] {DateTime.Now:HH:mm:ss} - {message}");
}
}
分部方法的核心价值
代码生成的文件 你手写的文件
┌─────────────────────────┐ ┌─────────────────────────┐
│ partial class MyForm │ │ partial class MyForm │
│ { │ │ { │
│ partial void OnLoad();│ 钩子 │ partial void OnLoad() │
│ │ ======> │ { │
│ void Initialize() │ │ // 你的自定义逻辑 │
│ { │ │ } │
│ // 自动生成的代码 │ │ } │
│ OnLoad(); ←──调用 │ └─────────────────────────┘
│ } │
│ } │
└─────────────────────────┘
如果不实现 OnLoad:调用 OnLoad() 那行代码编译时直接被删除,零开销!
如果实现了 OnLoad:就会像普通方法调用一样执行。
一句话总结:分部方法 = 代码生成工具给你留的"可选的填空位置",填了就执行,不填就当不存在。
7. 实战场景一:WinForms / WPF 自动生成代码
这是分部类最常见、最重要的应用场景。
问题
当你用 Visual Studio 拖拽一个按钮到 WinForms 窗口上时,VS 会自动生成大量代码(布局、样式、事件绑定等)。如果这些代码和你手写的业务逻辑混在同一个文件中:
- 你修改业务代码时可能不小心破坏自动生成的代码
- 自动生成代码更新时可能覆盖你的手写代码
- 文件变得非常臃肿
解决方案:分部类
VS 将代码自动分成两个文件:
你的项目目录
├── Form1.Designer.cs ← 自动生成的(不要手动修改!)
└── Form1.cs ← 你手写的业务逻辑
Form1.Designer.cs(自动生成,不要去改它):
partial class Form1
{
// 自动生成的控件声明
private System.Windows.Forms.Button btnSubmit;
private System.Windows.Forms.TextBox txtName;
private System.Windows.Forms.Label lblResult;
// 自动生成的初始化代码
private void InitializeComponent()
{
this.btnSubmit = new System.Windows.Forms.Button();
this.txtName = new System.Windows.Forms.TextBox();
this.lblResult = new System.Windows.Forms.Label();
// 位置、大小、样式设置...
this.btnSubmit.Text = "提交";
this.btnSubmit.Click += new System.EventHandler(this.btnSubmit_Click);
}
}
Form1.cs(你手写业务逻辑的地方):
using System;
using System.Windows.Forms;
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent(); // 调用自动生成的方法
}
// 按钮点击事件 —— 你的业务逻辑
private void btnSubmit_Click(object sender, EventArgs e)
{
string name = txtName.Text;
if (string.IsNullOrWhiteSpace(name))
{
lblResult.Text = "请输入姓名!";
}
else
{
lblResult.Text = $"你好,{name}!欢迎使用本系统。";
}
}
}
关键要点
┌──────────────────────────────────────────────────────────────┐
│ 重要!请记住: │
│ │
│ Designer.cs 文件 = 机器生成的 UI 骨架 │
│ .cs 文件 = 你写的业务逻辑 │
│ │
│ 永远不要手动修改 Designer.cs! │
│ 它会在你拖拽控件时自动更新。 │
└──────────────────────────────────────────────────────────────┘
8. 实战场景二:团队协作开发
问题
假设一个 OrderService(订单服务)类有 2000 行代码,包含:
- 订单创建逻辑(张三负责)
- 订单查询逻辑(李四负责)
- 订单支付逻辑(王五负责)
如果三个人都在同一个文件中写代码,Git 合并时必然产生冲突。
解决方案:按功能拆分成多个分部文件
文件:OrderService.Create.cs —— 张三负责
public partial class OrderService
{
// 创建订单
public Order CreateOrder(string customerName, List<OrderItem> items)
{
var order = new Order
{
Id = Guid.NewGuid(),
CustomerName = customerName,
Items = items,
Status = OrderStatus.Pending,
CreatedAt = DateTime.Now
};
// 计算总价
order.TotalAmount = items.Sum(i => i.Price * i.Quantity);
// 保存到数据库
_orderRepository.Save(order);
return order;
}
}
文件:OrderService.Query.cs —— 李四负责
public partial class OrderService
{
// 根据ID查询订单
public Order GetById(Guid orderId)
{
return _orderRepository.FindById(orderId);
}
// 查询用户的全部订单
public List<Order> GetByCustomer(string customerName)
{
return _orderRepository.FindByCustomer(customerName);
}
// 分页查询
public List<Order> GetPaged(int page, int pageSize)
{
return _orderRepository.FindPaged(page, pageSize);
}
}
文件:OrderService.Payment.cs —— 王五负责
public partial class OrderService
{
// 处理支付
public PaymentResult ProcessPayment(Guid orderId, PaymentInfo paymentInfo)
{
var order = _orderRepository.FindById(orderId);
if (order == null)
{
return new PaymentResult { Success = false, Message = "订单不存在" };
}
if (order.Status != OrderStatus.Pending)
{
return new PaymentResult { Success = false, Message = "订单状态不正确" };
}
// 调用支付网关
bool paymentSuccess = _paymentGateway.Charge(order.TotalAmount, paymentInfo);
if (paymentSuccess)
{
order.Status = OrderStatus.Paid;
order.PaidAt = DateTime.Now;
_orderRepository.Update(order);
return new PaymentResult { Success = true, Message = "支付成功" };
}
else
{
return new PaymentResult { Success = false, Message = "支付失败,请重试" };
}
}
}
文件:OrderService.Common.cs —— 公共部分
public partial class OrderService
{
// 依赖注入的仓储和支付网关
private readonly IOrderRepository _orderRepository;
private readonly IPaymentGateway _paymentGateway;
// 构造函数(也只需要在一处定义)
public OrderService(IOrderRepository orderRepository, IPaymentGateway paymentGateway)
{
_orderRepository = orderRepository;
_paymentGateway = paymentGateway;
}
}
团队协作的 Git 流程
张三改 OrderService.Create.cs ──→ Git 提交 ✓(无冲突)
李四改 OrderService.Query.cs ──→ Git 提交 ✓(无冲突)
王五改 OrderService.Payment.cs ──→ Git 提交 ✓(无冲突)
三个人改三个不同的文件 → 合并时完全没有冲突!
9. 实战场景三:大型类的模块化管理
即使是一个人开发,当类变得很大时,用分部类按功能模块拆分会极大提升可读性。
不好的写法(一个文件,全部混在一起)
// ❌ 一个巨大的文件 —— 难以维护
public class StudentManager
{
// 20 个字段
// 15 个属性
// 30 个方法
// 10 个事件
// 5 个构造函数
// 总共上千行...
// 你找一个方法要滚动很久...
}
好的写法(按功能拆分)
项目文件结构:
├── StudentManager.Core.cs ← 核心字段、属性、构造函数
├── StudentManager.Enroll.cs ← 选课相关方法
├── StudentManager.Grade.cs ← 成绩相关方法
├── StudentManager.Report.cs ← 报表统计方法
└── StudentManager.Event.cs ← 事件定义和处理
StudentManager.Core.cs:
public partial class StudentManager
{
// ===== 核心数据和构造函数 =====
private List<Student> _students = new List<Student>();
private List<Course> _courses = new List<Course>();
public string SchoolName { get; set; }
public int TotalStudents => _students.Count;
public StudentManager(string schoolName)
{
SchoolName = schoolName;
}
}
StudentManager.Enroll.cs:
public partial class StudentManager
{
// ===== 选课相关方法 =====
public bool EnrollStudent(Student student, Course course)
{
if (course.CurrentStudents >= course.MaxCapacity)
{
Console.WriteLine($"课程 {course.Name} 已满,选课失败!");
return false;
}
student.Courses.Add(course);
course.CurrentStudents++;
Console.WriteLine($"{student.Name} 成功选修 {course.Name}");
return true;
}
public void DropCourse(Student student, Course course)
{
if (student.Courses.Remove(course))
{
course.CurrentStudents--;
Console.WriteLine($"{student.Name} 退选了 {course.Name}");
}
}
}
StudentManager.Grade.cs:
public partial class StudentManager
{
// ===== 成绩相关方法 =====
public void SetGrade(Student student, Course course, double score)
{
if (!student.Courses.Contains(course))
{
Console.WriteLine($"{student.Name} 没有选修 {course.Name},不能设置成绩");
return;
}
student.Grades[course.Id] = score;
Console.WriteLine($"{student.Name} 的 {course.Name} 成绩设为:{score}");
}
public double GetAverageGrade(Student student)
{
if (student.Grades.Count == 0) return 0;
return student.Grades.Values.Average();
}
}
拆分前后对比
| 维度 | 拆分前 | 拆分后 |
|---|---|---|
| 单文件行数 | 1000+ 行 | 每个文件 50-100 行 |
| 查找方法 | 疯狂滚动/搜索 | 直接打开对应文件 |
| 与功能无关的干扰 | 高(其他功能的代码也在) | 低(只看相关功能的代码) |
| 改坏其他功能的概率 | 较高 | 较低 |
10. 综合示例
下面是一个完整的实战例子,模拟一个"图书管理系统"的 Book 类:
Book.Core.cs —— 核心属性
using System;
public partial class Book
{
// ===== 基本信息属性 =====
public string ISBN { get; set; }
public string Title { get; set; }
public string Author { get; set; }
public decimal Price { get; set; }
public DateTime PublishDate { get; set; }
// ===== 库存信息 =====
public int TotalCopies { get; set; }
public int BorrowedCopies { get; set; }
// ===== 计算属性 =====
public int AvailableCopies => TotalCopies - BorrowedCopies;
public bool IsAvailable => AvailableCopies > 0;
// ===== 标签 =====
public List<string> Tags { get; set; } = new List<string>();
}
Book.Borrow.cs —— 借阅/归还
using System;
public partial class Book
{
// ===== 借阅 =====
public bool Borrow(string borrowerName)
{
if (!IsAvailable)
{
Console.WriteLine($"《{Title}》已全部借出,暂时不可借阅。");
return false;
}
BorrowedCopies++;
Console.WriteLine($"《{Title}》被 {borrowerName} 借走。");
Console.WriteLine($" 剩余可借:{AvailableCopies}/{TotalCopies}");
return true;
}
// ===== 归还 =====
public bool Return(string borrowerName)
{
if (BorrowedCopies <= 0)
{
Console.WriteLine($"《{Title}》没有借出记录,无法归还。");
return false;
}
BorrowedCopies--;
Console.WriteLine($"{borrowerName} 归还了《{Title}》。");
Console.WriteLine($" 剩余可借:{AvailableCopies}/{TotalCopies}");
return true;
}
}
Book.Display.cs —— 显示信息
using System;
public partial class Book
{
// ===== 显示详细信息 =====
public void DisplayInfo()
{
Console.WriteLine("══════════════════════════════");
Console.WriteLine($" 书名:{Title}");
Console.WriteLine($" 作者:{Author}");
Console.WriteLine($" ISBN:{ISBN}");
Console.WriteLine($" 价格:¥{Price:F2}");
Console.WriteLine($" 出版日期:{PublishDate:yyyy-MM-dd}");
Console.WriteLine($" 库存状态:{AvailableCopies}/{TotalCopies}(可借/总数)");
Console.WriteLine($" 标签:{string.Join(", ", Tags)}");
Console.WriteLine("══════════════════════════════");
}
// ===== 显示简要信息 =====
public void DisplayBrief()
{
string status = IsAvailable ? "✓ 可借" : "✗ 已借完";
Console.WriteLine($"《{Title}》- {Author} | ¥{Price} | {status}");
}
}
Book.Constructor.cs —— 构造函数和工厂方法
using System;
public partial class Book
{
// ===== 默认构造函数 =====
public Book()
{
ISBN = "";
Title = "未知";
Author = "未知";
Price = 0;
PublishDate = DateTime.Now;
TotalCopies = 1;
BorrowedCopies = 0;
}
// ===== 完整构造函数 =====
public Book(string isbn, string title, string author, decimal price,
DateTime publishDate, int totalCopies)
{
ISBN = isbn;
Title = title;
Author = author;
Price = price;
PublishDate = publishDate;
TotalCopies = totalCopies;
BorrowedCopies = 0;
}
// ===== 工厂方法:快速创建一本书 =====
public static Book CreateQuick(string title, string author, decimal price)
{
return new Book
{
ISBN = Guid.NewGuid().ToString("N").Substring(0, 13),
Title = title,
Author = author,
Price = price,
PublishDate = DateTime.Now,
TotalCopies = 3
};
}
}
Program.cs —— 测试
using System;
class Program
{
static void Main(string[] args)
{
// 方式1:工厂方法快速创建
Book book1 = Book.CreateQuick("C#从入门到精通", "张三", 79.90m);
book1.Tags.AddRange(new[] { "编程", "C#", "入门" });
book1.DisplayBrief();
Console.WriteLine();
// 方式2:构造函数创建
Book book2 = new Book(
"978-7-111-12345-6",
"设计模式之美",
"李四",
99.00m,
new DateTime(2025, 1, 15),
5
);
book2.Tags.AddRange(new[] { "设计模式", "进阶", "架构" });
// 借阅和归还
book2.DisplayInfo();
book2.Borrow("王五");
book2.Borrow("赵六");
book2.Borrow("钱七");
Console.WriteLine();
book2.Return("王五");
Console.WriteLine();
// 最终状态
book2.DisplayBrief();
}
}
运行结果:
《C#从入门到精通》- 张三 | ¥79.90 | ✓ 可借
══════════════════════════════
书名:设计模式之美
作者:李四
ISBN:978-7-111-12345-6
价格:¥99.00
出版日期:2025-01-15
库存状态:5/5(可借/总数)
标签:设计模式, 进阶, 架构
══════════════════════════════
《设计模式之美》被 王五 借走。
剩余可借:4/5
《设计模式之美》被 赵六 借走。
剩余可借:3/5
《设计模式之美》被 钱七 借走。
剩余可借:2/5
王五 归还了《设计模式之美》。
剩余可借:3/5
《设计模式之美》- 李四 | ¥99.00 | ✓ 可借
11. 常见问题(FAQ)
Q1:分部类可以跨项目(程序集)吗?
不可以。 分部类必须在同一个程序集(同一个 .dll 或 .exe)中。如果你想跨程序集扩展一个类,请使用扩展方法(Extension Method)。
Q2:分部类能继承吗?
可以。 分部类的继承规则和普通类完全一样:
// 文件A
public partial class Dog : Animal { }
// 文件B
public partial class Dog { } // 不需要重复写 : Animal
子类也可以用分部类:
// 文件A
public partial class Puppy : Dog { }
// 文件B
public partial class Puppy { }
Q3:一个分部类最多能拆成多少个文件?
理论上没有限制。但实际开发中,超过 3-5 个文件就需要反思:是不是这个类承担的职责太多了?可能需要考虑重构,拆分成多个独立的类。
Q4:接口可以用 partial 吗?
可以。 接口也支持分部定义:
// 文件A
public partial interface IRepository
{
void Add(object entity);
void Delete(object entity);
}
// 文件B
public partial interface IRepository
{
object FindById(int id);
List<object> FindAll();
}
Q5:结构体(struct)可以用 partial 吗?
可以。 分部结构体和分部类的用法完全一样:
// 文件A
public partial struct Point
{
public int X;
public int Y;
}
// 文件B
public partial struct Point
{
public double Distance => Math.Sqrt(X * X + Y * Y);
}
Q6:如果部分文件用了 abstract、sealed 或 static,每个文件都要写吗?
不需要,但必须一致。 只需要在其中一个部分中声明即可:
// ✅ 正确
// 文件A
public abstract partial class BaseService { }
// 文件B
public partial class BaseService { } // 自动也是 abstract
// ❌ 错误:不一致
// 文件A
public abstract partial class BaseService { }
// 文件B
public sealed partial class BaseService { } // 冲突!
Q7:分部类和继承有什么区别?
这是初学者最容易混淆的问题:
| 对比维度 | 分部类(Partial) | 继承(Inheritance) |
|---|---|---|
| 本质 | 同一个类的代码拆分 | 不同类之间的关系 |
| 关键字 | partial |
: 基类名 |
| 成员访问 | 所有成员都是自己的 | 只能访问基类中允许的成员 |
| 类的数量 | 最终是一个类 | 至少两个类(父类和子类) |
| 内存中的对象 | 一个对象 | 子类是一个对象,其中包含基类部分 |
| 可以跨程序集吗 | ❌ 不可以 | ✅ 可以 |
简单区分口诀:分部类 ="一个人穿不同的衣服",继承 = "父子关系"。
12. 小结与练习
核心要点回顾
┌─────────────────────────────────────────────────────────────┐
│ 分部类核心概念总结 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. partial 关键字 → 把一个类拆分到多个文件 │
│ │
│ 2. 编译器合并 → 最终仍然是一个完整的类 │
│ │
│ 3. 所有部分 → 必须用 partial,访问修饰符必须一致 │
│ │
│ 4. 主要用途 → 自动生成代码分离 / 团队协作 / 大文件拆分 │
│ │
│ 5. 分部方法 → 可选实现的方法钩子,不实现则零开销 │
│ │
│ 6. 使用建议 → 一般 2-3 个部分足够,太多说明类设计有问题 │
│ │
└─────────────────────────────────────────────────────────────┘
动手练习
练习1(基础):创建一个 Temperature 分部类,拆成两个文件:
- 文件1:字段
_celsius(double),属性Celsius、Fahrenheit(摄氏/华氏互转) - 文件2:方法
Display()输出当前温度
练习2(进阶):创建一个 BankAccount(银行账户)分部类,拆成三个文件:
BankAccount.Core.cs:账号、户名、余额等字段和属性BankAccount.Transaction.cs:存款Deposit()、取款Withdraw()、转账Transfer()BankAccount.Report.cs:打印交易记录PrintStatement()
练习3(综合):模仿 WinForms 的场景:
- 创建一个分部类
Game,模拟一个游戏类 - Designer 文件(自动生成的):定义玩家血量、等级字段,以及一个分部方法
OnLevelUp() - 手写文件:实现
OnLevelUp(),当玩家升级时输出祝贺信息;实现GainExperience()方法,满经验时调用OnLevelUp()
学习建议:分部类的关键在于理解"形式上是多个文件,实质上是一个类"。多在 WinForms/WPF 项目或团队协作中实践,自然就能体会到它的好处。