C#中分部類和分部方法(partial)
分部類(Partial Class).cs
這個(gè)特性不是用來解決業(yè)務(wù)邏輯混亂的,而是為了解決機(jī)器生成代碼與人工編寫代碼之間的沖突。
核心應(yīng)用場(chǎng)景
- 自動(dòng)生成的代碼(Source Generators / Designer) 這是最主要的使用場(chǎng)景。當(dāng)你使用 Entity Framework (DB First)、或者早期的 WinForms/WPF 時(shí),IDE 會(huì)根據(jù)數(shù)據(jù)庫或 UI 設(shè)計(jì)圖自動(dòng)生成大量的 C# 代碼。
- 痛點(diǎn):如果你直接在自動(dòng)生成的類里寫邏輯,下次重新生成時(shí),你的代碼會(huì)被覆蓋。
- 方案:機(jī)器寫一個(gè)
partial class放在 A 文件,你寫一個(gè)partial class放在 B 文件。互不干擾,平安無事。
- 多人協(xié)作開發(fā) 當(dāng)一個(gè)類(比如一個(gè)極其復(fù)雜的業(yè)務(wù)控制器)由于歷史原因變得非常龐大時(shí),雖然最好的做法是重構(gòu),但在短期內(nèi)為了避免 Git 合并沖突(Merge Conflict),可以使用分部類讓不同的人在不同的文件里工作。
- 代碼組織與關(guān)注點(diǎn)分離 有時(shí)候一個(gè)類需要實(shí)現(xiàn)多個(gè)復(fù)雜的接口,或者內(nèi)部包含大量的常量定義。你可以通過分部類將“接口實(shí)現(xiàn)”、“私有成員”、“公共 API”分別放在不同的文件中,提高可讀性。

