C#教程

关注公众号 jb51net

关闭
首页 > 软件编程 > C#教程 > C#分部类和分部方法

C#中分部类和分部方法(partial)

作者:武藤一雄

分部类(partial class)是C#中用于解决机器生成代码与人工编写代码冲突的一种特性,主要用于自动生成的代码场景,本文给大家介绍C#中分部类和分部方法(partial),感兴趣的朋友跟随小编一起看看吧

分部类(Partial Class).cs

这个特性不是用来解决业务逻辑混乱的,而是为了解决机器生成代码人工编写代码之间的冲突。

核心应用场景

  1. 自动生成的代码(Source Generators / Designer) 这是最主要的使用场景。当你使用 Entity Framework (DB First)、或者早期的 WinForms/WPF 时,IDE 会根据数据库或 UI 设计图自动生成大量的 C# 代码。
    1. 痛点:如果你直接在自动生成的类里写逻辑,下次重新生成时,你的代码会被覆盖。
    2. 方案:机器写一个 partial class 放在 A 文件,你写一个 partial class 放在 B 文件。互不干扰,平安无事。
  2. 多人协作开发 当一个类(比如一个极其复杂的业务控制器)由于历史原因变得非常庞大时,虽然最好的做法是重构,但在短期内为了避免 Git 合并冲突(Merge Conflict),可以使用分部类让不同的人在不同的文件里工作。
  3. 代码组织与关注点分离 有时候一个类需要实现多个复杂的接口,或者内部包含大量的常量定义。你可以通过分部类将“接口实现”、“私有成员”、“公共 API”分别放在不同的文件中,提高可读性。

代码示例

假设你有一个用户类,一分部由数据库工具生成,一分部是你自己写的验证逻辑。

文件 1: User.Generated.cs (机器生成,不要动)

public partial class User
{
    public int Id { get; set; }
    public string Name { get; set; }
}

文件 2: User.Logic.cs (你写的逻辑)

public partial class User
{
    public bool Validate()
    {
        return !string.IsNullOrEmpty(Name);
    }
}

在 C# 中,您可以使用 partial 关键字将类、结构、方法或接口的实现拆分到多个 .cs 文件中。编译器在编译程序时会将来自多个 .cs 文件的所有实现组合起来。

考虑以下包含 Employee 类的 EmployeeProps.csEmployeeMethods.cs 文件。

// EmployeeProps
 public partial class Employee
 {
     public int EmpId { get; set; }
     public string Name { get; set; }
 }
// EmployeeMethods
 public partial class Employee
 {
     //constructor
     public Employee(int id, string name){
         this.EmpId = id;
         this.Name = name;
     }
     public void DisplayEmpInfo() {
         Console.WriteLine(this.EmpId + " " this.Name);
     }
 }

上面,EmployeeProps.cs 包含 Employee 类的属性,而 EmployeeMethods.cs 包含 Employee 类的所有方法。这些文件将被编译成一个 Employee 类。

示例:组合类

 public class Employee
 {
     public int EmpId { get; set; }
     public string Name { get; set; }
     public Employee(int id, string name){
         this.EmpId = id;
         this.Name = name;
     }
     public void DisplayEmpInfo(){
         Console.WriteLine(this.EmpId + " " this.Name );
     }
 }

分部类的规则

分部方法

分部类还支持分部方法。这就像是一个“钩子(Hook)”。

// 在 A 文件定义钩子
partial void OnDataChanged();
// 在 B 文件实现钩子(如果不实现,编译器会直接删掉这个调用,零性能损耗)
partial void OnDataChanged()
{
    Console.WriteLine("数据变了!");
}

分部类或结构可以包含一个方法,该方法被拆分到分部类或结构的两个单独的 .cs 文件中。其中一个 .cs 文件必须包含该方法的签名,而另一个文件可以包含分部方法的可选实现。方法的声明和实现都必须具有 partial 关键字。

 public partial class Employee
 {
     public Employee() {
         GenerateEmpId();
     }
     public int EmpId { get; set; }
     public string Name { get; set; }
     partial void GenerateEmployeeId();
 }
 public partial class Employee
 {
     partial void GenerateEmployeeId()
     {
         this.EmpId = random();
     }
 }

