以下是一個針對 Java 開發者快速轉向 C# 的簡明教程,重點對比 Java 與 C# 的異同,幫助你快速上手。
項目結構:
- .sln :解決方案文件,管理多個項目之間的依賴關系。
- .csproj :項目文件,定義目標框架(如 net6.0)、依賴項(NuGet 包或本地 DLL)。
- global.json: 控制 .NET SDK 行為
- 指定 .NET SDK 版本 :確保項目使用特定版本的 SDK 構建(避免本地環境版本不一致)。
- 控制項目掃描范圍 :在多項目解決方案中,指定哪些目錄參與構建。
- 啟用/禁用 SDK 安裝提示 :控制是否自動下載未安裝的 SDK 版本。
- app.manifest: 應用程序清單文件
- 聲明應用程序權限 (如以管理員身份運行)。
- 指定兼容性需求 (如支持的 Windows 版本)。
- 啟用 Visual Studio 高 DPI 支持 。
- 配置應用程序隔離(Side-by-Side Assembly) 。
- Program.cs :程序入口(包含 Main 方法)。
一、基礎語法對比
1. 變量與數據類型
Java | C# |
---|---|
int a = 10; | int a = 10; |
String name = "Hello"; | string name = "Hello"; |
final int MAX = 100; | const int MAX = 100; |
var list = new ArrayList<>(); (Java 10+) | var list = new List<string>(); |
C# 特色:
var
是隱式類型變量(編譯器推斷類型)。dynamic
類型可動態賦值(類似 Object)。
2. 拓展方法
C# 的擴展方法允許你為現有類型 (包括密封類、接口、甚至第三方庫的類型)“添加”方法,而無需修改其源代碼或繼承。這是 C# 特有的語法特性,Java 中無直接等價物(需通過工具類或繼承實現)。
定義擴展方法
- 必須在靜態類 中定義。
- 第一個參數使用 this 關鍵字,表示該方法擴展的目標類型
// 靜態類:擴展方法容器
public static class StringExtensions {// 擴展 string 類型的方法public static bool IsNullOrEmpty(this string str) {return string.IsNullOrEmpty(str);}
}
使用拓展方法
string name = null;// 調用擴展方法(如同實例方法)
if (name.IsNullOrEmpty()) {Console.WriteLine("Name is null or empty");
}
值得注意的是,拓展方法作為一個語法糖對應的可以解決在Java中 xxUtils 的工具類。同時具有以下注意:
- 無法訪問內部方法/屬性
- 若類型本身有同名方法,實例方法優先于擴展方法
- 避免過度使用,防止命名沖突(需顯式導入命名空間)
3. 空運算符(Null Handling Operators)
C# 提供了強大的空值處理運算符,簡化空值檢查邏輯,避免 NullReferenceException
。
1. 空條件運算符(?.
)
用于安全訪問對象的成員,若對象為 null
則返回 null
而非拋出異常。
Person person = GetPerson(); // 可能為 null// 安全訪問屬性和方法
int length = person?.Name?.Length ?? 0;
person?.SayHello();
對比 Java
-
Java 中需顯式判斷或使用
Optional
:int length = Optional.ofNullable(person).map(p -> p.getName()).map(String::length).orElse(0);
2. 空合并運算符(??
)
提供默認值,當左側表達式為 null
時返回右側值。
string name = null;
string displayName = name ?? "Guest"; // 如果 name 為 null,則使用 "Guest"
對比 Java
-
Java 中使用三元運算符或
Optional
:String displayName = name != null ? name : "Guest"; // 或 String displayName = Optional.ofNullable(name).orElse("Guest");
3. 空合并賦值運算符(??=
)
僅當變量為 null
時才賦值(C# 8.0+)。
string message = GetMessage();
message ??= "Default Message"; // 如果 GetMessage() 返回 null,則賦值為默認值
對比 Java
- Java 中需顯式判斷:
if (message == null) {message = "Default Message"; }
4. 非空斷言運算符(!
)
告知編譯器某個表達式不為 null
(C# 8.0+,用于可空引用類型上下文)。
string name = GetName()!; // 告訴編譯器 GetName() 返回值不為 null
注意事項
- 空條件運算符返回的類型可能是
null
(需結合??
使用)。 - 空合并運算符適用于
null
檢查,但不適用于值類型(如int
)。 - 使用
?.
和??
組合可顯著減少防御性代碼(如嵌套if
判斷)。
二、面向對象編程
1. 類與對象
public class Person {// 字段private string name;// 屬性(推薦封裝字段)public string Name {get { return name; }set { name = value; }}// 構造函數public Person(string name) {this.name = name;}// 方法public void SayHello() {Console.WriteLine($"Hello, {name}");}
}
對比 Java:
- C# 使用
property
(屬性)替代 Java 的getter/setter
。 this
關鍵字用法相同。
2. 繼承與接口
// 繼承
public class Student : Person {public Student(string name) : base(name) {}
}// 接口
public interface IRunnable {void Run();
}public class Car : IRunnable {public void Run() {Console.WriteLine("Car is running");}
}
對比 Java:
- C# 使用
:
替代 Java 的extends/implements
。 - 接口方法默認
public
,無需顯式聲明。
三、C# 特有特性
1. 委托與事件(Delegates & Events)
// 委托(類似 Java 的函數式接口)
// 定義一個名為 Notify 的委托類型,它表示一種方法模板,要求方法返回 void 并接受一個 string 參數
// 類比 Java :類似 Java 中的函數式接口(如 Consumer<String>),但 C# 的委托更直接,可以直接綁定方法。
public delegate void Notify(string message);// 事件
public class EventPublisher {// 聲明一個事件 OnNotify,其類型是 Notify 委托。事件本質上是委托的安全封裝,外部只能通過 +=/-= 訂閱/取消訂閱,不能直接調用(如 OnNotify.Invoke() 會報錯)。public event Notify OnNotify;// 調用 OnNotify?.Invoke(...) 觸發事件,?. 是空值保護操作符(避免空引用異常)。只有當至少有一個訂閱者時,才會執行。public void TriggerEvent() {OnNotify?.Invoke("Event triggered!");}
}// 使用
EventPublisher publisher = new EventPublisher();
publisher.OnNotify += (msg) => Console.WriteLine(msg);
publisher.TriggerEvent();
-
訂閱事件
使用+=
運算符將一個 lambda 表達式(msg) => Console.WriteLine(msg)
綁定到 OnNotify 事件。當事件觸發時,會執行此方法。 -
觸發事件
調用 TriggerEvent() 后,所有訂閱者都會收到 “Event triggered!” 消息。
2. LINQ(Language Integrated Query)
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var even = numbers.Where(n => n % 2 == 0).ToList();
對比 Java:
- 類似 Java Stream,但語法更簡潔。
3. 異步編程(Async/Await)
public async Task DownloadDataAsync() {var client = new HttpClient();var data = await client.GetStringAsync("https://example.com");Console.WriteLine(data);
}
對比 Java:
- 類似
CompletableFuture
,但語法更直觀。
Parallel.Invoke(() => {// 并行執行CPU密集任務
});
- 多個 CPU 密集型任務并行執行。
- 任務之間沒有依賴。
- 不需要返回結果。
四、常用工具與框架
Java | C# |
---|---|
Maven/Gradle | NuGet(包管理) |
Spring | .NET Core(框架) |
JUnit | xUnit/NUnit(測試框架) |
IntelliJ IDEA | Visual Studio / IntelliJ Rider(IDE) |
五、項目結構與命名空間
// 文件:Program.cs
using System;
namespace MyApplication;class Program {static void Main(string[] args) {Console.WriteLine("Hello World!");}
}
對比 Java:
- C# 使用
namespace
組織代碼,Java 使用package
。 - 程序入口是
Main
方法(Java 是main
)。
六、Java 到 C# 的常見轉換技巧
Java | C# |
---|---|
System.out.println() | Console.WriteLine() |
ArrayList<T> | List<T> |
HashMap<K,V> | Dictionary<K,V> |
interface | interface |
enum | enum |
try-catch-finally | try-catch-finally |
在 C# 中,反射(Reflection) 是一種強大的機制,允許在運行時動態地獲取類型信息、創建對象實例、調用方法、訪問字段和屬性等。對于從 Java 轉向 C# 的開發者來說,反射的概念是相似的,但 C# 的反射 API 更加簡潔、直觀,并且與語言特性(如 dynamic
、nameof
、LINQ
)結合更緊密。
七、反射
反射是指在 運行時(runtime) 動態地:
- 獲取類型信息(如類名、方法、屬性等)
- 創建對象實例
- 調用方法、訪問字段或屬性
- 檢查程序集(Assembly)的結構
Java 與 C# 反射的對比
功能 | Java | C# |
---|---|---|
獲取類型對象 | MyClass.class 或 obj.getClass() | typeof(MyClass) 或 obj.GetType() |
獲取方法 | clazz.getMethod("name", params...) | type.GetMethod("Name") |
調用方法 | method.invoke(obj, args) | method.Invoke(obj, args) |
獲取屬性 | clazz.getDeclaredField("name") | type.GetProperty("Name") |
獲取程序集 | 無直接等價物 | Assembly.GetExecutingAssembly() |
動態創建實例 | clazz.newInstance() | Activator.CreateInstance(type) |
動態訪問成員 | 通過 Field/Method 對象 | 通過 PropertyInfo/MethodInfo 等 |
1. 獲取 Type
對象
// 通過類型名獲取
Type type = typeof(string);// 通過對象獲取
object obj = new Person();
Type type = obj.GetType();
2. 獲取類成員信息(屬性、方法、字段)
Type type = typeof(Person);// 獲取所有屬性
PropertyInfo[] properties = type.GetProperties();// 獲取特定方法
MethodInfo method = type.GetMethod("SayHello");// 獲取所有字段
FieldInfo[] fields = type.GetFields();
3. 動態創建實例
object person = Activator.CreateInstance(typeof(Person));
4. 調用方法
MethodInfo method = type.GetMethod("SayHello");
method.Invoke(person, null);
5. 訪問屬性
PropertyInfo prop = type.GetProperty("Name");
prop.SetValue(person, "Alice");
string name = (string)prop.GetValue(person);
6. 訪問字段(不推薦,除非必要)
FieldInfo field = type.GetField("age", BindingFlags.NonPublic | BindingFlags.Instance);
field.SetValue(person, 30);
int age = (int)field.GetValue(person);
7. 獲取程序集信息
Assembly assembly = Assembly.GetExecutingAssembly();
foreach (Type type in assembly.GetTypes()) {Console.WriteLine(type.Name);
}
8. 動態加載 DLL 并調用方法
Assembly assembly = Assembly.LoadFile("path/to/MyLibrary.dll");
Type type = assembly.GetType("MyNamespace.MyClass");
object instance = Activator.CreateInstance(type);
MethodInfo method = type.GetMethod("DoSomething");
method.Invoke(instance, null);
9. 使用 dynamic
替代部分反射操作
dynamic person = new ExpandoObject();
person.Name = "Bob";
person.SayHello = new Action(() => Console.WriteLine("Hello"));
person.SayHello(); // 無需反射即可調用
10. 使用 Expression
構建高性能的反射調用
Func<object> factory = Expression.Lambda<Func<object>>(Expression.New(typeof(Person))
).Compile();
object person = factory();
11. 使用 IL Emit
或 Source Generator
優化性能
對于高性能場景(如 ORM、序列化框架),可以使用:
System.Reflection.Emit
:動態生成 IL 代碼Source Generator
(C# 9+):編譯時生成代碼,避免運行時反射
性能問題
- 反射調用比直接調用慢(約慢 10~100 倍)
- 頻繁使用
GetMethod
、GetProperty
會增加開銷
解決方案
- 緩存反射結果(如
MethodInfo
、PropertyInfo
) - 使用
Expression
構建委托 - 使用
dynamic
(在合適場景下) - 使用
System.Reflection.DispatchProxy
實現代理 - 使用
System.Text.Json
、Newtonsoft.Json
等庫已優化的反射機制
安全性
- 可以訪問私有成員(需設置
BindingFlags.NonPublic
) - 在部分受限環境中(如 UWP、AOT 編譯)可能受限
Java 到 C# 反射的轉換技巧
Java | C# |
---|---|
Class.forName("MyClass") | Type.GetType("MyNamespace.MyClass") |
clazz.newInstance() | Activator.CreateInstance(type) |
method.invoke(obj, args) | method.Invoke(obj, args) |
clazz.getDeclaredMethods() | type.GetMethods() |
clazz.getDeclaredFields() | type.GetFields() |
clazz.getDeclaredField("name") | type.GetField("Name") |
clazz.getDeclaredMethod("name", params...) | type.GetMethod("Name", parameterTypes) |
clazz.getInterfaces() | type.GetInterfaces() |
八、引入包(NuGet 包管理)
在 C# 中,NuGet 是官方推薦的包管理系統,類似于 Java 中的 Maven/Gradle。它用于管理項目依賴項(如第三方庫、框架等)。
NuGet 包典型命名規則:[組織名].[功能模塊].[平臺/框架]
1. NuGet 的作用
- 管理項目依賴(如
Newtonsoft.Json
、EntityFramework
等) - 自動下載、安裝、更新依賴包
- 支持跨平臺(Windows、Linux、macOS)
2. 常用方式
方法 1:通過 Visual Studio 引入
- 右鍵項目 → Manage NuGet Packages
- 在 Browse 標簽頁搜索包名(如
Newtonsoft.Json
) - 點擊 Install 安裝包
- 安裝完成后,會自動添加到項目中
方法 2:通過 CLI 命令行
# 安裝包
dotnet add package Newtonsoft.Json# 更新包
dotnet add package Newtonsoft.Json --version 13.0.1# 卸載包
dotnet remove package Newtonsoft.Json
方法 3:手動編輯 .csproj
文件
在項目文件中添加 <PackageReference>
:
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>net6.0</TargetFramework></PropertyGroup><ItemGroup><PackageReference Include="Newtonsoft.Json" Version="13.0.1" /></ItemGroup>
</Project>
3. NuGet 源配置
默認源是 nuget.org,但也可以配置私有源(如公司內部源):
# 添加私有源
dotnet nuget add source https://mycompany.com/nuget -n MyCompany
4. 對比 Java
功能 | Java (Maven/Gradle) | C# (NuGet) |
---|---|---|
包管理 | pom.xml / build.gradle | .csproj |
安裝包 | mvn install / gradle build | dotnet add package |
私有倉庫 | settings.xml / repositories { maven { url "..." } } | dotnet nuget add source |
九、引用本地的 DLL
有時你需要引用本地的 DLL 文件(如團隊內部開發的庫、第三方未提供 NuGet 包的庫),可以通過以下方式實現。
1. 添加本地 DLL 引用
方法 1:通過 Visual Studio 添加
- 右鍵項目 → Add → Reference…
- 在彈出窗口中選擇 Browse
- 瀏覽并選擇本地 DLL 文件(如
MyLibrary.dll
) - 點擊 Add → OK
方法 2:手動編輯 .csproj
文件
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>net6.0</TargetFramework></PropertyGroup><ItemGroup><Reference Include="MyLibrary"><HintPath>..\Libraries\MyLibrary.dll</HintPath></Reference></ItemGroup>
</Project>
2. 確保 DLL 被正確復制到輸出目錄
在 .csproj
中添加以下配置,確保 DLL 被復制到 bin
目錄:
<ContentWithTargetPath Include="..\Libraries\MyLibrary.dll"><TargetPath>MyLibrary.dll</TargetPath><CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</ContentWithTargetPath>
3. 加載本地 DLL 的運行時行為
- Windows:直接復制到
bin\Debug\net6.0
目錄即可 - Linux/macOS:確保 DLL 與主程序在同一目錄,或設置
LD_LIBRARY_PATH
/DYLD_LIBRARY_PATH
4. 注意事項
- 強名稱簽名(Strong Name):如果 DLL 是強名稱簽名的,引用時需確保簽名一致
- 平臺相關性:某些 DLL 僅支持特定平臺(如 Windows 專用的 DLL)
- 版本沖突:多個 DLL 依賴相同庫的不同版本時,可能出現沖突(需手動綁定重定向)
常見問題與解決方案
1. 無法找到 DLL
- 原因:DLL 未正確復制到輸出目錄
- 解決:檢查
.csproj
中是否設置了<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
2. 加載 DLL 失敗
- 原因:DLL 依賴的其他庫缺失
- 解決:使用
Fusion Log Viewer
(fuslogvw.exe
)查看綁定失敗日志
3. 版本沖突
- 原因:多個 DLL 依賴相同庫的不同版本
十、DllImport(平臺調用,P/Invoke)
在 C# 中,[DllImport("xxx.dll")]
是 平臺調用(Platform Invocation Services,P/Invoke) 的核心特性,用于直接調用 非托管代碼(如 Windows API、C/C++ 編寫的 DLL)。這是 Java 中沒有的特性(Java 需要通過 JNI 調用本地代碼)。
1. 基本概念
[DllImport]
是 System.Runtime.InteropServices
命名空間下的特性(Attribute),用于聲明某個方法的實現來自外部 DLL。它允許你在 C# 中直接調用 Windows API 或其他非托管函數。
2. 使用步驟
步驟 1:引入命名空間
using System.Runtime.InteropServices;
步驟 2:聲明外部方法
使用 [DllImport("dll名稱")]
特性修飾方法,指定 DLL 名稱和調用約定(Calling Convention)。
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern int MessageBox(IntPtr hWnd, String text, String caption, uint type);
步驟 3:調用方法
MessageBox(IntPtr.Zero, "Hello from C#", "Greeting", 0);
3. 參數說明
參數 | 說明 |
---|---|
dllName | DLL 文件名(如 "user32.dll" ) |
CharSet | 字符集(CharSet.Ansi 、CharSet.Unicode 、CharSet.Auto ) |
CallingConvention | 調用約定(默認為 CallingConvention.Winapi ,也可指定 ThisCall 、StdCall 等) |
EntryPoint | 可選,指定 DLL 中函數的入口點(當方法名與 DLL 函數名不同時使用) |
4. 常見示例
示例 1:調用 user32.dll
的 MessageBox
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type);// 調用
MessageBox(IntPtr.Zero, "Hello from C#", "Greeting", 0);
示例 2:調用 kernel32.dll
的 GetTickCount
[DllImport("kernel32.dll")]
public static extern uint GetTickCount();// 調用
uint tickCount = GetTickCount();
Console.WriteLine($"System uptime: {tickCount} ms");
示例 3:調用 gdi32.dll
的 CreateDC
[DllImport("gdi32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr CreateDC(string lpszDriver, string lpszDevice, string lpszOutput, IntPtr lpInitData);// 調用
IntPtr hdc = CreateDC("DISPLAY", null, null, IntPtr.Zero);
5. 結構體與指針傳參
當調用的函數需要結構體或指針參數時,需使用 ref
、out
或 IntPtr
,并可能需要使用 StructLayout
和 MarshalAs
來控制內存布局。
示例:調用 user32.dll
的 GetWindowRect
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{public int Left;public int Top;public int Right;public int Bottom;
}[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);// 使用
RECT rect;
bool success = GetWindowRect(hWnd, out rect);
if (success)
{Console.WriteLine($"Window Rect: {rect.Left}, {rect.Top}, {rect.Right}, {rect.Bottom}");
}
6. 注意事項
安全性
- 調用非托管代碼可能帶來 安全風險(如緩沖區溢出、非法訪問內存)。
- 需要 Full Trust 權限 才能執行 P/Invoke 操作。
平臺依賴性
DllImport
僅適用于 Windows 平臺(除非使用跨平臺兼容的庫)。- 某些 DLL(如
user32.dll
、kernel32.dll
)是 Windows 系統庫,其他平臺無法直接使用。
性能
- P/Invoke 調用比純托管代碼慢(涉及 上下文切換 和 參數封送)。
- 頻繁調用時應考慮緩存結果或使用
unsafe
代碼優化。
參數封送(Marshaling)
- 需要特別注意 數據類型映射(如
int
對應Int32
,char*
對應string
)。 - 使用
MarshalAs
明確指定封送方式(如UnmanagedType.LPStr
、UnmanagedType.BStr
)。
7. 示例:封裝一個 Windows API 工具類
using System;
using System.Runtime.InteropServices;public static class Win32Api
{[DllImport("user32.dll", CharSet = CharSet.Auto)]public static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type);[DllImport("kernel32.dll")]public static extern uint GetTickCount();[StructLayout(LayoutKind.Sequential)]public struct RECT{public int Left;public int Top;public int Right;public int Bottom;}[DllImport("user32.dll")][return: MarshalAs(UnmanagedType.Bool)]public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
}// 使用
class Program
{static void Main(){// 調用 MessageBoxWin32Api.MessageBox(IntPtr.Zero, "Hello from C#", "Greeting", 0);// 獲取系統運行時間uint tickCount = Win32Api.GetTickCount();Console.WriteLine($"System uptime: {tickCount} ms");// 獲取窗口位置Win32Api.RECT rect;bool success = Win32Api.GetWindowRect(new IntPtr(0x123456), out rect);if (success){Console.WriteLine($"Window Rect: {rect.Left}, {rect.Top}, {rect.Right}, {rect.Bottom}");}}
}
通過掌握 DllImport
和 P/Invoke,你可以在 C# 中直接調用 Windows API 或其他非托管函數,實現更底層的系統級操作。結合良好的封裝和錯誤處理,可以顯著提升程序的功能性和靈活性。