如果你已经掌握了C#的基础语法,能够写出简单的控制台程序,甚至完成了一些小项目,那么恭喜你,你已经成功迈出了第一步。但接下来,你可能会陷入一个典型的“中级开发者陷阱”:感觉什么都会一点,但面对一个稍复杂的真实项目,比如开发一个带用户管理、数据持久化和API接口的桌面应用,却不知从何下手,代码写着写着就变得混乱不堪。
这种感觉就像你学会了所有乐器的演奏方法,却不知道如何将它们编排成一首和谐的乐章。从“会写代码”到“会写好代码”,从“实现功能”到“构建可维护、可扩展的应用程序”,这中间的鸿沟,正是“进阶”所要跨越的核心。
本文是《Learn Complete C# – Beginner to Advanced》系列的第二部分,我们将不再讨论变量、循环这些基础语法。我们将直击痛点,深入C#进阶开发中那些真正决定代码质量、开发效率和项目成败的核心领域:面向对象设计的精髓、异常处理的正确姿势、集合与LINQ的高效运用、以及异步编程如何避免让程序“卡死”。这些知识,是把你从一个“代码搬运工”提升为“软件设计师”的关键。
读完本文,你将能清晰地构建中小型应用的骨架,优雅地处理程序中的各种意外,用更少的代码做更多的事,并写出响应迅速的程序。我们不仅讲“是什么”,更重点剖析“为什么”和“怎么用”,并附带大量可直接用于项目的代码示例。
1. 面向对象设计:超越“类与对象”的语法层面
很多教程讲到面向对象,就是“类、对象、继承、封装、多态”这十二个字。这没错,但这是语法概念。进阶的关键在于设计思想:如何用这些语法构建出灵活、健壮、易于变化的软件结构。
1.1 封装:不仅仅是private字段
封装的核心是“信息隐藏”和“职责明确”。一个设计良好的类,应该像一个黑盒,对外提供清晰、稳定的接口(方法),而将复杂的实现细节和易变的数据隐藏在内部。
反面教材:一个“透明”的订单类
// Bad Practice: 过度暴露内部细节 public class Order { public List<OrderItem> Items; // 公共字段,外部可以随意修改集合 public decimal TotalAmount; // 计算总额的责任被抛给了外部 public Customer Customer; // 直接暴露关联对象 }这段代码的问题在于:
- 外部代码可以直接
order.Items.Add(...)或order.Items.Clear(),破坏了订单项管理的逻辑完整性。 TotalAmount需要外部计算和维护,容易产生不一致。- 耦合度高,
Order的内部结构变化会直接影响所有使用它的代码。
正面示例:具有良好封装的订单类
// Good Practice: 隐藏细节,提供明确接口 public class Order { private readonly List<OrderItem> _items = new List<OrderItem>(); private readonly Customer _customer; // 通过构造函数注入依赖,确保订单创建时就有客户 public Order(Customer customer) { _customer = customer ?? throw new ArgumentNullException(nameof(customer)); CreatedTime = DateTime.UtcNow; } public DateTime CreatedTime { get; private set; } // 创建时间不应被修改 public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly(); // 只读视图 public Customer Customer => _customer; // 只读属性 // 核心业务逻辑封装在类内部 public decimal CalculateTotalAmount() { return _items.Sum(item => item.UnitPrice * item.Quantity); } public void AddItem(Product product, int quantity) { if (product == null) throw new ArgumentNullException(nameof(product)); if (quantity <= 0) throw new ArgumentException("Quantity must be positive.", nameof(quantity)); // 这里可以加入业务规则,例如检查库存、限购等 var existingItem = _items.FirstOrDefault(i => i.ProductId == product.Id); if (existingItem != null) { existingItem.IncreaseQuantity(quantity); } else { _items.Add(new OrderItem(product, quantity)); } } // 标记订单状态等业务操作 public void MarkAsPaid() { // 检查状态转换的合法性(如从未支付到已支付) // 触发领域事件等 } }设计要点分析:
- 不变性(Immutability):
_customer和CreatedTime在构造后不可变,保证了对象的部分状态始终一致。 - 控制修改途径:
Items属性返回一个只读集合,防止外部直接修改集合。添加物品必须通过AddItem方法,该方法内部可以执行所有必要的业务规则校验。 - 计算属性:
CalculateTotalAmount方法保证了总额总是根据当前订单项实时计算,不存在数据不一致。 - 明确的职责:
Order类负责管理订单项和计算逻辑,外部代码无需关心细节。
1.2 继承与多态:谨慎使用“是一个(is-a)”关系
继承是代码复用的强大工具,但滥用继承(尤其是深度继承)会导致代码僵化、脆弱。“组合优于继承”是更常被倡导的原则。
场景:我们有一个报表系统,需要生成PDF和Excel格式的报表。
不灵活的继承设计:
public abstract class ReportGenerator { public abstract void GenerateHeader(); public abstract void GenerateBody(); public abstract void GenerateFooter(); public void GenerateReport() { GenerateHeader(); GenerateBody(); GenerateFooter(); } } public class PdfReportGenerator : ReportGenerator { public override void GenerateHeader() { /* PDF特有的页眉逻辑 */ } public override void GenerateBody() { /* PDF特有的正文逻辑 */ } public override void GenerateFooter() { /* PDF特有的页脚逻辑 */ } } public class ExcelReportGenerator : ReportGenerator { public override void GenerateHeader() { /* Excel特有的页眉逻辑 */ } public override void GenerateBody() { /* Excel特有的正文逻辑 */ } public override void GenerateFooter() { /* Excel特有的页脚逻辑 */ } }问题:如果现在要增加一个CSV报表,或者报表的头部、正文、尾部逻辑需要复用(比如PDF和Word的页眉类似),这个继承体系就很难扩展。
更灵活的组合与策略模式设计:
// 定义策略接口 public interface IReportPartGenerator { void Generate(ReportData data); } public class PdfHeaderGenerator : IReportPartGenerator { /* ... */ } public class ExcelHeaderGenerator : IReportPartGenerator { /* ... */ } public class CommonBodyGenerator : IReportPartGenerator { /* ... */ } // 通用的正文生成器 public class PdfFooterGenerator : IReportPartGenerator { /* ... */ } public class ExcelFooterGenerator : IReportPartGenerator { /* ... */ } // 报告生成器上下文,组合不同的部分生成器 public class ReportGenerator { private readonly IReportPartGenerator _headerGenerator; private readonly IReportPartGenerator _bodyGenerator; private readonly IReportPartGenerator _footerGenerator; public ReportGenerator(IReportPartGenerator headerGenerator, IReportPartGenerator bodyGenerator, IReportPartGenerator footerGenerator) { _headerGenerator = headerGenerator; _bodyGenerator = bodyGenerator; _footerGenerator = footerGenerator; } public void GenerateReport(ReportData data) { _headerGenerator.Generate(data); _bodyGenerator.Generate(data); _footerGenerator.Generate(data); } } // 使用依赖注入等方式灵活组装 var pdfReportGenerator = new ReportGenerator( new PdfHeaderGenerator(), new CommonBodyGenerator(), // 复用了通用逻辑 new PdfFooterGenerator() );设计优势:
- 高内聚、低耦合:每个生成器只负责一个具体的部分,职责单一。
- 易于扩展:要新增一种报表格式(如CSV),只需实现对应的
IReportPartGenerator,无需修改现有类。 - 易于复用:
CommonBodyGenerator可以被PDF和Excel报告共用。 - 易于测试:每个部分都可以独立进行单元测试。
多态的真谛:多态的魅力不在于继承树有多深,而在于客户端代码依赖于抽象(接口),而非具体实现。上面的ReportGenerator依赖于IReportPartGenerator接口,至于运行时传入的是PDF还是Excel的实现,它并不关心,这就是多态。
2. 异常处理:从“抓了又抛”到构建健壮性
try-catch人人都会写,但如何有效处理异常却是区分新手和老手的重要标志。糟糕的异常处理会隐藏错误,让调试变得噩梦般困难。
2.1 异常处理的核心原则
- 只捕获你能处理的异常:不要用
catch (Exception ex)捕获所有异常然后什么都不做(空的catch块)或只是记录一下。如果你不知道如何处理这个异常,就应该让它向上层抛出。 - 使用具体的异常类型:优先捕获
FileNotFoundException、ArgumentNullException等具体异常,而不是通用的Exception。 - 异常信息要丰富:抛出自定义异常时,务必提供清晰易懂的Message,并考虑传递内部异常(InnerException)。
- 区分业务异常和系统异常:像“用户余额不足”、“商品已下架”这类属于可预见的业务规则违规,应使用自定义的业务异常(如
InsufficientBalanceException),而不是用系统异常(如InvalidOperationException)来代表。
2.2 实践:创建有意义的自定义异常
// 自定义业务异常 public class DomainException : Exception { // 可以添加错误码等业务相关属性 public string ErrorCode { get; } public DomainException(string message, string errorCode = null) : base(message) { ErrorCode = errorCode; } public DomainException(string message, Exception innerException, string errorCode = null) : base(message, innerException) { ErrorCode = errorCode; } } public class InsufficientBalanceException : DomainException { public decimal CurrentBalance { get; } public decimal RequiredAmount { get; } public InsufficientBalanceException(decimal currentBalance, decimal requiredAmount) : base($"Insufficient balance. Current: {currentBalance:C}, Required: {requiredAmount:C}", "INSUFFICIENT_BALANCE") { CurrentBalance = currentBalance; RequiredAmount = requiredAmount; } } // 使用示例 public class PaymentService { public void ProcessPayment(string userId, decimal amount) { var user = _userRepository.GetById(userId); if (user.Balance < amount) { // 抛出具体的业务异常,上层(如控制器)可以捕获并转换为对用户友好的错误信息 throw new InsufficientBalanceException(user.Balance, amount); } try { // 调用第三方支付网关 _paymentGateway.Charge(amount, user.PaymentToken); user.DeductBalance(amount); _userRepository.Update(user); } catch (PaymentGatewayException ex) // 捕获特定的第三方异常 { // 记录日志,并可能包装成更通用的业务异常向上抛 _logger.LogError(ex, "Payment gateway failed for user {UserId}", userId); throw new DomainException("Payment processing failed, please try again later.", ex, "PAYMENT_FAILED"); } // 不捕获其他未知异常,让它们冒泡到全局异常处理器 } }2.3 全局异常处理(ASP.NET Core Web API 示例)
在Web API中,你通常不希望任何未处理的异常直接返回给客户端。一个全局异常中间件可以优雅地处理这个问题。
// 自定义中间件 public class ExceptionHandlingMiddleware { private readonly RequestDelegate _next; private readonly ILogger<ExceptionHandlingMiddleware> _logger; public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger) { _next = next; _logger = logger; } public async Task InvokeAsync(HttpContext context) { try { await _next(context); } catch (DomainException ex) // 捕获业务异常 { _logger.LogWarning(ex, "A domain exception occurred."); context.Response.StatusCode = StatusCodes.Status400BadRequest; // 或自定义状态码 await context.Response.WriteAsJsonAsync(new { errorCode = ex.ErrorCode, message = ex.Message, // 对于InsufficientBalanceException,可以返回更详细的数据 currentBalance = (ex as InsufficientBalanceException)?.CurrentBalance, requiredAmount = (ex as InsufficientBalanceException)?.RequiredAmount }); } catch (Exception ex) // 捕获其他所有未预料异常 { _logger.LogError(ex, "An unhandled exception occurred."); // 生产环境不应返回详细错误给客户端 context.Response.StatusCode = StatusCodes.Status500InternalServerError; await context.Response.WriteAsJsonAsync(new { errorCode = "INTERNAL_SERVER_ERROR", message = "An internal server error has occurred." }); } } } // 在 Program.cs 或 Startup.cs 中注册 app.UseMiddleware<ExceptionHandlingMiddleware>();3. 集合与LINQ:告别循环,声明式编程
C#的LINQ(Language Integrated Query)是处理集合数据的革命性特性。它让你能用类似SQL的声明式语法来操作内存中的数据,代码更简洁,意图更清晰。
3.1 常用集合类型的选择
List<T>:最通用的动态数组。随机访问快(O(1)),但在中间插入/删除慢(O(n))。适用于大多数需要有序、可重复、频繁按索引访问的场景。Dictionary<TKey, TValue>:基于哈希表的键值对集合。按键查找、插入、删除都非常快(平均O(1))。适用于需要通过唯一键快速查找数据的场景。HashSet<T>:不包含重复元素的集合,基于哈希表。判断是否包含某个元素极快(O(1))。适用于需要去重或快速存在性检查的场景。Queue<T>/Stack<T>:先进先出(FIFO)/后进先出(LIFO)的集合。适用于任务队列、撤销操作等特定算法。IEnumerable<T>:所有集合的基接口,代表一个可枚举的序列。LINQ操作主要针对IEnumerable<T>。
3.2 LINQ核心操作:查询语法 vs 方法语法
LINQ有两种写法:查询语法(类似SQL)和方法语法(链式调用)。方法语法更灵活,也是更主流和推荐的方式。
public class Product { public int Id { get; set; } public string Name { get; set; } public string Category { get; set; } public decimal Price { get; set; } public int Stock { get; set; } } List<Product> products = GetProducts(); // 假设这个方法返回产品列表 // 目标:找出“电子产品”类别中,库存大于0,并按价格降序排列的前5个产品名和价格。 // 方法1:命令式编程(传统循环) - 冗长,意图分散 var resultTraditional = new List<(string Name, decimal Price)>(); foreach (var p in products) { if (p.Category == "Electronics" && p.Stock > 0) { resultTraditional.Add((p.Name, p.Price)); } } resultTraditional.Sort((a, b) => b.Price.CompareTo(a.Price)); var top5Traditional = resultTraditional.Take(5).ToList(); // 方法2:LINQ查询语法 - 清晰,但某些操作受限 var resultQuerySyntax = (from p in products where p.Category == "Electronics" && p.Stock > 0 orderby p.Price descending select new { p.Name, p.Price }).Take(5).ToList(); // 方法3:LINQ方法语法(推荐)- 灵活,功能强大,可读性高 var resultMethodSyntax = products .Where(p => p.Category == "Electronics" && p.Stock > 0) // 过滤 .OrderByDescending(p => p.Price) // 排序 .Select(p => new { p.Name, p.Price }) // 投影(选择需要的字段) .Take(5) // 取前5个 .ToList(); // 立即执行,将结果物化为列表显然,LINQ方法语法最简洁,且清晰地表达了“过滤 -> 排序 -> 投影 -> 限制 -> 物化”这一系列操作。
3.3 关键概念:延迟执行 vs 立即执行
这是LINQ最重要的概念之一,理解错误会导致性能问题。
- 延迟执行(Deferred Execution):像
Where,Select,OrderBy这样的操作,它们并不立即执行查询,而是返回一个“查询计划”(一个IEnumerable<T>)。真正的查询执行被推迟到当你迭代这个序列时(例如使用foreach,或调用ToList,ToArray,First,Count等方法)。 - 立即执行(Immediate Execution):像
ToList(),ToArray(),First(),Count(),Max()这样的操作,会立即触发查询执行,并返回具体的结果(列表、数组、单个值等)。
// 示例:延迟执行的陷阱 var expensiveQuery = products.Where(p => ExpensiveFilter(p)); // 1. 定义查询,未执行 Console.WriteLine("Query defined."); foreach (var item in expensiveQuery) // 2. 第一次迭代,执行过滤! { Console.WriteLine(item.Name); } // 假设 ExpensiveFilter 是一个耗时操作,这里会执行 N 次(N=products.Count) foreach (var item in expensiveQuery) // 3. 第二次迭代,再次执行过滤! { Console.WriteLine(item.Name); } // 又执行了 N 次!如果数据源是数据库,这就是两次独立的查询。 // 正确做法:对需要重用的结果进行物化 var filteredList = products.Where(p => ExpensiveFilter(p)).ToList(); // 立即执行一次,结果存入内存 Console.WriteLine("Query executed and materialized."); foreach (var item in filteredList) // 遍历内存列表,很快 { Console.WriteLine(item.Name); } foreach (var item in filteredList) // 再次遍历,还是很快 { Console.WriteLine(item.Name); }3.4 进阶LINQ操作:GroupBy, Join, Aggregate
// 1. GroupBy: 按类别分组,并计算每类的平均价格和总库存 var categoryStats = products .GroupBy(p => p.Category) .Select(g => new { Category = g.Key, AveragePrice = g.Average(p => p.Price), TotalStock = g.Sum(p => p.Stock), Products = g.ToList() // 如果需要保留组内所有产品 }) .ToList(); // 2. Join: 假设有另一个订单项列表,关联产品和订单项 List<OrderItem> orderItems = GetOrderItems(); var productSales = products .Join(orderItems, p => p.Id, // 产品表关联键 oi => oi.ProductId, // 订单项表关联键 (p, oi) => new { Product = p, OrderItem = oi }) // 结果选择器 .GroupBy(x => x.Product.Id) .Select(g => new { ProductId = g.Key, ProductName = g.First().Product.Name, TotalQuantitySold = g.Sum(x => x.OrderItem.Quantity), TotalRevenue = g.Sum(x => x.OrderItem.Quantity * x.OrderItem.UnitPrice) }) .OrderByDescending(x => x.TotalRevenue) .ToList(); // 3. Aggregate: 复杂的聚合操作,例如计算所有产品的总价值(价格*库存) decimal totalInventoryValue = products .Aggregate(0M, // 种子值(初始累加器) (sum, product) => sum + (product.Price * product.Stock)); // 累加函数 // 等同于:products.Sum(p => p.Price * p.Stock),但 Aggregate 更通用。4. 异步编程:释放被阻塞的线程
在桌面应用或Web服务器中,同步执行耗时操作(如文件I/O、网络请求、数据库查询)会阻塞当前线程,导致界面“卡死”或服务器无法响应新请求。异步编程(async/await)就是为了解决这个问题。
4.1 核心概念:async, await, Task
async:修饰方法,表明该方法内部包含异步操作。它本身不会让方法异步运行,只是允许方法体内使用await。await:用于等待一个Task或Task<T>完成。当执行到await时,当前方法会“暂停”,控制权返回给调用者,但当前线程不会被阻塞,可以去处理其他工作。当await的任务完成后,该方法会从暂停的地方继续执行(可能在原线程,也可能在新线程,由同步上下文决定)。Task/Task<T>:表示一个异步操作。Task无返回值,Task<T>返回类型为T的值。
4.2 从同步到异步:一个文件读取的对比
// 同步版本 - 在UI线程调用会导致界面冻结 public string ReadFileSynchronously(string filePath) { // Thread.Sleep 模拟耗时CPU操作(这是错误示范,会阻塞线程) // Thread.Sleep(5000); // 不要这样做! // 同步文件读取,会阻塞线程直到文件读完 string content = File.ReadAllText(filePath); return content; } // 异步版本 - 不会阻塞调用线程 public async Task<string> ReadFileAsynchronously(string filePath) { // 异步文件读取,await 会释放当前线程 string content = await File.ReadAllTextAsync(filePath); return content; } // 在UI事件处理函数中调用(如WPF/WinForms/MAUI按钮点击事件) private async void btnReadFile_Click(object sender, EventArgs e) { lblStatus.Text = "Reading file..."; btnReadFile.Enabled = false; try { // 调用异步方法,UI线程不会被阻塞 string content = await ReadFileAsynchronously("largefile.txt"); txtContent.Text = content; lblStatus.Text = "File read successfully."; } catch (FileNotFoundException ex) { lblStatus.Text = $"File not found: {ex.Message}"; } finally { btnReadFile.Enabled = true; } // 注意:事件处理函数本身是 `async void`,这是特例,通常方法应返回 `Task`。 }4.3 常见误区与最佳实践
误区1:async void滥用
- 只有事件处理程序(如按钮点击)才应使用
async void。 - 其他所有异步方法都应返回
Task或Task<T>。因为async void方法无法被等待,其内部抛出的异常会直接触发进程级的未处理异常事件,难以捕获。
// 错误 public async void ProcessDataAsync() { /* ... */ } // 正确 public async Task ProcessDataAsync() { /* ... */ }误区2:在同步方法中调用.Result或.Wait()这会导致死锁风险,尤其是在拥有同步上下文的环境中(如UI线程、ASP.NET Core旧版本)。
// 危险!可能导致死锁 public string GetData() { var task = SomeAsyncMethod(); // 返回 Task<string> return task.Result; // 同步阻塞等待任务完成 } // 正确做法:让同步方法也变成异步方法,并一直使用 await public async Task<string> GetDataAsync() { return await SomeAsyncMethod(); } // 如果无法修改调用链(如在Main方法中),可以使用 `.GetAwaiter().GetResult()` // 但这仍然是阻塞的,应作为最后手段。 public void MainMethod() { var result = SomeAsyncMethod().GetAwaiter().GetResult(); }误区3:不必要地包装同步代码为Task.RunTask.Run是将CPU密集型工作推送到线程池的好方法,但不应滥用它来包装本身就是异步的I/O操作或已经很快的同步操作。
// 不必要的包装 public async Task<string> ReadFileAsync(string path) { return await Task.Run(() => File.ReadAllText(path)); // File.ReadAllText 是同步的,这会占用线程池线程 } // 更好的做法:使用真正的异步API public async Task<string> ReadFileAsync(string path) { return await File.ReadAllTextAsync(path); // 使用异步版本 } // 正确的使用场景:CPU密集型计算 public async Task<int> CalculatePrimeCountAsync(int limit) { // 这是一个耗时的CPU计算,适合放到线程池 return await Task.Run(() => { int count = 0; for (int i = 2; i <= limit; i++) { if (IsPrime(i)) count++; } return count; }); }最佳实践:配置等待(ConfigureAwait)在类库代码中,如果不关心后续代码在哪个上下文(如UI线程)中恢复,应使用ConfigureAwait(false)。这可以避免不必要的线程上下文切换,提升性能,并有助于避免死锁。
public async Task<string> GetDataFromNetworkAsync() { using var httpClient = new HttpClient(); // 在类库中,我们通常不关心回到UI线程,所以使用 ConfigureAwait(false) string result = await httpClient.GetStringAsync("https://api.example.com/data") .ConfigureAwait(false); // 这里的后续代码会在线程池线程上执行,而不是UI线程 return ProcessData(result); }注意:在UI层(如按钮事件处理程序)中,通常需要回到UI线程来更新控件,所以不要使用ConfigureAwait(false)。
5. 实战:构建一个简单的异步数据处理器
让我们综合运用以上知识,构建一个模拟的“产品数据处理器”。它从文件异步读取产品数据,进行过滤和转换,然后异步保存到另一个文件,并处理可能出现的异常。
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; // 使用 System.Text.Json 进行序列化 using System.Threading.Tasks; namespace AdvancedCSharpDemo { public class ProductProcessor { private readonly ILogger _logger; public ProductProcessor(ILogger logger) { _logger = logger; } // 主处理流程 public async Task ProcessProductsAsync(string inputFilePath, string outputFilePath) { if (string.IsNullOrWhiteSpace(inputFilePath)) throw new ArgumentException("Input file path cannot be empty.", nameof(inputFilePath)); _logger.LogInfo($"Starting to process products from '{inputFilePath}'..."); List<Product> products; try { // 1. 异步读取文件 products = await ReadProductsFromFileAsync(inputFilePath); } catch (FileNotFoundException ex) { _logger.LogError($"Input file not found: {ex.Message}"); throw new DomainException($"Cannot find input file: {inputFilePath}", ex, "FILE_NOT_FOUND"); } catch (JsonException ex) { _logger.LogError($"Invalid JSON format in file: {ex.Message}"); throw new DomainException($"Invalid product data format in file.", ex, "INVALID_DATA"); } if (products == null || !products.Any()) { _logger.LogWarning("No products found in the input file."); return; } _logger.LogInfo($"Loaded {products.Count} products."); // 2. 使用LINQ进行数据处理 var processedData = ProcessProductData(products); // 3. 异步写入结果 await SaveProcessedDataAsync(processedData, outputFilePath); _logger.LogInfo($"Processing completed. Results saved to '{outputFilePath}'."); } private async Task<List<Product>> ReadProductsFromFileAsync(string filePath) { // 模拟异步读取 await Task.Delay(100); // 模拟I/O延迟 if (!File.Exists(filePath)) { throw new FileNotFoundException($"The file '{filePath}' does not exist."); } string jsonContent; try { jsonContent = await File.ReadAllTextAsync(filePath); } catch (IOException ex) { throw new DomainException($"Cannot read file '{filePath}'.", ex, "IO_ERROR"); } var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; var products = JsonSerializer.Deserialize<List<Product>>(jsonContent, options); return products ?? new List<Product>(); // 如果反序列化结果为null,返回空列表 } private List<ProcessedProductInfo> ProcessProductData(List<Product> products) { // 使用LINQ进行复杂的查询和转换 return products .Where(p => p.Stock > 0) // 只处理有库存的产品 .GroupBy(p => p.Category) .Select(g => new ProcessedProductInfo { Category = g.Key, TotalValueInStock = g.Sum(p => p.Price * p.Stock), AveragePrice = g.Average(p => p.Price), Products = g.OrderByDescending(p => p.Price) .Select(p => new ProductSummary { Id = p.Id, Name = p.Name, Price = p.Price, Stock = p.Stock }).ToList() }) .OrderByDescending(info => info.TotalValueInStock) .ToList(); } private async Task SaveProcessedDataAsync(List<ProcessedProductInfo> data, string outputFilePath) { var options = new JsonSerializerOptions { WriteIndented = true }; string jsonOutput = JsonSerializer.Serialize(data, options); try { // 确保目录存在 var directory = Path.GetDirectoryName(outputFilePath); if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) { Directory.CreateDirectory(directory); } await File.WriteAllTextAsync(outputFilePath, jsonOutput); } catch (IOException ex) { throw new DomainException($"Cannot write to output file '{outputFilePath}'.", ex, "IO_ERROR"); } } } // 辅助类 public class ProcessedProductInfo { public string Category { get; set; } public decimal TotalValueInStock { get; set; } public decimal AveragePrice { get; set; } public List<ProductSummary> Products { get; set; } = new List<ProductSummary>(); } public class ProductSummary { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } public int Stock { get; set; } } // 简单的日志接口 public interface ILogger { void LogInfo(string message); void LogWarning(string message); void LogError(string message); } // 使用示例 public class Program { static async Task Main(string[] args) { var logger = new ConsoleLogger(); var processor = new ProductProcessor(logger); try { await processor.ProcessProductsAsync("products.json", "output/results.json"); Console.WriteLine("Processing succeeded."); } catch (DomainException ex) { Console.WriteLine($"Business error ({ex.ErrorCode}): {ex.Message}"); } catch (Exception ex) { Console.WriteLine($"Unexpected error: {ex.Message}"); } } } public class ConsoleLogger : ILogger { public void LogError(string message) => Console.WriteLine($"[ERROR] {DateTime.Now}: {message}"); public void LogInfo(string message) => Console.WriteLine($"[INFO] {DateTime.Now}: {message}"); public void LogWarning(string message) => Console.WriteLine($"[WARN] {DateTime.Now}: {message}"); } }6. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| LINQ查询性能慢 | 1. 延迟执行导致重复查询数据库/耗时操作。 2. 在循环内执行LINQ查询(N+1问题)。 3. 对大数据集使用 ToList()过早,导致内存压力。 | 1. 使用性能分析工具(如Visual Studio Profiler)。 2. 检查SQL Profiler(如果使用EF Core),查看生成的SQL语句。 | 1. 对需要重用的查询结果及时使用ToList()或ToArray()物化。2. 使用 Include或投影(Select)一次性加载关联数据,避免N+1查询。3. 考虑使用分页( Skip/Take)。 |
async/await导致UI仍然卡顿 | 1. 在await后执行了耗时的同步CPU工作。2. 错误地使用了 .Result或.Wait()导致死锁。 | 1. 检查await后的代码是否包含密集循环或复杂计算。2. 检查调用栈中是否有同步阻塞调用。 | 1. 将CPU密集型工作用Task.Run包装并await。2. 确保异步调用链一路 async/await到底,避免同步阻塞。在类库代码中使用ConfigureAwait(false)。 |
集合修改时抛出InvalidOperationException | 在foreach循环中直接修改正在遍历的集合(如List)。 | 查看异常堆栈,定位到修改集合的代码行。 | 1. 遍历集合的副本:foreach(var item in list.ToList())。2. 将要删除/修改的元素先记录在另一个列表中,循环结束后再处理。 |
| 多线程下集合操作不安全 | 多个线程同时读写非线程安全的集合(如List<T>)。 | 观察随机出现的索引越界、数据损坏等异常。 | 1. 使用线程安全集合:ConcurrentBag<T>,ConcurrentDictionary<TKey,TValue>。2. 使用锁( lock)来保护对集合的访问。 |
| 自定义异常信息不清晰 | 异常Message过于简单,没有包含关键的业务数据。 | 查看异常日志,判断是否能快速定位问题。 | 在自定义异常的构造函数中,将相关参数(如ID、状态等)格式化到Message中,或作为异常属性暴露。 |
| 内存泄漏(对象未释放) | 1. 事件注册后未注销。 2. 静态集合长期持有对象引用。 3. 非托管资源未正确实现 IDisposable。 | 使用内存分析工具(如 dotMemory, Visual Studio Diagnostic Tools)。 | 1. 事件订阅者生命周期短于发布者时,记得取消订阅。 2. 定期清理静态缓存。 3. 正确实现 Dispose模式,并在using语句中使用。 |
7. 最佳实践与工程建议
- 面向接口编程:依赖抽象(接口),而非具体实现。这提高了代码的可测试性和可扩展性。上面的
ILogger就是一个简单例子。 - 遵循单一职责原则:一个类只应有一个引起它变化的原因。如果一个类负责用户认证、数据验证、邮件发送和日志记录,它就已经违反了此原则。
- 善用
using语句:对于实现了IDisposable接口的对象(如文件流、数据库连接、HttpClient在某些场景下),务必使用using语句确保资源被及时释放。using (var stream = new FileStream("file.txt", FileMode.Open)) { // 使用 stream } // 离开作用域时自动调用 stream.Dispose() // C# 8.0 起可以使用 using 声明,更简洁 using var stream = new FileStream("file.txt", FileMode.Open); - 为异步方法添加
Async后缀:这是一个广泛接受的命名约定,如GetDataAsync,方便区分同步和异步方法。 - 谨慎使用静态类和静态成员:静态成员在整个应用程序域生命周期内存在,可能导致状态难以管理和测试困难。优先考虑依赖注入。
- 编写单元测试:对核心业务逻辑(如
ProcessProductData方法)编写单元测试。使用测试框架(如 xUnit, NUnit, MSTest)和模拟框架(如 Moq, NSubstitute)。良好的设计(如依赖注入)会让代码更容易测试。 - 代码格式化与命名规范:使用一致的代码风格。遵循 C# 命名约定(类名
PascalCase,局部变量camelCase,常量UPPER_CASE)。使用.editorconfig文件来统一团队规范。
从理解语法到精通设计,从能跑通代码到构建健壮应用,C#进阶之路的关键在于思维的转变。不再满足于“它能工作”,而要追求“它工作得好、易于理解和修改”。本文探讨的面向对象设计、异常处理、LINQ和异步编程,正是支撑这一转变的四大支柱。掌握它们,你编写的将不再是简单的脚本,而是结构清晰、应对变化、高效可靠的专业级软件。
下一步,你可以深入探索依赖注入、单元测试、Entity Framework Core、ASP.NET Core Web API 开发等更具体的领域技术,将这些核心原则应用到实际的项目框架中,你的开发能力将迎来质的飞跃。