上面,EmployeeProps.cs 包含分部方法 GenerateEmployeeId() 的声明,该方法在构造函数中使用。EmployeeMethods.cs 包含 GenerateEmployeeId() 方法的实现。以下代码演示了如何创建一个使用分部方法的 Employee 类对象。

 class Program
 {
     static void Main(string[] args)
     {
         var emp = new Employee();
         Console.WriteLine(emp.EmpId); // prints genereted id
         Console.ReadLine();
     }
 }

分部方法的规则

简单粗暴地说,分部方法(Partial Method)的意义在于:给机器生成的代码“预留后悔药”,同时不给运行环境增加一丁点负担。

这种设计解决了软件工程中一个经典的矛盾:“自动化生成的代码”与“个性化业务需求”的冲突。

分部方法的意义

核心意义:安全的“挂钩(Hook)”

在大型项目中(尤其是使用 Entity Framework 或 WPF 时),工具会自动生成成千上万行代码。

性能极致

这是它和 Virtual 方法或 Event(事件)最大的区别。

场景对比:为什么不直接定义一个普通方法?

方案机器生成代码中的动作开发者动作后果
普通方法直接写死逻辑无法干预必须修改生成的文件,重构即地狱。
虚方法 (Virtual)定义 virtual 方法override 重写存在虚函数表查询开销,必须实例化对象。
分部方法声明 partial 方法并调用实现 partial 方法不实现则代码消失,实现则无缝嵌入,两全其美。

只要是虚方法(Virtual Method),哪怕你大括号里一个字都不写,它在运行时依然有“身份支出”。

作为老司机,我给你拆解一下这背后的“隐形账单”。

虚方法的“买路钱”:vtable 查找

虚方法的本质是运行时多态。编译器无法在编译时确定你到底要执行哪个方法,所以它得留个心眼。

在 C# 中,虚表(VTable) 是一种底层机制,用于支持面向对象编程中的多态性和虚方法调用。它是一个数据结构,存储了类的虚方法的地址,以便在运行时动态调用正确的方法实现。

哪怕方法体是空的,这套“三级跳”的动作一次都少不了。

无法“内联”的损失

这是性能差距最大的地方。

性能对比表(Partial & Virtual)

特性分部方法 (Partial Method)虚方法 (Virtual Method)
未实现/空实现时彻底消失。编译器直接把调用代码删了。依然存在。CPU 照样执行跳转指令。
内存占用0 额外占用。每个对象多出指针空间(4/8 bytes)。
调用速度极快(直接调用或内联)。较慢(需要查表跳转)。
灵活性编译时决定,不能跨程序集。运行时决定,支持动态加载插件。

什么时候用哪种?

你可以根据这个逻辑来选:

该执行哪一个方法?

答案很简单:你不需要知道,也不需要选。

因为在 C# 中,分部方法(Partial Method)遵循的是**“有且仅有一个实现”**的原则。

编译器眼里的“合并”

