在实际 C# 开发中,从掌握基础语法到能够独立完成复杂项目,中间存在一个巨大的鸿沟。很多开发者学习了变量、循环和类之后,面对异步编程、泛型约束、反射、依赖注入等高级主题时,常常感到无从下手,或者只能写出能跑但难以维护的代码。本篇文章将作为“从入门到精通”系列的第二部分,聚焦于那些将初级开发者与中高级开发者区分开的核心概念与实战技能。我们将不再重复
if-else
和
for
循环,而是深入探讨如何构建健壮、高效且易于维护的 C# 应用程序。
本文适合已经熟悉 C# 基本语法(如变量、方法、类、接口)、了解面向对象基础,并希望向中高级水平迈进的开发者。我们将通过具体的代码示例、设计模式的应用以及生产环境中常见的陷阱分析,帮助你理解如何有效地使用委托与事件、深入 LINQ、掌握异步编程模型、运用反射与特性,并最终构建一个结构清晰的小型项目。学完后,你将能够更有信心地处理复杂的业务逻辑,编写出更具扩展性和可测试性的代码。
1. 深入理解委托、事件与 Lambda 表达式
委托和事件是 C# 实现回调、事件驱动编程以及 LINQ 的基石。许多中级开发者对它们的理解停留在“声明-实例化-调用”的层面,但对其在解耦、扩展订阅者以及与现代语法糖结合方面的威力认识不足。
1.1 委托:不仅仅是类型安全的函数指针
委托本质上是一种类型,它定义了方法的签名(参数类型和返回类型)。你可以将符合该签名的方法“赋值”给委托变量,并通过该变量来调用方法。这为实现策略模式、回调机制提供了基础。
// 1. 声明委托类型
public delegate int CalculationDelegate(int x, int y);
// 2. 符合签名的方法
public int Add(int a, int b) => a + b;
public int Multiply(int a, int b) => a * b;
// 3. 使用委托
CalculationDelegate calc = Add;
Console.WriteLine(calc(5, 3)); // 输出: 8
calc = Multiply;
Console.WriteLine(calc(5, 3)); // 输出: 15
然而,在实际项目中,我们很少直接使用自定义委托类型,因为 .NET 提供了强大的内置泛型委托:
Action
(无返回值)和
Func
(有返回值)。
// 使用 Func<int, int, int> 替代自定义 CalculationDelegate
Func<int, int, int> calcFunc = Add;
// Action<string> 表示接收一个string参数,无返回值的方法
Action<string> logger = message => Console.WriteLine($"[LOG] {DateTime.Now}: {message}");
logger("Application started.");
关键点
:委托实现了方法的“间接调用”,将“谁来做”与“什么时候做”解耦。例如,一个排序算法可以接收一个
Comparison<T>
委托来决定排序规则,从而实现算法逻辑与具体比较规则的分离。
1.2 事件:基于委托的发布-订阅模型
事件是委托的一个特殊应用,用于实现发布-订阅模式。事件的声明者(发布者)只暴露一个“事件”,而事件的响应者(订阅者)可以“订阅”或“取消订阅”该事件。.NET 的事件模式强制使用
event
关键字和
EventHandler
或
EventHandler<TEventArgs>
委托,这带来了两个重要约束:1) 在类外部,只能对事件进行
+=
和
-=
操作,不能直接赋值 (
=
) 或调用;2) 它鼓励使用标准的
(object sender, EventArgs e)
参数模式。
public class OrderProcessor
{
// 声明一个事件,使用泛型 EventHandler<T>
public event EventHandler<OrderProcessedEventArgs> OrderProcessed;
// 触发事件的方法,通常命名为 On[EventName],并标记为 protected virtual 以便派生类重写触发逻辑
protected virtual void OnOrderProcessed(Order order)
{
// 空值条件运算符 ?. 是线程安全的调用方式
OrderProcessed?.Invoke(this, new OrderProcessedEventArgs { ProcessedOrder = order });
}
public void Process(Order order)
{
// 处理订单的业务逻辑...
Console.WriteLine($"Processing order {order.Id}");
// 处理完成后触发事件
OnOrderProcessed(order);
}
}
// 自定义事件参数,继承自 EventArgs
public class OrderProcessedEventArgs : EventArgs
{
public Order ProcessedOrder { get; set; }
}
// 订阅者
public class NotificationService
{
public void Subscribe(OrderProcessor processor)
{
processor.OrderProcessed += HandleOrderProcessed;
}
private void HandleOrderProcessed(object sender, OrderProcessedEventArgs e)
{
Console.WriteLine($"Notification: Order {e.ProcessedOrder.Id} has been processed.");
// 这里可以发送邮件、短信等
}
}
常见坑点一:忘记检查空值直接调用事件
在触发事件前,必须检查是否有订阅者。使用
?.Invoke()
是推荐且线程安全的方式。直接调用
OrderProcessed(this, args)
在事件为
null
时会抛出
NullReferenceException
。
常见坑点二:内存泄漏 如果订阅者是一个生命周期较长的对象(如单例服务)订阅了一个生命周期较短对象(如临时视图模型)的事件,或者反之,且没有及时取消订阅,就会导致内存泄漏,因为事件持有对订阅者方法的引用,阻止了垃圾回收。
// 错误示例:临时视图模型订阅了长期存在的服务事件,且未取消订阅
public class MyViewModel
{
public MyViewModel(MessageService service)
{
service.NewMessageArrived += OnNewMessage; // 订阅
}
private void OnNewMessage(object sender, MessageEventArgs e) { /*...*/ }
// 缺少取消订阅的逻辑,ViewModel 实例可能无法被回收
}
// 正确做法:实现 IDisposable 或在适当生命周期取消订阅
public class MyViewModel : IDisposable
{
private MessageService _service;
public MyViewModel(MessageService service)
{
_service = service;
_service.NewMessageArrived += OnNewMessage;
}
private void OnNewMessage(object sender, MessageEventArgs e) { /*...*/ }
public void Dispose()
{
_service.NewMessageArrived -= OnNewMessage; // 取消订阅
}
}
1.3 Lambda 表达式与匿名方法
Lambda 表达式是编写匿名函数的简洁语法,极大地简化了委托的实例化,尤其是在 LINQ 和事件处理中。
// 匿名方法 (C# 2.0)
button.Click += delegate(object sender, EventArgs e) { Console.WriteLine("Clicked!"); };
// Lambda 表达式 (C# 3.0+)
button.Click += (sender, e) => Console.WriteLine("Clicked!");
// 带语句体的 Lambda
Func<int, int> square = x =>
{
int result = x * x;
Console.WriteLine($"Squaring {x} gives {result}");
return result;
};
关键点 :Lambda 表达式可以捕获外部变量(闭包)。理解闭包的行为至关重要,因为它可能导致意想不到的结果,尤其是在循环中。
// 一个经典的闭包陷阱
var actions = new List<Action>();
for (int i = 0; i < 5; i++)
{
actions.Add(() => Console.WriteLine(i));
}
foreach (var action in actions)
{
action(); // 全部输出 5,而不是 0,1,2,3,4
}
// 原因:所有Lambda都捕获了同一个变量 i 的引用,循环结束时 i 的值为5。
// 解决方法:在循环内创建局部变量副本
for (int i = 0; i < 5; i++)
{
int temp = i; // 创建副本
actions.Add(() => Console.WriteLine(temp)); // 捕获副本
}
2. 掌握 LINQ 的查询语法与方法语法
语言集成查询 (LINQ) 是 C# 最强大的特性之一。它允许你以声明式的方式对数据(无论是对象集合、XML、数据库还是其他)进行查询和转换。LINQ 有两种主要语法:查询语法(类似 SQL)和方法语法(链式方法调用)。
2.1 方法语法:更灵活,更函数式
方法语法基于扩展方法,是实际开发中最常用的形式。它依赖于
System.Linq
命名空间下的一系列扩展方法,如
Where
,
Select
,
OrderBy
,
GroupBy
,
FirstOrDefault
,
Any
,
All
等。
using System.Linq;
List<Product> products = GetProducts();
// 筛选价格大于100的产品,并按名称排序
var expensiveProducts = products
.Where(p => p.Price > 100)
.OrderBy(p => p.Name)
.ToList();
// 投影:只选择产品名称和价格,形成新对象
var productSummaries = products
.Select(p => new { p.Name, p.Price }) // 使用匿名类型
.ToList();
// 聚合:计算总价
decimal totalPrice = products.Sum(p => p.Price);
// 判断:是否存在任何缺货产品
bool anyOutOfStock = products.Any(p => p.Stock == 0);
// 分组:按类别分组
var productsByCategory = products
.GroupBy(p => p.Category)
.ToDictionary(g => g.Key, g => g.ToList());
延迟执行 (Deferred Execution)
:这是 LINQ 的核心概念之一。像
Where
,
Select
,
OrderBy
这样的操作并不会立即执行查询,它们只是构建了一个查询表达式。只有当真正需要结果时(例如调用
ToList()
,
ToArray()
,
First()
,或进行
foreach
迭代),查询才会执行。这允许你分步构建复杂的查询,并且只在最后一步才访问数据源。
var query = products.Where(p => p.Price > 50); // 查询未执行
query = query.OrderByDescending(p => p.Price); // 仍未执行
var result = query.ToList(); // 在此刻,查询才真正执行
立即执行 (Immediate Execution)
:像
ToList()
,
ToArray()
,
Count()
,
Max()
,
First()
等方法会立即执行查询并返回结果。
2.2 查询语法:更接近 SQL,可读性更强
查询语法对于来自数据库背景的开发者更友好,尤其在处理连接 (
join
) 和分组 (
group by
) 时。
// 使用查询语法实现与上面方法语法相同的筛选和排序
var expensiveProductsQuery =
from p in products
where p.Price > 100
orderby p.Name
select p;
var result = expensiveProductsQuery.ToList();
// 连接查询示例
var query =
from order in orders
join customer in customers on order.CustomerId equals customer.Id
select new { OrderId = order.Id, CustomerName = customer.Name };
重要原则 :查询语法在编译时会被转换为对应的方法语法。两者在功能上是等价的,可以混合使用(通常以查询语法开始,以方法语法结束)。选择哪种取决于团队习惯和具体场景的清晰度。
2.3 常见性能陷阱与最佳实践
-
避免在循环中重复执行 LINQ 查询 :每次执行
ToList()或迭代查询都会访问数据源。如果数据源是数据库或网络服务,这将导致严重的性能问题。// 错误做法 for (int i = 0; i < categories.Count; i++) { var productsInCategory = products.Where(p => p.CategoryId == categories[i].Id).ToList(); // ... 每次循环都执行一次查询 } // 正确做法:先缓存结果 var productLookup = products.ToLookup(p => p.CategoryId); foreach (var category in categories) { var productsInCategory = productLookup[category.Id].ToList(); } -
谨慎使用
Select后的Where:有时先过滤再投影效率更高。// 可能低效:先投影出所有对象的Name,再过滤 var names = products.Select(p => p.Name).Where(name => name.StartsWith("A")).ToList(); // 通常更高效:先过滤对象,再投影 var names = products.Where(p => p.Name.StartsWith("A")).Select(p => p.Name).ToList(); -
理解
First()vsFirstOrDefault()vsSingle()vsSingleOrDefault():方法 行为 空序列时 多个匹配时 First()返回第一个元素 抛出 InvalidOperationException返回第一个,不报错 FirstOrDefault()返回第一个元素或默认值 返回 default(T)(如 null, 0)返回第一个,不报错 Single()返回唯一元素 抛出 InvalidOperationException抛出 InvalidOperationExceptionSingleOrDefault()返回唯一元素或默认值 返回 default(T)抛出 InvalidOperationException根据你的业务逻辑严格选择。如果你期望有且仅有一个元素,使用
Single系列;如果只关心第一个,使用First系列。错误的选择可能导致隐藏的 bug。
3. 征服异步编程:async/await 深入解析
异步编程是现代 C# 开发避免阻塞 UI 线程或提高服务器吞吐量的关键。
async
和
await
关键字让异步代码看起来像同步代码一样直观,但背后的机制需要清晰理解。
3.1 核心概念:Task 与 async/await
-
Task和Task<T>:表示一个异步操作。Task表示无返回值的操作,Task<T>表示返回T类型值的操作。 -
async:修饰方法,表明该方法内部包含异步操作(await)。async方法必须返回Task,Task<T>,ValueTask,ValueTask<T>或void(仅限事件处理程序)。 -
await:用于等待一个Task完成。它会暂停当前方法的执行,将控制权返回给调用者,但不会阻塞线程。当Task完成后,该方法从暂停处恢复执行。
public async Task<string> DownloadStringAsync(string url)
{
// 模拟一个耗时的网络请求
await Task.Delay(1000); // 异步等待,不阻塞线程
using (var client = new HttpClient())
{
// HttpClient.GetStringAsync 是另一个异步方法
return await client.GetStringAsync(url);
}
}
// 调用异步方法
public async Task ProcessDataAsync()
{
Console.WriteLine("Starting download...");
string data = await DownloadStringAsync("https://api.example.com/data");
Console.WriteLine($"Downloaded {data.Length} characters.");
// 在这里,data 已经可用,代码像同步一样顺序执行
}
3.2 配置上下文:
ConfigureAwait(false)
在 UI 应用程序(如 WPF, WinForms, ASP.NET Core Blazor)中,存在一个“同步上下文”(
SynchronizationContext
),它负责将异步操作完成后的延续代码封送回 UI 线程执行,以便安全地更新 UI 控件。
ConfigureAwait(false)
告诉运行时:我不需要回到原始的同步上下文(通常是 UI 线程)。这在库代码或非 UI 的后台代码中非常重要,可以避免不必要的线程切换和潜在的死锁。
// 在类库或后台服务中
public async Task<int> ComputeValueAsync()
{
var data = await DownloadDataAsync().ConfigureAwait(false); // 不捕获上下文
// 这里不会回到调用者的线程(如UI线程),可能在线程池线程上运行
return PerformHeavyComputation(data);
}
// 在UI事件处理程序中
private async void Button_Click(object sender, EventArgs e)
{
// 这里需要上下文来更新UI
var result = await SomeAsyncOperation(); // 默认捕获上下文
textBox.Text = result; // 这行代码会在UI线程上执行
}
最佳实践
:在编写可重用的库代码时,除非明确需要与调用者上下文交互(如更新 UI),否则应在每一个
await
后使用
.ConfigureAwait(false)
。
3.3 常见异步陷阱
-
async void:应尽量避免。async void方法无法被等待,且其中抛出的异常会直接触发SynchronizationContext的未处理异常事件,可能导致应用程序崩溃。仅用于事件处理程序(如按钮点击事件)。 -
死锁
:在拥有同步上下文的线程(如 UI 线程)上同步等待一个异步任务(使用
.Result或.Wait()),而该异步任务内部又需要该同步上下文来完成时,就会发生死锁。// UI 线程上执行,会导致死锁! public string GetData() { // .Result 会阻塞当前(UI)线程,等待任务完成。 // 但 DownloadStringAsync 内部的 await 完成后,试图回到UI线程来继续执行, // 而UI线程正被 .Result 阻塞着,互相等待 -> 死锁。 return DownloadStringAsync(url).Result; } // 正确做法:一直使用 async/await “异步到底” public async Task<string> GetDataAsync() { return await DownloadStringAsync(url); } -
忘记
await:如果你调用了一个返回Task的异步方法但没有await它,那么这个操作就是“即发即忘”的。如果该方法抛出异常,你将无法捕获。编译器通常会对此给出警告。// 错误:异常可能无法被观察到,且操作可能未完成就继续执行后续代码。 public void Process() { SaveDataAsync(); // 缺少 await Console.WriteLine("Data saved?"); // 不,可能还没保存完! }
3.4 异步与并行 (
Task.Run
vs
Parallel
)
-
异步 (
async/await) :主要解决 I/O 密集型操作的等待问题(如网络请求、文件读写、数据库查询),目的是释放线程去处理其他工作,提高资源利用率。 -
并行 (
Task.Run,Parallel.ForEach) :主要解决 CPU 密集型计算问题,目的是利用多核 CPU 同时执行多个计算任务,缩短总计算时间。
不要用
Task.Run
去包装一个本身就是异步的 I/O 操作,这只会增加不必要的线程池调度开销。正确的做法是直接
await
那个异步 I/O 方法。
// 错误:不必要的包装
public Task<string> GetDataAsync()
{
return Task.Run(() => _httpClient.GetStringAsync(url).Result); // 在后台线程同步等待,浪费线程
}
// 正确:直接返回异步任务
public Task<string> GetDataAsync()
{
return _httpClient.GetStringAsync(url);
}
对于 CPU 密集型工作,如果它阻塞时间较长,可以考虑用
Task.Run
将其推入线程池,避免阻塞 UI 线程或 ASP.NET Core 的请求线程。
// 在UI或Web请求上下文中处理CPU密集型工作
public async Task<int> CalculateAsync()
{
// 将CPU密集型计算卸载到线程池
return await Task.Run(() => PerformHeavyCalculation());
}
4. 高级特性:反射、特性与动态编程
反射允许在运行时检查类型、创建对象、调用方法、访问字段和属性。特性(Attribute)则为代码元素添加元数据。它们共同为框架开发、序列化、ORM、依赖注入等高级功能提供了基础。
4.1 反射基础
using System.Reflection;
Type type = typeof(Product); // 获取 Product 类的 Type 对象
// 获取所有公共属性
PropertyInfo[] properties = type.GetProperties();
foreach (var prop in properties)
{
Console.WriteLine($"Property: {prop.Name}, Type: {prop.PropertyType}");
}
// 创建实例 (需要无参构造函数)
object? instance = Activator.CreateInstance(type);
// 或者
Product? product = Activator.CreateInstance<Product>();
// 设置属性值
PropertyInfo nameProperty = type.GetProperty("Name");
nameProperty?.SetValue(instance, "Laptop");
// 调用方法
MethodInfo calculateMethod = type.GetMethod("CalculateTax");
object? result = calculateMethod?.Invoke(instance, new object[] { 100m });
反射功能强大但性能开销大,应谨慎使用,并考虑缓存
Type
和
MethodInfo
等对象。
4.2 自定义特性
特性是继承自
System.Attribute
的类。你可以用它们来装饰代码元素,然后在运行时通过反射读取这些信息。
// 1. 定义特性
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false)]
public class DisplayNameAttribute : Attribute
{
public string Name { get; }
public DisplayNameAttribute(string name) => Name = name;
}
// 2. 应用特性
[DisplayName("产品信息")]
public class Product
{
[DisplayName("产品名称")]
public string Name { get; set; }
public decimal Price { get; set; }
}
// 3. 通过反射读取特性
public static string GetDisplayName(Type type)
{
var attr = type.GetCustomAttribute<DisplayNameAttribute>();
return attr?.Name ?? type.Name;
}
public static string GetPropertyDisplayName(PropertyInfo property)
{
var attr = property.GetCustomAttribute<DisplayNameAttribute>();
return attr?.Name ?? property.Name;
}
特性广泛应用于数据验证(如
[Required]
,
[StringLength]
)、序列化控制(如
[JsonPropertyName]
)、ORM 映射(如
[Column]
)、依赖注入(如
[Inject]
)等场景。
4.3 动态类型 (
dynamic
)
dynamic
关键字绕过了编译时的类型检查,将类型解析推迟到运行时。这在处理 COM 对象、动态 JSON(如
JsonNode
)或与动态语言交互时非常有用,但牺牲了类型安全和 IDE 的智能提示。
dynamic obj = new System.Dynamic.ExpandoObject();
obj.Name = "Dynamic Object";
obj.Value = 42;
Console.WriteLine(obj.Name); // 输出: Dynamic Object
// 与反射相比,语法更简洁,但错误会在运行时才暴露
obj.NonExistentMethod(); // 编译通过,但运行时会抛出 RuntimeBinderException
使用建议
:仅在必要时使用
dynamic
,并尽量将其使用范围限制在最小。在大多数强类型场景下,使用接口、泛型或模式匹配是更安全的选择。
5. 项目实战:构建一个简单的依赖注入容器
依赖注入 (DI) 是现代 .NET 应用的核心模式。为了深入理解其原理,我们将手动实现一个极简的依赖注入容器。这个练习将综合运用接口、反射、泛型和生命周期管理等概念。
5.1 定义容器接口与生命周期
首先,我们定义服务生命周期枚举和容器接口。
public enum ServiceLifetime
{
Transient, // 每次请求都创建新实例
Scoped, // 在同一作用域内是单例
Singleton // 全局单例
}
public interface IServiceContainer
{
void Register<TService, TImplementation>(ServiceLifetime lifetime) where TImplementation : TService;
TService GetService<TService>();
object GetService(Type serviceType);
}
5.2 实现容器核心
public class SimpleContainer : IServiceContainer
{
private readonly Dictionary<Type, ServiceDescriptor> _descriptors = new();
// 注册服务
public void Register<TService, TImplementation>(ServiceLifetime lifetime) where TImplementation : TService
{
var descriptor = new ServiceDescriptor(
serviceType: typeof(TService),
implementationType: typeof(TImplementation),
lifetime: lifetime
);
_descriptors[typeof(TService)] = descriptor;
}
// 解析服务
public TService GetService<TService>()
{
return (TService)GetService(typeof(TService));
}
public object GetService(Type serviceType)
{
if (!_descriptors.TryGetValue(serviceType, out var descriptor))
{
throw new InvalidOperationException($"Service of type {serviceType.Name} is not registered.");
}
return CreateInstance(descriptor);
}
private object CreateInstance(ServiceDescriptor descriptor)
{
// 单例:如果已存在实例,直接返回
if (descriptor.Lifetime == ServiceLifetime.Singleton && descriptor.Instance != null)
{
return descriptor.Instance;
}
// 选择实现类型(支持注册接口->具体类)
Type implementationType = descriptor.ImplementationType ?? descriptor.ServiceType;
// 获取构造函数(这里简化:只取第一个公共构造函数)
ConstructorInfo constructor = implementationType.GetConstructors().FirstOrDefault();
if (constructor == null)
{
throw new InvalidOperationException($"No public constructor found for {implementationType.Name}");
}
// 解析构造函数参数
ParameterInfo[] parameters = constructor.GetParameters();
object[] parameterInstances = new object[parameters.Length];
for (int i = 0; i < parameters.Length; i++)
{
// 递归解析依赖
parameterInstances[i] = GetService(parameters[i].ParameterType);
}
// 创建实例
object instance = constructor.Invoke(parameterInstances);
// 如果是单例,保存实例
if (descriptor.Lifetime == ServiceLifetime.Singleton)
{
descriptor.Instance = instance;
}
return instance;
}
// 服务描述符内部类
private class ServiceDescriptor
{
public Type ServiceType { get; }
public Type? ImplementationType { get; }
public ServiceLifetime Lifetime { get; }
public object? Instance { get; set; }
public ServiceDescriptor(Type serviceType, Type? implementationType, ServiceLifetime lifetime)
{
ServiceType = serviceType;
ImplementationType = implementationType;
Lifetime = lifetime;
}
}
}
5.3 使用容器
// 定义接口和实现
public interface ILogger { void Log(string message); }
public class ConsoleLogger : ILogger { public void Log(string message) => Console.WriteLine($"[LOG] {message}"); }
public interface IDataService { string GetData(); }
public class DataService : IDataService
{
private readonly ILogger _logger;
// 构造函数注入
public DataService(ILogger logger)
{
_logger = logger;
_logger.Log("DataService created.");
}
public string GetData()
{
_logger.Log("Getting data...");
return "Sample Data";
}
}
// 配置和使用容器
class Program
{
static void Main(string[] args)
{
var container = new SimpleContainer();
container.Register<ILogger, ConsoleLogger>(ServiceLifetime.Singleton);
container.Register<IDataService, DataService>(ServiceLifetime.Transient);
var service = container.GetService<IDataService>();
string data = service.GetData();
Console.WriteLine($"Data: {data}");
}
}
这个简易容器演示了 DI 的核心:注册、解析和生命周期管理。生产级容器(如 .NET 内置的
IServiceCollection
)要复杂得多,包括作用域管理、泛型注册、工厂方法、装饰器模式等,但基本原理相通。
6. 生产环境考量与最佳实践
将代码从学习环境推向生产环境,需要关注更多非功能性需求。
6.1 错误处理与日志记录
-
结构化日志
:不要只用
Console.WriteLine。使用像 Serilog 或 NLog 这样的日志库,它们支持结构化日志、多种输出目标(文件、数据库、ELK)和日志级别。 -
全局异常处理
:在应用程序入口点(如
Main方法、ASP.NET Core 的中间件、WPF 的App.OnStartup)设置全局异常处理程序,记录未处理的异常,并给用户友好的反馈。 -
特定异常 vs 通用异常
:捕获最具体的异常类型,而不是通用的
Exception。这有助于更精确地处理错误。
6.2 性能与内存
-
字符串操作
:大量字符串拼接使用
StringBuilder。使用string.IsNullOrEmpty/string.IsNullOrWhiteSpace进行检查。 -
集合选择
:根据访问模式选择合适的集合。频繁查找用
Dictionary或HashSet,需要排序用SortedDictionary或SortedSet,简单列表用List。 -
避免装箱拆箱
:在性能敏感的循环中,使用泛型集合(如
List<int>)而非ArrayList。 -
使用
using语句 :对实现了IDisposable的对象(如文件流、数据库连接、网络流)使用using语句以确保资源被及时释放。
6.3 代码质量与可维护性
- 命名规范 :遵循 PascalCase(类、方法、属性)和 camelCase(局部变量、参数)约定。
- 单一职责 :类和方法应该只做一件事。
- 依赖倒置 :依赖于抽象(接口),而不是具体实现。这通过依赖注入实现。
- 单元测试 :为关键业务逻辑编写单元测试。使用像 xUnit、NUnit 或 MSTest 这样的测试框架,以及 Moq 或 NSubstitute 进行模拟。
6.4 配置与安全
-
不要硬编码
:将连接字符串、API 密钥、功能开关等配置信息放在配置文件(如
appsettings.json)或环境变量中。使用IConfiguration来读取。 - 输入验证 :永远不要信任用户输入。对来自外部的所有数据进行验证和清理,防止 SQL 注入、XSS 等攻击。
- 安全存储密钥 :使用像 Azure Key Vault、HashiCorp Vault 或 .NET Secret Manager(仅用于开发)这样的服务来管理密钥。
7. 下一步学习路径
掌握了本文介绍的中高级主题后,你可以根据兴趣方向深入:
- 领域驱动设计 (DDD) :学习实体、值对象、聚合、领域事件等概念,构建更符合业务逻辑的复杂系统。
- 微服务架构 :了解服务发现、API 网关、分布式事务、弹性模式(熔断、限流、重试)。
-
性能优化与诊断
:深入学习 .NET 内存管理、垃圾回收、性能分析工具(如 dotTrace、PerfView)、诊断工具(如
dotnet-counters,dotnet-dump)。 -
特定领域框架
:
- Web 开发 :深入 ASP.NET Core 中间件、过滤器、身份认证与授权、SignalR、gRPC。
- 桌面开发 :学习 WPF 的 MVVM 模式、数据绑定、控件模板;或 WinForms 的现代化实践。
- 移动开发 :探索 .NET MAUI 进行跨平台移动应用开发。
- 游戏开发 :了解 Unity 引擎与 C# 的配合。
- 函数式编程思想 :虽然 C# 主要是面向对象语言,但越来越多地融入函数式特性(如 LINQ、记录类型、模式匹配)。学习不可变性、纯函数等概念可以写出更安全、更易测试的代码。
学习的关键不是记住所有 API,而是理解设计模式、原则和权衡。持续编写代码,阅读优秀开源项目(如 .NET Runtime、ASP.NET Core、EF Core 的源码),参与实际项目,是成长为高级 C# 开发者的必经之路。

452

被折叠的 条评论
为什么被折叠?