代碼示例
假設(shè)你有一個(gè)用戶類,一分部由數(shù)據(jù)庫工具生成,一分部是你自己寫的驗(yàn)證邏輯。
文件 1: User.Generated.cs (機(jī)器生成,不要?jiǎng)?
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 關(guān)鍵字將類、結(jié)構(gòu)、方法或接口的實(shí)現(xiàn)拆分到多個(gè) .cs 文件中。編譯器在編譯程序時(shí)會(huì)將來自多個(gè) .cs 文件的所有實(shí)現(xiàn)組合起來。
考慮以下包含 Employee 類的 EmployeeProps.cs 和 EmployeeMethods.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 類的所有方法。這些文件將被編譯成一個(gè) 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 );
}
}分部類的規(guī)則
- 所有分部類定義必須在相同的程序集和命名空間中。
- 所有分部必須具有相同的可訪問性,例如 public 或 private 等。
- 如果任何分部被聲明為 abstract、sealed 或基類型,則整個(gè)類都被聲明為相同的類型。
- 不同的分部可以有不同的基類型,因此最終類將繼承所有基類型。
partial修飾符只能緊接在class、struct或interface關(guān)鍵字之前。- 允許嵌套分部類型。
分部方法
分部類還支持分部方法。這就像是一個(gè)“鉤子(Hook)”。
// 在 A 文件定義鉤子
partial void OnDataChanged();
// 在 B 文件實(shí)現(xiàn)鉤子(如果不實(shí)現(xiàn),編譯器會(huì)直接刪掉這個(gè)調(diào)用,零性能損耗)
partial void OnDataChanged()
{
Console.WriteLine("數(shù)據(jù)變了!");
}分部類或結(jié)構(gòu)可以包含一個(gè)方法,該方法被拆分到分部類或結(jié)構(gòu)的兩個(gè)單獨(dú)的 .cs 文件中。其中一個(gè) .cs 文件必須包含該方法的簽名,而另一個(gè)文件可以包含分部方法的可選實(shí)現(xiàn)。方法的聲明和實(shí)現(xiàn)都必須具有 partial 關(guān)鍵字。
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() 的聲明,該方法在構(gòu)造函數(shù)中使用。EmployeeMethods.cs 包含 GenerateEmployeeId() 方法的實(shí)現(xiàn)。以下代碼演示了如何創(chuàng)建一個(gè)使用分部方法的 Employee 類對(duì)象。
class Program
{
static void Main(string[] args)
{
var emp = new Employee();
Console.WriteLine(emp.EmpId); // prints genereted id
Console.ReadLine();
}
}分部方法的規(guī)則
- 分部方法必須使用
partial關(guān)鍵字,并且必須返回void。 - 分部方法可以有
in或ref參數(shù),但不能有out參數(shù)。 - 分部方法是隱式私有方法,因此不能是虛方法。
- 分部方法可以是靜態(tài)方法。
- 分部方法可以是泛型方法。
簡單粗暴地說,分部方法(Partial Method)的意義在于:給機(jī)器生成的代碼“預(yù)留后悔藥”,同時(shí)不給運(yùn)行環(huán)境增加一丁點(diǎn)負(fù)擔(dān)。
這種設(shè)計(jì)解決了軟件工程中一個(gè)經(jīng)典的矛盾:“自動(dòng)化生成的代碼”與“個(gè)性化業(yè)務(wù)需求”的沖突。
分部方法的意義
核心意義:安全的“掛鉤(Hook)”
在大型項(xiàng)目中(尤其是使用 Entity Framework 或 WPF 時(shí)),工具會(huì)自動(dòng)生成成千上萬行代碼。
- 如果不提供分部方法:你想在類初始化時(shí)加一行日志,你只能改動(dòng)生成的文件。一旦數(shù)據(jù)庫表結(jié)構(gòu)變了,你重新生成代碼,你寫的日志代碼瞬間被沖掉。你會(huì)陷入“改了丟,丟了再改”的死循環(huán)。
- 有了分部方法:機(jī)器在生成的代碼里到處埋下
partial void OnSomething()。它告訴你:“我在這里留了個(gè)口子,你想寫邏輯就去另一個(gè)文件寫,不寫也沒關(guān)系。”
性能極致
這是它和 Virtual 方法或 Event(事件)最大的區(qū)別。
- Virtual/Event:即便沒人重寫方法,即便沒有訂閱者,程序運(yùn)行時(shí)依然要分配內(nèi)存、進(jìn)行跳轉(zhuǎn)判斷。
- Partial Method:如果你不寫實(shí)現(xiàn),編譯器在生成
.dll時(shí)會(huì)直接抹除這個(gè)調(diào)用。- 意義:對(duì)于需要極高性能的基礎(chǔ)底層庫,既給了開發(fā)者擴(kuò)展的能力,又保證了不擴(kuò)展時(shí)是“零開銷”。
場(chǎng)景對(duì)比:為什么不直接定義一個(gè)普通方法?
| 方案 | 機(jī)器生成代碼中的動(dòng)作 | 開發(fā)者動(dòng)作 | 后果 |
|---|---|---|---|
| 普通方法 | 直接寫死邏輯 | 無法干預(yù) | 必須修改生成的文件,重構(gòu)即地獄。 |
| 虛方法 (Virtual) | 定義 virtual 方法 | override 重寫 | 存在虛函數(shù)表查詢開銷,必須實(shí)例化對(duì)象。 |
| 分部方法 | 聲明 partial 方法并調(diào)用 | 實(shí)現(xiàn) partial 方法 | 不實(shí)現(xiàn)則代碼消失,實(shí)現(xiàn)則無縫嵌入,兩全其美。 |
只要是虛方法(Virtual Method),哪怕你大括號(hào)里一個(gè)字都不寫,它在運(yùn)行時(shí)依然有“身份支出”。
作為老司機(jī),我給你拆解一下這背后的“隱形賬單”。
虛方法的“買路錢”:vtable 查找
虛方法的本質(zhì)是運(yùn)行時(shí)多態(tài)。編譯器無法在編譯時(shí)確定你到底要執(zhí)行哪個(gè)方法,所以它得留個(gè)心眼。
- vtable(虛函數(shù)表):每個(gè)包含虛方法的類,在內(nèi)存里都會(huì)多出一張表,記錄著方法的地址。
- vptr(虛表指針):每一個(gè)該類的實(shí)例(對(duì)象),頭部都會(huì)偷偷多占 4 到 8 個(gè)字節(jié),專門用來指向這張表。
- 查找開銷:每次調(diào)用虛方法,CPU 都要玩一次“三級(jí)跳”:
- 找到對(duì)象的指針。
- 通過指針找到
vtable。 - 在
vtable里查到方法真正的內(nèi)存地址,最后才跳過去執(zhí)行。
在 C# 中,虛表(VTable) 是一種底層機(jī)制,用于支持面向?qū)ο缶幊讨械亩鄳B(tài)性和虛方法調(diào)用。它是一個(gè)數(shù)據(jù)結(jié)構(gòu),存儲(chǔ)了類的虛方法的地址,以便在運(yùn)行時(shí)動(dòng)態(tài)調(diào)用正確的方法實(shí)現(xiàn)。
哪怕方法體是空的,這套“三級(jí)跳”的動(dòng)作一次都少不了。
無法“內(nèi)聯(lián)”的損失
這是性能差距最大的地方。
- 內(nèi)聯(lián)(Inlining):JIT 編譯器發(fā)現(xiàn)一個(gè)方法很簡單,會(huì)直接把代碼“平鋪”到調(diào)用點(diǎn),省掉函數(shù)調(diào)用的壓棧和出棧。
- 虛方法的阻礙:因?yàn)樘摲椒ㄒ竭\(yùn)行時(shí)才知道跑哪段邏輯,JIT 編譯器通常不敢對(duì)其進(jìn)行內(nèi)聯(lián)優(yōu)化。
性能對(duì)比表(Partial & Virtual)
| 特性 | 分部方法 (Partial Method) | 虛方法 (Virtual Method) |
|---|---|---|
| 未實(shí)現(xiàn)/空實(shí)現(xiàn)時(shí) | 徹底消失。編譯器直接把調(diào)用代碼刪了。 | 依然存在。CPU 照樣執(zhí)行跳轉(zhuǎn)指令。 |
| 內(nèi)存占用 | 0 額外占用。 | 每個(gè)對(duì)象多出指針空間(4/8 bytes)。 |
| 調(diào)用速度 | 極快(直接調(diào)用或內(nèi)聯(lián))。 | 較慢(需要查表跳轉(zhuǎn))。 |
| 靈活性 | 編譯時(shí)決定,不能跨程序集。 | 運(yùn)行時(shí)決定,支持動(dòng)態(tài)加載插件。 |
什么時(shí)候用哪種?
你可以根據(jù)這個(gè)邏輯來選:
- 用
partial的情況:你寫的是底層框架或代碼生成工具,你想給開發(fā)者留個(gè)“可選的開關(guān)”,而且對(duì)性能有極致追求。如果不寫邏輯,你希望這功能像從未存在過一樣。 - 用
virtual的情況:你需要真正的多態(tài)。比如你寫了一個(gè)Animal類,你想讓Dog和Cat在運(yùn)行時(shí)表現(xiàn)出不同的Eat()行為。
該執(zhí)行哪一個(gè)方法?
答案很簡單:你不需要知道,也不需要選。
因?yàn)樵?C# 中,分部方法(Partial Method)遵循的是**“有且僅有一個(gè)實(shí)現(xiàn)”**的原則。
編譯器眼里的“合并”
在你提供的代碼中,編譯器會(huì)報(bào)錯(cuò),原因是你寫了兩個(gè)實(shí)現(xiàn)(方法體 {})。你可以把分部方法的機(jī)制理解為:
- 定義端(聲明):像是一個(gè)“槽位”,告訴編譯器:“我這里可能會(huì)有一段邏輯”。
- 實(shí)現(xiàn)端(邏輯):像是往“槽位”里填東西。
如果你在兩個(gè)地方都寫了 { ... }(哪怕一個(gè)是空的 {}),編譯器就會(huì)像見到兩個(gè)同名同姓的人一樣陷入混亂,直接報(bào)錯(cuò)。
正確的執(zhí)行邏輯
如果你希望在某些情況下輸出 “BBB”,在另一些情況下“什么都不做”,你不能靠定義兩個(gè)方法實(shí)現(xiàn),而是要靠邏輯判斷。
正確的寫法應(yīng)該是這樣的:
// 【生成代碼部分】
public partial class MyClass
{
// 只有聲明,沒有大括號(hào)!這代表“此處預(yù)留鉤子”
partial void OnOpen();
public void DoWork()
{
OnOpen(); // 編譯器在這里調(diào)用鉤子
}
}
// 【開發(fā)者代碼部分】
public partial class MyClass
{
// 如果你想執(zhí)行邏輯,就實(shí)現(xiàn)它
partial void OnOpen()
{
if (DateTime.Now.Hour > 12)
{
Console.WriteLine("BBB");
}
// 否則什么都不做(這就是你想要的“空方法”效果)
}
}為什么分部方法不能像普通方法那樣“重載”?
分部方法的設(shè)計(jì)初衷不是為了讓你做選擇題,而是為了**“無縫注入”**。
- 如果你沒寫實(shí)現(xiàn):編譯器在生成的
.dll里會(huì)把OnOpen()徹底抹除。調(diào)用它的地方(如DoWork內(nèi)部)也會(huì)被刪掉。就像這段代碼從未存在過。 - 如果你寫了實(shí)現(xiàn):編譯器會(huì)把實(shí)現(xiàn)里的代碼“縫”到原本聲明調(diào)用的地方。
它不存在“調(diào)用哪一個(gè)”的問題,因?yàn)樽罱K編譯出來的類里,只有一個(gè) OnOpen。
易混淆
那這樣執(zhí)行的時(shí)候,不會(huì)沖突么,或者有兩個(gè)分部類,他會(huì)執(zhí)行哪一個(gè)
不會(huì)沖突,因?yàn)樵诰幾g器眼里,它們壓根就不是兩個(gè)類,而是一個(gè)類。
你可以把“分部類”想象成一塊拼圖:源代碼階段你手里拿著幾塊碎片,但編譯完之后,這些碎片已經(jīng)嚴(yán)絲合縫地粘在一起,變成了一塊完整的板子。
為什么不會(huì)沖突?
- 物理上的“合體”:當(dāng)你點(diǎn)擊“生成”或“運(yùn)行”時(shí),C# 編譯器(Roslyn)會(huì)先掃描所有帶
partial關(guān)鍵字的類。如果發(fā)現(xiàn)命名空間相同、類名相同的partial類,它會(huì)把它們所有的成員(字段、屬性、方法、構(gòu)造函數(shù))全部收集起來,塞進(jìn)同一個(gè) IL(中間語言)定義里。 - 符號(hào)的唯一性:由于它們最終是一個(gè)類,所以普通的 C# 規(guī)則依然適用。比如,你在 A 文件里定義了
public int Id,就不能在 B 文件里再定義一個(gè)public int Id,否則編譯器會(huì)報(bào)錯(cuò):“類已經(jīng)包含該成員的定義”。
執(zhí)行順序是怎么算的?
既然物理上合二為一了,就不存在“執(zhí)行哪一個(gè)”的問題。程序的入口(比如 Main 方法)調(diào)用該類的方法時(shí),該在哪就在哪。

