以下是一个针对 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 或其他非托管函数,实现更底层的系统级操作。结合良好的封装和错误处理,可以显著提升程序的功能性和灵活性。