在你提供的代码中,编译器会报错,原因是你写了两个实现(方法体 {}。你可以把分部方法的机制理解为:

如果你在两个地方都写了 { ... }(哪怕一个是空的 {}),编译器就会像见到两个同名同姓的人一样陷入混乱,直接报错。

正确的执行逻辑

如果你希望在某些情况下输出 “BBB”,在另一些情况下“什么都不做”,你不能靠定义两个方法实现,而是要靠逻辑判断

正确的写法应该是这样的:

// 【生成代码部分】
public partial class MyClass
{
    // 只有声明,没有大括号!这代表“此处预留钩子”
    partial void OnOpen();
    public void DoWork()
    {
        OnOpen(); // 编译器在这里调用钩子
    }
}
// 【开发者代码部分】
public partial class MyClass
{
    // 如果你想执行逻辑,就实现它
    partial void OnOpen()
    {
        if (DateTime.Now.Hour > 12)
        {
            Console.WriteLine("BBB");
        }
        // 否则什么都不做(这就是你想要的“空方法”效果)
    }
}

为什么分部方法不能像普通方法那样“重载”?

分部方法的设计初衷不是为了让你做选择题,而是为了**“无缝注入”**。

它不存在“调用哪一个”的问题,因为最终编译出来的类里,只有一个 OnOpen

易混淆

那这样执行的时候,不会冲突么,或者有两个分部类,他会执行哪一个

不会冲突,因为在编译器眼里,它们压根就不是两个类,而是一个类

你可以把“分部类”想象成一块拼图:源代码阶段你手里拿着几块碎片,但编译完之后,这些碎片已经严丝合缝地粘在一起,变成了一块完整的板子。

为什么不会冲突?

  1. 物理上的“合体”:当你点击“生成”或“运行”时,C# 编译器(Roslyn)会先扫描所有带 partial 关键字的类。如果发现命名空间相同、类名相同的 partial 类,它会把它们所有的成员(字段、属性、方法、构造函数)全部收集起来,塞进同一个 IL(中间语言)定义里。
  2. 符号的唯一性:由于它们最终是一个类,所以普通的 C# 规则依然适用。比如,你在 A 文件里定义了 public int Id,就不能在 B 文件里再定义一个 public int Id,否则编译器会报错:“类已经包含该成员的定义”。

执行顺序是怎么算的?

既然物理上合二为一了,就不存在“执行哪一个”的问题。程序的入口(比如 Main 方法)调用该类的方法时,该在哪就在哪。

唯一需要注意的顺序:构造函数

如果两个分部类里都写了构造函数,情况如下:

避坑指南

虽然编译器帮你合体了,但在开发中要遵循以下守则:

同一个成员(方法)在同一个类中不能有多个实现

// 生成代码分部
public partial class MyClass
{
	public string Name{get;set;}
    partial void OnOpen()
	{
		Console.WriteLine("AAA");
	};
}
// 开发者代码分部
public partial class MyClass
{
	public string Age{get;set;}
    partial void OnOpen()
	{
		Console.WriteLine("BBB");
	};
}

虽然 partial 类可以将代码拆分,但它依然遵循 C# 最基本的语法规则:同一个成员(方法)在同一个类中不能有多个实现

为什么会报错?

分部方法(Partial Method)的设计初衷是:一个地方定义声明(类似占位符),另一个地方编写实现(可选)。

在你的例子中,两个分部类都写了具体的 { ... } 逻辑块,编译器会认为你对同一个方法定义了两次,直接抛出编译错误:

CS0757: 分部方法声明具有多个实现方法。

正确的用法应该是这样

// ------------------------------------
// 文件 A:生成代码分部(通常由工具生成)
// ------------------------------------
public partial class MyClass
{
    public string Name { get; set; }
    // 这里只定义“钩子”的签名,不写大括号逻辑
    partial void OnOpen();
    public void Trigger()
    {
        OnOpen(); // 编译器在这里埋下调用点
    }
}
// ------------------------------------
// 文件 B:开发者代码分部(由你编写)
// ------------------------------------
public partial class MyClass
{
    public string Age { get; set; }
    // 这里编写具体的业务逻辑
    partial void OnOpen()
    {
        Console.WriteLine("BBB");
    }
}

执行结果: 调用 Trigger() 方法时,屏幕会输出:BBB

如果开发者没写代码会怎样?

这是分部方法最聪明的地方。如果你在文件 B 中没有编写 partial void OnOpen() 的实现:

  1. 编译器会发现这个方法没被实现。
  2. 为了绝对的性能,编译器会直接删掉文件 A 中的方法声明,并删掉所有对 OnOpen() 的调用。
  3. 程序运行时,就像这个方法从未存在过一样,没有任何性能开销。

专业词汇详解

到此这篇关于C#中分部类和分部方法(partial)的文章就介绍到这了,更多相关C#分部类和分部方法内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文