唯一需要注意的順序:構(gòu)造函數(shù)
如果兩個(gè)分部類里都寫了構(gòu)造函數(shù),情況如下:
- 普通構(gòu)造函數(shù):如果你在兩個(gè)文件里都定義了
public User()(無參構(gòu)造),編譯會(huì)報(bào)錯(cuò)。因?yàn)橥粋€(gè)類里不能有兩個(gè)簽名完全一樣的構(gòu)造函數(shù)。 - 執(zhí)行邏輯:如果你想在不同文件中初始化數(shù)據(jù),通常會(huì)在 A 文件寫構(gòu)造函數(shù),在構(gòu)造函數(shù)里調(diào)用一個(gè)在 B 文件實(shí)現(xiàn)的分部方法(
partial method)。
避坑指南
雖然編譯器幫你合體了,但在開發(fā)中要遵循以下守則:
- 同一程序集:所有的分部類必須在同一個(gè)程序集(Project)內(nèi),不能跨 DLL。
- 修飾符一致:如果 A 文件寫了
public partial,B 文件寫了internal partial,編譯器會(huì)直接罷工。訪問級(jí)別必須完全統(tǒng)一。 - 基類一致:如果 A 繼承自
BaseClassA,B 繼承自BaseClassB,編譯報(bào)錯(cuò)。一個(gè)類只能有一個(gè)親爹。
同一個(gè)成員(方法)在同一個(gè)類中不能有多個(gè)實(shí)現(xiàn)
// 生成代碼分部
public partial class MyClass
{
public string Name{get;set;}
partial void OnOpen()
{
Console.WriteLine("AAA");
};
}
// 開發(fā)者代碼分部
public partial class MyClass
{
public string Age{get;set;}
partial void OnOpen()
{
Console.WriteLine("BBB");
};
}雖然 partial 類可以將代碼拆分,但它依然遵循 C# 最基本的語法規(guī)則:同一個(gè)成員(方法)在同一個(gè)類中不能有多個(gè)實(shí)現(xiàn)。
為什么會(huì)報(bào)錯(cuò)?
分部方法(Partial Method)的設(shè)計(jì)初衷是:一個(gè)地方定義聲明(類似占位符),另一個(gè)地方編寫實(shí)現(xiàn)(可選)。
在你的例子中,兩個(gè)分部類都寫了具體的 { ... } 邏輯塊,編譯器會(huì)認(rèn)為你對(duì)同一個(gè)方法定義了兩次,直接拋出編譯錯(cuò)誤:
CS0757: 分部方法聲明具有多個(gè)實(shí)現(xiàn)方法。
正確的用法應(yīng)該是這樣
// ------------------------------------
// 文件 A:生成代碼分部(通常由工具生成)
// ------------------------------------
public partial class MyClass
{
public string Name { get; set; }
// 這里只定義“鉤子”的簽名,不寫大括號(hào)邏輯
partial void OnOpen();
public void Trigger()
{
OnOpen(); // 編譯器在這里埋下調(diào)用點(diǎn)
}
}
// ------------------------------------
// 文件 B:開發(fā)者代碼分部(由你編寫)
// ------------------------------------
public partial class MyClass
{
public string Age { get; set; }
// 這里編寫具體的業(yè)務(wù)邏輯
partial void OnOpen()
{
Console.WriteLine("BBB");
}
}執(zhí)行結(jié)果: 調(diào)用 Trigger() 方法時(shí),屏幕會(huì)輸出:BBB。
如果開發(fā)者沒寫代碼會(huì)怎樣?
這是分部方法最聰明的地方。如果你在文件 B 中沒有編寫 partial void OnOpen() 的實(shí)現(xiàn):
- 編譯器會(huì)發(fā)現(xiàn)這個(gè)方法沒被實(shí)現(xiàn)。
- 為了絕對(duì)的性能,編譯器會(huì)直接刪掉文件 A 中的方法聲明,并刪掉所有對(duì)
OnOpen()的調(diào)用。 - 程序運(yùn)行時(shí),就像這個(gè)方法從未存在過一樣,沒有任何性能開銷。
專業(yè)詞匯詳解
- 編譯時(shí)合并 (Compile-time Merging): 意味著
partial只是編譯器層面的語法糖。運(yùn)行時(shí)的 CLR(公共語言運(yùn)行時(shí))根本不知道分部類的存在,它看到的永遠(yuǎn)是一個(gè)完整的類。 - Source Generators (源代碼生成器): .NET 5+ 引入的新技術(shù),在代碼編譯期間實(shí)時(shí)生成額外的 C# 代碼。它極度依賴分部類,因?yàn)樗荒苄薷哪悻F(xiàn)有的代碼,只能通過
partial注入新功能。 - 零性能損耗 (Zero Performance Overhead): 如果你定義了一個(gè)分部方法但沒有去實(shí)現(xiàn)它,編譯器在生成 IL 代碼時(shí)會(huì)徹底移除該方法的聲明和所有調(diào)用點(diǎn)。
到此這篇關(guān)于C#中分部類和分部方法(partial)的文章就介紹到這了,更多相關(guān)C#分部類和分部方法內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C#中winform窗體實(shí)現(xiàn)注冊(cè)/登錄功能實(shí)例(DBHelper類)
在編寫項(xiàng)目時(shí),編寫了一部分關(guān)于登錄頁面的一些代碼,下面這篇文章主要給大家介紹了關(guān)于C#中winform窗體實(shí)現(xiàn)注冊(cè)/登錄功能(DBHelper類)的相關(guān)資料,文中通過圖文介紹的非常詳細(xì),需要的朋友可以參考下2023-06-06
C#寫入對(duì)象或集合類型數(shù)據(jù)到xml文件的方法
這篇文章主要介紹了C#寫入對(duì)象或集合類型數(shù)據(jù)到xml文件的方法,涉及C#針對(duì)XML文件的相關(guān)操作技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-07-07
C#使用SevenZipSharp實(shí)現(xiàn)壓縮文件和目錄
SevenZipSharp壓縮/解壓(.7z?.zip)”是指使用SevenZipSharp庫進(jìn)行7z和zip格式的文件壓縮與解壓縮操作,SevenZipSharp是C#語言封裝的7-Zip?API,它使得在.NET環(huán)境中調(diào)用7-Zip的功能變得簡單易行,本文給大家介紹了C#使用SevenZipSharp實(shí)現(xiàn)壓縮文件和目錄2025-01-01
C#實(shí)現(xiàn)從windows剪貼板獲取內(nèi)容的方法
這篇文章主要介紹了C#實(shí)現(xiàn)從windows剪貼板獲取內(nèi)容的方法,涉及C#操作剪貼板的相關(guān)技巧,非常簡單實(shí)用,需要的朋友可以參考下2015-05-05
桌面浮動(dòng)窗口(類似惡意廣告)的實(shí)現(xiàn)詳解
本篇文章是對(duì)桌面浮動(dòng)窗口的實(shí)現(xiàn)方法進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-06-06
Winform窗口實(shí)現(xiàn)多顯示屏顯示的2種方法
這篇文章主要介紹了Winform窗口實(shí)現(xiàn)多顯示屏顯示的2種方法,本文直接給出了實(shí)現(xiàn)代碼,并對(duì)其中的一些重要參數(shù)做了解釋,需要的朋友可以參考下2015-06-06
python實(shí)現(xiàn)AutoResetEvent類的阻塞模式方法解析
AutoResetEvent :當(dāng)某個(gè)線程執(zhí)行到WaitOne()方法時(shí),該線程則會(huì)處于阻塞模式,當(dāng)被調(diào)用了Set()方法,阻塞的線程則會(huì)繼續(xù)向下執(zhí)行,其狀態(tài)立即被自動(dòng)設(shè)置為阻塞模式2012-11-11

