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

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

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

