C#编程实例大全与核心技术详解
简介:C#是由微软开发的面向对象编程语言,广泛应用于Windows平台、.NET框架、Unity游戏开发以及桌面和Web应用开发。本资源“程序集C#”是一个包含多个实践示例的集合,适合初学者和进阶开发者学习和使用。内容涵盖C#基础语法、面向对象编程、异常处理、LINQ、委托与事件、异步编程、泛型、集合操作、文件流处理、网络编程及单元测试等核心技术。通过实例操作,开发者可全面提升C#编程能力,掌握实际项目开发所需技能。
1. C#基础语法与实例应用
本章将从最基础的C#语法入手,帮助初学者建立扎实的编程基础。我们首先了解C#的基本语法结构,包括变量声明、常用数据类型(如int、string、bool)、运算符的使用,以及程序流程控制语句如if条件判断、for循环和while循环。通过一个简单的控制台应用程序实例,展示如何将这些语法元素组合在一起,实现一个完整的功能逻辑。
例如,以下是一个输出1到10数字的简单循环示例:
using System;
class Program
{
static void Main()
{
for (int i = 1; i <= 10; i++)
{
Console.WriteLine(i); // 输出当前i的值
}
}
}
代码说明:
using System;:引入System命名空间,包含基本输入输出类。class Program:定义一个类Program,C#程序的基本组成单位。static void Main():程序入口点,程序运行时从这里开始执行。for循环:控制变量i从1递增到10,每次循环输出i的值。Console.WriteLine(i);:向控制台输出当前i的值。
该示例展示了C#程序的基本结构与控制流的实现方式,为后续章节中更复杂的编程概念打下基础。
2. 面向对象编程(类、对象、继承、多态)
面向对象编程(Object-Oriented Programming,简称 OOP)是现代软件开发中最主流的编程范式之一。C#作为一门面向对象的语言,充分支持封装、继承和多态这三大核心特性。本章将深入探讨这些概念在C#中的具体实现方式,并通过代码示例展示其应用。理解并掌握OOP思想,对于开发结构清晰、易于维护和扩展的大型应用至关重要。
2.1 面向对象编程概述
面向对象编程是一种以对象为中心的编程方法,对象是数据和操作数据的方法的集合。OOP的核心理念在于通过封装、继承和多态来组织代码,提升代码的复用性和可维护性。
2.1.1 OOP核心概念:封装、继承、多态
封装(Encapsulation)
封装是将数据和行为包装在一个类中,并通过访问修饰符控制其可见性。这样可以隐藏实现细节,仅暴露必要的接口供外部调用。
代码示例:
public class Person
{
private string name;
public string Name
{
get { return name; }
set { name = value; }
}
public void SayHello()
{
Console.WriteLine($"Hello, my name is {name}");
}
}
逐行分析:
private string name;:声明一个私有字段,只能在类内部访问。public string Name:定义一个公共属性,用于安全地访问私有字段。get { return name; }:获取字段值。set { name = value; }:设置字段值。public void SayHello():定义一个公共方法,用于输出问候语。
继承(Inheritance)
继承允许一个类(派生类)继承另一个类(基类)的成员,从而实现代码的复用和层次结构的构建。
代码示例:
public class Animal
{
public void Eat()
{
Console.WriteLine("This animal is eating.");
}
}
public class Dog : Animal
{
public void Bark()
{
Console.WriteLine("Woof!");
}
}
逻辑分析:
Dog类继承了Animal类的Eat方法。Bark是Dog自身定义的方法。- 通过继承,
Dog对象可以直接调用Eat方法。
多态(Polymorphism)
多态是指一个对象可以以多种形态出现。在C#中,多态通常通过方法重写(virtual/override)或接口实现来体现。
代码示例:
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("Animal sound");
}
}
public class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine("Meow");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Woof");
}
}
参数说明:
virtual:允许派生类重写该方法。override:在派生类中重新定义基类的虚方法。- 多态允许我们通过基类引用调用派生类的方法。
Animal myAnimal = new Cat();
myAnimal.MakeSound(); // 输出 "Meow"
2.1.2 类与对象的关系
类是对象的模板或蓝图,对象是类的实例。类定义了对象的状态(字段)和行为(方法),而对象则是类的具体表现。
代码示例:
public class Car
{
public string Brand { get; set; }
public int Year { get; set; }
public void StartEngine()
{
Console.WriteLine($"{Brand} engine started.");
}
}
// 创建对象
Car myCar = new Car();
myCar.Brand = "Toyota";
myCar.Year = 2020;
myCar.StartEngine(); // 输出 "Toyota engine started."
表格:类与对象对比
| 特性 | 类(Class) | 对象(Object) |
|---|---|---|
| 定义 | 模板、蓝图 | 实例化后的具体实体 |
| 状态 | 包含字段和属性定义 | 具有具体值的状态 |
| 行为 | 定义方法 | 调用实际的方法 |
| 内存分配 | 不占用内存 | 占用内存 |
| 生命周期 | 永久存在(编译时) | 有生命周期(运行时) |
2.2 类的定义与使用
在C#中,类是最基本的类型单位。通过类可以定义对象的结构和行为,包括构造函数、析构函数、成员变量和访问修饰符等。
2.2.1 构造函数与析构函数
构造函数用于在创建对象时初始化对象的状态,析构函数则用于在对象被销毁时释放资源。
代码示例:
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
// 构造函数
public Product(string name, decimal price)
{
Name = name;
Price = price;
Console.WriteLine("Product created.");
}
// 析构函数
~Product()
{
Console.WriteLine("Product destroyed.");
}
}
// 使用
Product p = new Product("Laptop", 999.99m);
执行逻辑说明:
- 构造函数
Product(string name, decimal price)在对象实例化时自动调用。 - 析构函数
~Product()在对象被垃圾回收器(GC)回收时调用,通常用于释放非托管资源。 - 构造函数可重载,析构函数不能。
2.2.2 成员变量与方法的访问修饰符
C#提供了多种访问修饰符来控制类成员的可访问性,包括:
| 修饰符 | 作用范围 |
|---|---|
public |
可从任何位置访问 |
private |
仅类内部可访问 |
protected |
类及其派生类可访问 |
internal |
同一程序集内可访问 |
protected internal |
同一程序集或派生类中可访问 |
代码示例:
public class BankAccount
{
private decimal balance;
public void Deposit(decimal amount)
{
if (amount > 0)
balance += amount;
}
public decimal GetBalance()
{
return balance;
}
}
逻辑分析:
private decimal balance;:对外隐藏,只能通过方法修改。Deposit方法用于安全地修改余额。GetBalance方法用于读取余额,防止外部直接访问。
2.3 继承与多态实现
继承和多态是面向对象编程中实现代码复用和灵活性的关键机制。通过继承,可以构建类的层次结构;通过多态,可以实现灵活的接口设计和运行时绑定。
2.3.1 基类与派生类的定义
基类是其他类继承的基础类,派生类是基于基类扩展的新类。
代码示例:
// 基类
public class Vehicle
{
public virtual void Move()
{
Console.WriteLine("Vehicle is moving.");
}
}
// 派生类
public class Bicycle : Vehicle
{
public override void Move()
{
Console.WriteLine("Bicycle is pedaling.");
}
}
逻辑流程图:
classDiagram
direction LR
Vehicle <|-- Bicycle
Vehicle : +Move()
Bicycle : +Move()
说明:
Vehicle是基类,包含一个虚方法Move()。Bicycle继承自Vehicle并重写了Move()方法。
2.3.2 方法重写与虚方法
虚方法( virtual )允许派生类对其进行重写( override ),实现运行时多态。
代码示例:
public class Shape
{
public virtual double Area()
{
return 0;
}
}
public class Circle : Shape
{
public double Radius { get; set; }
public override double Area()
{
return Math.PI * Radius * Radius;
}
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override double Area()
{
return Width * Height;
}
}
使用方式:
Shape shape1 = new Circle() { Radius = 5 };
Shape shape2 = new Rectangle() { Width = 4, Height = 6 };
Console.WriteLine(shape1.Area()); // 输出圆的面积
Console.WriteLine(shape2.Area()); // 输出矩形的面积
逻辑分析:
- 程序在运行时根据对象的实际类型决定调用哪个
Area()方法。 - 多态允许我们使用统一的接口处理不同类型的对象。
2.3.3 接口与抽象类的对比与应用
| 特性 | 接口(Interface) | 抽象类(Abstract Class) |
|---|---|---|
| 方法实现 | 仅定义,无实现 | 可以有实现 |
| 多继承支持 | 支持多个接口 | 不支持多继承 |
| 构造函数 | 不允许有构造函数 | 可以有构造函数 |
| 字段 | 不允许有字段 | 允许有字段 |
| 设计目的 | 定义行为契约 | 定义共享逻辑和接口 |
代码示例:接口
public interface ILogger
{
void Log(string message);
}
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine($"Log: {message}");
}
}
代码示例:抽象类
public abstract class Animal
{
public abstract void MakeSound();
public void Sleep()
{
Console.WriteLine("Sleeping...");
}
}
public class Lion : Animal
{
public override void MakeSound()
{
Console.WriteLine("Roar!");
}
}
逻辑分析:
- 接口更适合定义行为规范,实现多继承。
- 抽象类适合定义部分实现,同时要求子类完成其余实现。
本章通过深入剖析C#中面向对象编程的三大核心特性:封装、继承和多态,并结合构造函数、访问修饰符、虚方法、接口与抽象类的使用,全面展示了如何在实际项目中构建模块化、可扩展和易于维护的代码结构。下一章我们将进入C#异常处理机制,进一步提升代码的健壮性与稳定性。
3. 异常处理机制(try-catch块使用)
在C#编程中,异常处理机制是确保程序稳定性和健壮性的关键部分。通过有效的异常处理,开发者可以捕获并处理运行时错误,从而避免程序崩溃并提供更友好的用户反馈。本章将从异常处理的基本概念出发,逐步深入到异常处理的实践技巧和最佳实践,帮助开发者构建更加健壮的应用程序。
3.1 异常处理的基本概念
异常处理是程序在运行过程中遇到错误时所采取的一种处理机制。C#中的异常处理通过 try-catch-finally 结构实现,它允许程序在发生异常时进行捕获、处理,并在最后执行清理操作。
3.1.1 异常类型与异常对象
在C#中,所有异常都继承自 System.Exception 类。常见的内置异常类型包括:
| 异常类型 | 描述 |
|---|---|
System.Exception |
所有异常的基类 |
System.ArgumentException |
参数无效时抛出 |
System.NullReferenceException |
访问空对象成员时抛出 |
System.DivideByZeroException |
除以零时抛出 |
System.IndexOutOfRangeException |
索引超出范围时抛出 |
System.IO.IOException |
输入/输出操作失败时抛出 |
每个异常对象都包含以下重要属性:
Message:描述异常的简要信息。StackTrace:提供异常发生时调用堆栈的信息。InnerException:指示引发当前异常的原始异常。
示例代码如下:
try
{
int[] numbers = { 1, 2, 3 };
Console.WriteLine(numbers[5]); // 触发IndexOutOfRangeException
}
catch (IndexOutOfRangeException ex)
{
Console.WriteLine("异常信息:" + ex.Message);
Console.WriteLine("堆栈跟踪:" + ex.StackTrace);
}
代码分析:
try块中执行可能抛出异常的代码;catch块用于捕获特定类型的异常;ex.Message提供异常描述;ex.StackTrace显示异常发生的调用路径。
3.1.2 try-catch-finally结构
try-catch-finally 是C#中处理异常的标准结构,其语法如下:
try
{
// 可能抛出异常的代码
}
catch (ExceptionType ex)
{
// 处理特定类型的异常
}
finally
{
// 始终执行的清理代码(无论是否发生异常)
}
流程图如下所示:
graph TD
A[开始执行try块] --> B{是否发生异常?}
B -->|是| C[进入catch块处理异常]
B -->|否| D[继续执行try块剩余代码]
C --> E[执行finally块]
D --> E
E --> F[结束]
示例代码:
FileStream file = null;
try
{
file = new FileStream("test.txt", FileMode.Open);
int data = file.ReadByte();
Console.WriteLine("读取到的数据:" + data);
}
catch (FileNotFoundException ex)
{
Console.WriteLine("文件未找到:" + ex.Message);
}
catch (IOException ex)
{
Console.WriteLine("IO异常:" + ex.Message);
}
finally
{
if (file != null)
{
file.Close();
Console.WriteLine("文件已关闭");
}
}
逻辑分析:
try块尝试打开文件并读取数据;- 如果文件不存在,会触发
FileNotFoundException; - 如果读取过程中发生错误,会触发
IOException; finally块确保即使发生异常,也能关闭文件流,防止资源泄漏。
3.2 异常处理的实践技巧
异常处理不仅仅是捕获错误,还包括如何记录错误信息、如何构建自定义异常类,以及如何利用这些信息进行调试和优化程序行为。
3.2.1 自定义异常类的创建
在实际开发中,有时内置异常类型无法满足业务需求。此时可以创建自定义异常类,继承自 Exception 或其子类。
步骤如下:
- 定义一个类,继承自
Exception; - 提供构造函数,支持传递消息和内部异常;
- 可选:添加自定义属性以提供更多上下文信息。
public class InvalidUserInputException : Exception
{
public string InputValue { get; }
public InvalidUserInputException() : base() { }
public InvalidUserInputException(string message) : base(message) { }
public InvalidUserInputException(string message, Exception inner) : base(message, inner) { }
public InvalidUserInputException(string message, string inputValue)
: base(message)
{
InputValue = inputValue;
}
}
使用示例:
void ValidateInput(string input)
{
if (string.IsNullOrEmpty(input))
throw new InvalidUserInputException("输入不能为空", input);
}
try
{
ValidateInput("");
}
catch (InvalidUserInputException ex)
{
Console.WriteLine($"错误:{ex.Message},输入值:{ex.InputValue}");
}
分析:
- 自定义异常类
InvalidUserInputException允许携带输入值; - 捕获该异常后可以获取更多上下文信息,便于调试和日志记录。
3.2.2 异常日志记录与调试技巧
在大型项目中,仅仅捕获异常是不够的,还需要记录异常信息以便后续分析。常用做法是将异常信息写入日志文件或发送到监控系统。
推荐做法:
- 使用日志框架如
NLog、log4net或Serilog; - 在
catch块中记录异常的Message、StackTrace和InnerException; - 不要吞异常(即空的
catch块); - 使用
Debug.WriteLine()或断点调试来分析异常发生时的上下文。
示例:使用 System.Diagnostics.Debug 记录异常
catch (Exception ex)
{
Debug.WriteLine("异常类型:" + ex.GetType().Name);
Debug.WriteLine("消息:" + ex.Message);
Debug.WriteLine("堆栈:" + ex.StackTrace);
if (ex.InnerException != null)
{
Debug.WriteLine("内部异常:" + ex.InnerException.Message);
}
}
分析:
Debug.WriteLine()适用于调试阶段,不会影响发布版本;- 在调试器附加时输出日志,便于快速定位问题;
- 包含内部异常信息可以追踪更深层次的问题根源。
3.3 异常处理的最佳实践
良好的异常处理不仅仅是捕获错误,更是一种设计哲学,它关乎程序的健壮性、可维护性和用户体验。
3.3.1 避免空catch块
空的 catch 块会导致异常被“吞掉”,程序看似正常运行,但实际可能已处于错误状态。这种做法会严重干扰调试和维护。
反例:
try
{
// 可能出错的代码
}
catch
{
// 什么也不做
}
改进做法:
- 至少记录异常信息;
- 或重新抛出异常以让上层处理;
- 或根据业务逻辑进行适当处理。
catch (Exception ex)
{
Console.WriteLine("发生异常:" + ex.Message);
// 可选:重新抛出异常
throw;
}
3.3.2 异常处理与程序健壮性的关系
异常处理是程序健壮性的重要组成部分。一个设计良好的异常处理机制应当:
- 避免程序崩溃 :即使发生异常,也能给出合理的提示并保持程序运行;
- 资源安全释放 :在
finally块中释放资源,如关闭文件、数据库连接等; - 异常封装与传递 :将底层异常封装为更高级别的业务异常,便于调用方理解;
- 避免过度捕获 :只捕获你能够处理的异常类型,不要捕获所有异常;
- 异步异常处理 :在
async/await中正确处理异常,使用try-catch包裹await表达式。
示例:异步方法中处理异常
async Task<int> GetDataAsync()
{
try
{
var result = await FetchDataFromServerAsync();
return result;
}
catch (HttpRequestException ex)
{
Console.WriteLine("网络请求失败:" + ex.Message);
return -1;
}
}
分析:
- 在
await表达式外部包裹try-catch,确保异步异常被捕获; - 返回默认值
-1表示失败,调用方可以根据此值判断; - 这种方式避免了未处理的异常导致整个程序崩溃。
通过本章的学习,我们不仅掌握了C#中异常处理的基本语法结构,还了解了如何创建自定义异常、记录日志以及遵循异常处理的最佳实践。这些内容构成了构建健壮应用程序的基础,为后续章节中更复杂的编程模型(如异步编程、LINQ查询、委托事件等)提供了坚实的支撑。
4. LINQ查询表达式与数据操作
LINQ(Language Integrated Query)是C#中一项极具表现力和实用性的语言特性,它将查询能力直接集成到C#语言中,使得开发者能够以统一的方式对各种数据源(如集合、数据库、XML等)进行高效的数据查询和操作。本章将深入讲解LINQ的基础概念、数据查询操作以及其在数据库交互中的应用,帮助开发者在实际项目中灵活运用LINQ,提高代码的可读性与开发效率。
4.1 LINQ基础概念
4.1.1 LINQ的组成部分与适用场景
LINQ 由多个组件构成,包括:
- LINQ to Objects :用于在内存中的集合(如 List 、Array 等)上进行查询。
- LINQ to SQL :用于与 SQL Server 数据库交互。
- LINQ to XML :用于处理 XML 文档。
- LINQ to Entities :用于与 Entity Framework 交互。
- LINQ to DataSet :用于查询 ADO.NET 的 DataSet。
适用场景对比表:
| 组件名称 | 适用数据源 | 特点说明 |
|---|---|---|
| LINQ to Objects | 内存集合 | 高性能、适用于数据操作 |
| LINQ to SQL | SQL Server | 映射简单、适合小型数据库应用 |
| LINQ to XML | XML 文档 | 提供强类型操作 XML 的能力 |
| LINQ to Entities | Entity Framework | 支持多种数据库,适合大型项目 |
| LINQ to DataSet | ADO.NET DataSet | 适合已有 ADO.NET 架构的系统迁移 |
4.1.2 查询语法与方法语法的区别
LINQ 提供了两种查询方式: 查询语法(Query Syntax) 和 方法语法(Method Syntax) 。
查询语法示例:
var query = from p in people
where p.Age > 25
select p;
方法语法示例:
var query = people.Where(p => p.Age > 25);
两种语法在底层编译后是等效的,但它们在表达方式和可读性上有一定区别。
| 特性 | 查询语法 | 方法语法 |
|---|---|---|
| 可读性 | 更适合初学者阅读 | 更适合熟悉 Lambda 表达式的开发者 |
| 灵活性 | 有限(不支持所有操作符) | 完全支持所有 LINQ 操作符 |
| 与数据库交互 | 更直观地映射 SQL 查询 | 更加函数式风格 |
代码逻辑分析:
以方法语法为例:
people.Where(p => p.Age > 25);
people是一个IEnumerable<Person>类型的集合。Where()是 LINQ 的扩展方法,接受一个Func<Person, bool>委托。p => p.Age > 25是一个 Lambda 表达式,用于筛选年龄大于25岁的人员。
4.2 使用LINQ进行数据查询
4.2.1 集合数据的筛选与排序
LINQ 提供了丰富的操作符用于对集合进行筛选(Filtering)和排序(Ordering)。
示例:筛选与排序操作
var filteredPeople = people
.Where(p => p.Age > 25)
.OrderBy(p => p.Name)
.ThenByDescending(p => p.Age);
逻辑分析:
Where():筛选出年龄大于25的记录。OrderBy():按姓名升序排列。ThenByDescending():若姓名相同,按年龄降序排列。
表格:常用LINQ操作符功能说明
| 操作符 | 功能描述 | 示例代码 |
|---|---|---|
| Where | 筛选符合条件的元素 | .Where(p => p.Age > 30) |
| OrderBy | 按指定字段升序排列 | .OrderBy(p => p.Name) |
| ThenBy | 多字段排序(升序) | .OrderBy(p => p.Name).ThenBy(p => p.Age) |
| OrderByDescending | 按指定字段降序排列 | .OrderByDescending(p => p.Age) |
| Select | 投影,选择部分字段或转换数据 | .Select(p => p.Name.ToUpper()) |
查询语法与方法语法的等价性图示(Mermaid流程图):
graph TD
A[查询语法] --> B(LINQ 查询表达式)
B --> C[转换为方法调用]
C --> D[编译为 IL 代码]
A --> D
E[方法语法] --> C
4.2.2 联接与分组操作
LINQ 支持对多个集合进行联接(Join)和分组(Grouping)操作。
示例:使用 Join 联接两个集合
var query = from p in people
join d in departments on p.DepartmentId equals d.Id
select new { p.Name, d.DepartmentName };
方法语法实现:
var query = people.Join(
departments,
p => p.DepartmentId,
d => d.Id,
(p, d) => new { p.Name, d.DepartmentName }
);
分组操作示例:
var groupedPeople = from p in people
group p by p.Gender into g
select new { Gender = g.Key, Count = g.Count() };
参数说明:
group p by p.Gender into g:按性别分组。g.Key:表示分组的关键字段(性别)。g.Count():统计每组人数。
4.3 LINQ与数据库交互
4.3.1 LINQ to SQL简介
LINQ to SQL 是 Microsoft 提供的一种轻量级 ORM(对象关系映射)框架,专为 SQL Server 设计。它允许开发者将数据库表映射为 C# 类,并使用 LINQ 进行查询、插入、更新和删除操作。
示例:使用 LINQ to SQL 查询数据库
using (var db = new MyDataContext())
{
var query = from c in db.Customers
where c.Country == "USA"
select c;
foreach (var customer in query)
{
Console.WriteLine(customer.CompanyName);
}
}
参数说明:
MyDataContext:数据库上下文类,由 LINQ to SQL 自动生成。Customers:映射的数据库表。- 查询将被转换为 SQL 并在数据库中执行。
4.3.2 实体模型的创建与查询优化
创建实体模型流程图:
graph TD
A[数据库表] --> B[LINQ to SQL 类设计器]
B --> C[生成实体类]
C --> D[在代码中使用实体类进行查询]
查询优化技巧:
-
延迟执行(Deferred Execution) :
- LINQ 查询默认是延迟执行的,直到遍历结果集(如foreach或ToList())才会真正执行。
- 示例:csharp var query = from c in db.Customers where c.Country == "USA"; // 此时查询未执行 var list = query.ToList(); // 执行查询 -
避免 N+1 查询问题 :
- 使用DataLoadOptions预加载关联表数据。csharp var options = new DataLoadOptions(); options.LoadWith<Customer>(c => c.Orders); db.LoadOptions = options; -
使用
AsNoTracking()提高性能 :
- 当不需要跟踪实体状态时,可以使用:csharp var query = db.Customers.AsNoTracking().Where(c => c.Country == "USA");
表格:LINQ 查询优化策略对比
| 优化策略 | 用途说明 | 使用场景 |
|---|---|---|
| 延迟执行 | 提高性能,避免不必要的数据加载 | 多次使用同一查询条件 |
| LoadWith | 预加载关联数据,减少数据库访问次数 | 查询主表及关联表 |
| AsNoTracking | 不跟踪实体变化,节省资源 | 只读操作、报表展示 |
| 编译查询 | 将查询缓存以提高重复执行效率 | 高频调用的固定查询 |
本章通过理论与代码示例相结合的方式,系统讲解了LINQ的基础概念、集合数据的查询操作以及与数据库的交互实践。通过掌握这些内容,开发者可以更加高效地编写C#代码,实现对各类数据源的统一查询与操作,为后续的复杂业务逻辑打下坚实基础。
5. 委托与事件通信机制
在现代的 C# 编程中, 委托(Delegate) 和 事件(Event) 是构建可扩展、高内聚、低耦合应用程序的核心机制。它们不仅支撑了 .NET 框架的事件驱动模型,还广泛应用于 GUI 编程、异步操作、模块间通信等场景中。本章将从基础概念入手,深入探讨委托与事件的定义、调用机制以及实际应用场景。
5.1 委托的基本概念与使用
5.1.1 委托的定义与调用
委托本质上是一个 类型安全的函数指针 ,它可以封装一个或多个方法的引用,并允许在运行时动态调用这些方法。C# 中的委托使用 delegate 关键字定义,并具有严格的类型匹配规则,包括参数类型、返回值类型等。
示例:定义并调用一个委托
// 定义一个委托,表示一个接收两个整数并返回整数的方法
public delegate int CalculateDelegate(int a, int b);
class Program
{
// 方法1:加法
public static int Add(int a, int b)
{
return a + b;
}
// 方法2:乘法
public static int Multiply(int a, int b)
{
return a * b;
}
static void Main()
{
// 实例化委托,指向 Add 方法
CalculateDelegate calc = new CalculateDelegate(Add);
// 调用委托
int result = calc(3, 4);
Console.WriteLine("Add Result: " + result); // 输出 7
// 改变委托指向 Multiply 方法
calc = Multiply;
result = calc(3, 4);
Console.WriteLine("Multiply Result: " + result); // 输出 12
}
}
逻辑分析:
- 委托定义 :
CalculateDelegate是一个委托类型,它描述了一个接受两个int参数并返回int的方法。 - 委托实例化 :通过
new CalculateDelegate(Add)创建一个委托实例,指向Add方法。 - 调用方式 :委托实例可以像方法一样调用,例如
calc(3, 4)。 - 灵活性 :委托可以动态指向不同的方法,这使得其在事件、回调等机制中具有广泛应用。
5.1.2 多播委托与链式调用
多播委托(Multicast Delegate)允许一个委托实例绑定多个方法,形成一个 调用链(Invocation List) 。当调用该委托时,会按顺序依次执行所有绑定的方法。
示例:多播委托的使用
public delegate void NotifyDelegate(string message);
class Program
{
public static void SendEmail(string message)
{
Console.WriteLine("Sending Email: " + message);
}
public static void LogMessage(string message)
{
Console.WriteLine("Logging Message: " + message);
}
static void Main()
{
// 创建多播委托
NotifyDelegate notify = SendEmail;
notify += LogMessage;
// 触发调用链
notify("System is starting up.");
}
}
输出结果:
Sending Email: System is starting up.
Logging Message: System is starting up.
逻辑分析:
- += 运算符 :用于将多个方法绑定到同一个委托实例上。
- 调用顺序 :多播委托调用时,方法按照绑定的顺序依次执行。
- 注意事项 :
- 如果委托返回值类型不是void,则只能绑定一个方法。
- 可以使用-=运算符解除绑定。
表格:单播与多播委托对比
| 特性 | 单播委托 | 多播委托 |
|---|---|---|
| 方法绑定数量 | 1 | 多个 |
| 返回值类型 | 任意 | 通常为 void |
| 调用顺序 | 单一方法调用 | 多个方法按顺序调用 |
| 应用场景 | 简单回调机制 | 事件通知、日志记录等 |
5.2 事件机制详解
5.2.1 事件的定义与触发
事件(Event)是基于委托的一种封装机制,用于实现 发布-订阅模式(Publisher-Subscriber Pattern) 。事件的发布者不直接调用订阅者的方法,而是通知订阅者发生了某个动作。
示例:定义并触发事件
public class Button
{
// 定义事件
public event EventHandler Click;
// 触发事件的方法
public void OnClick()
{
Console.WriteLine("Button clicked.");
Click?.Invoke(this, EventArgs.Empty); // 使用空事件参数
}
}
class Program
{
static void Main()
{
Button btn = new Button();
// 订阅事件
btn.Click += Button_Click;
// 模拟点击
btn.OnClick();
}
static void Button_Click(object sender, EventArgs e)
{
Console.WriteLine("Event handled: Button was clicked.");
}
}
输出结果:
Button clicked.
Event handled: Button was clicked.
逻辑分析:
- 事件定义 :使用
event关键字声明一个EventHandler类型的事件。 - 事件触发 :在
OnClick方法中调用Click?.Invoke(...),其中?.是空条件运算符,防止空引用异常。 - 事件处理 :通过
+=操作符将方法绑定到事件上。
5.2.2 事件处理与回调函数
事件本质上是回调机制的高级封装。事件处理函数(如 Button_Click )通常遵循特定的签名格式: void MethodName(object sender, EventArgs e) 。
自定义 EventArgs 示例
public class FileEventArgs : EventArgs
{
public string FileName { get; set; }
public DateTime TimeStamp { get; set; }
}
public class FileProcessor
{
public event EventHandler<FileEventArgs> FileProcessed;
public void ProcessFile(string fileName)
{
Console.WriteLine($"Processing file: {fileName}");
OnFileProcessed(fileName);
}
protected virtual void OnFileProcessed(string fileName)
{
FileProcessed?.Invoke(this, new FileEventArgs
{
FileName = fileName,
TimeStamp = DateTime.Now
});
}
}
class Program
{
static void Main()
{
FileProcessor processor = new FileProcessor();
processor.FileProcessed += Processor_FileProcessed;
processor.ProcessFile("report.txt");
}
private static void Processor_FileProcessed(object sender, FileEventArgs e)
{
Console.WriteLine($"File '{e.FileName}' processed at {e.TimeStamp}");
}
}
输出结果:
Processing file: report.txt
File 'report.txt' processed at 2025/04/05 10:30:00
逻辑分析:
- 自定义 EventArgs :继承
EventArgs,添加自定义属性(如文件名、时间戳)。 - 泛型 EventHandler :使用
EventHandler<FileEventArgs>可以传递更丰富的信息。 - 事件生命周期 :
- 发布者定义事件;
- 订阅者绑定处理方法;
- 事件被触发时自动调用所有订阅的方法。
5.3 委托与事件的实际应用
5.3.1 在 GUI 编程中的事件响应机制
在 Windows Forms 或 WPF 等图形界面开发中,用户操作(如按钮点击、文本框输入等)都是通过事件机制来响应的。这种机制将 UI 逻辑与业务逻辑解耦,提高了代码的可维护性。
WPF 示例:按钮点击事件绑定
<!-- XAML 定义 -->
<Button Content="Click Me" Click="Button_Click"/>
// C# 后台代码
private void Button_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("Button clicked in WPF!");
}
分析:
- 事件绑定 :在 XAML 中通过
Click="Button_Click"绑定事件处理方法。 - 运行时机制 :点击按钮时,WPF 框架自动调用
Button_Click方法。 - 优势 :
- UI 与逻辑分离;
- 易于扩展与维护;
- 支持多个订阅者。
5.3.2 使用事件实现模块间通信
在大型系统中,不同模块之间往往需要通信。事件机制非常适合用于模块解耦,例如主窗口监听子窗口关闭事件、数据更新通知等。
示例:模块间通信(观察者模式)
public class DataProvider
{
public event EventHandler DataUpdated;
public void UpdateData()
{
Console.WriteLine("Data updated internally.");
OnDataUpdated();
}
protected virtual void OnDataUpdated()
{
DataUpdated?.Invoke(this, EventArgs.Empty);
}
}
public class DataConsumer
{
public void Subscribe(DataProvider provider)
{
provider.DataUpdated += HandleDataUpdate;
}
private void HandleDataUpdate(object sender, EventArgs e)
{
Console.WriteLine("DataConsumer: Received data update notification.");
}
}
class Program
{
static void Main()
{
DataProvider provider = new DataProvider();
DataConsumer consumer = new DataConsumer();
consumer.Subscribe(provider);
provider.UpdateData();
}
}
输出结果:
Data updated internally.
DataConsumer: Received data update notification.
分析:
- 发布-订阅模式 :
-DataProvider作为发布者;
-DataConsumer作为订阅者;
- 当数据更新时,所有订阅者都会收到通知。 - 模块解耦 :
- 消费者不需要知道数据更新的具体实现;
- 提高了系统的可扩展性与灵活性。 - 应用场景 :
- MVVM 架构中的 ViewModel 通信;
- 模块化系统中各组件的状态同步;
- 日志、监控、通知系统等。
总结
本章深入探讨了 C# 中委托与事件的定义、调用机制及其实际应用。通过本章的学习,我们掌握了以下核心知识点:
- 委托是一种类型安全的函数指针,支持单播与多播调用;
- 事件基于委托,用于实现发布-订阅机制;
- 委托与事件广泛应用于 GUI 编程、模块通信、异步通知等场景;
- 使用泛型
EventHandler<T>可以传递丰富信息; - 事件机制有助于构建松耦合、可扩展的系统架构。
在下一章中,我们将进入 C# 的异步编程世界,深入理解 async/await 如何优化程序性能与响应能力。
6. 异步编程(async/await实现)
异步编程是现代软件开发中不可或缺的一部分,尤其是在处理高并发、网络请求、UI响应等场景时,异步机制能够显著提升程序性能和用户体验。在 C# 中, async/await 关键字的引入极大地简化了异步代码的编写与维护,使得异步逻辑更加清晰、易于理解。本章将从异步编程的基础概念入手,逐步深入讲解 async/await 的实现方式,并通过实际案例探讨其最佳实践。
6.1 异步编程基础
6.1.1 同步与异步的区别
在传统的同步编程模型中,程序按照顺序执行每一条语句,当前线程会一直等待某项操作完成,例如文件读取、网络请求等。这种模式在单线程环境中可能导致界面卡顿、资源浪费等问题。
异步编程则通过非阻塞方式执行耗时操作,主线程可以继续执行其他任务,避免程序“冻结”。在 C# 中,异步编程主要依赖于 Task 和 async/await 语法。
| 对比维度 | 同步编程 | 异步编程 |
|---|---|---|
| 线程使用 | 单线程阻塞 | 多线程/线程池异步执行 |
| 用户体验 | 容易卡顿 | 响应流畅 |
| 资源利用 | 效率低 | 高效利用 |
| 编程复杂度 | 简单 | 需要理解异步模型 |
6.1.2 async/await关键字的作用
async:用于标记一个方法为异步方法,方法返回类型可以是Task、Task<T>或void(通常用于事件处理)。await:用于等待一个异步操作完成,不会阻塞当前线程,而是将控制权交还给调用方,直到异步操作完成后再继续执行。
public async Task<int> FetchDataAsync()
{
HttpClient client = new HttpClient();
string result = await client.GetStringAsync("https://example.com"); // 非阻塞等待
return result.Length;
}
逐行解析:
- 第 1 行:async Task<int>表示这是一个异步方法,返回一个Task<int>对象。
- 第 3 行:创建HttpClient实例,用于发送 HTTP 请求。
- 第 4 行:使用GetStringAsync异步获取网页内容,await关键字确保程序不会阻塞主线程。
- 第 5 行:返回结果长度,该值将封装在Task<int>中返回。
6.2 异步方法的实现方式
6.2.1 使用Task类进行异步操作
Task 类是 .NET 中异步编程的核心类之一,它封装了异步操作的状态、结果和异常等信息。我们可以使用 Task.Run 启动后台任务,也可以使用 ContinueWith 来定义后续操作。
public void RunBackgroundTask()
{
Task<int> task = Task.Run(() =>
{
Thread.Sleep(2000); // 模拟耗时操作
return 42;
});
task.ContinueWith(t =>
{
Console.WriteLine($"后台任务完成,结果为:{t.Result}");
}, TaskScheduler.FromCurrentSynchronizationContext());
}
逐行解析:
- 第 2 行:使用Task.Run启动一个后台任务。
- 第 3–5 行:模拟一个耗时 2 秒的操作,返回整数 42。
- 第 7–10 行:使用ContinueWith在任务完成后执行后续操作,输出结果。
-TaskScheduler.FromCurrentSynchronizationContext()确保 UI 线程安全更新。
6.2.2 await异步等待与异常处理
异步方法中也可能抛出异常,使用 try-catch 捕获异常是推荐的做法。 await 会将异常封装在 Task 中,需在 catch 块中处理。
public async Task HandleExceptionAsync()
{
try
{
string result = await GetStringFromUrlAsync("https://invalid-url.com");
Console.WriteLine(result);
}
catch (HttpRequestException ex)
{
Console.WriteLine($"网络请求失败:{ex.Message}");
}
}
private async Task<string> GetStringFromUrlAsync(string url)
{
using (HttpClient client = new HttpClient())
{
return await client.GetStringAsync(url);
}
}
逐行解析:
- 第 3–7 行:尝试获取网页内容,若失败则进入catch处理。
- 第 11–15 行:封装了一个异步方法,使用HttpClient获取字符串。
-using确保HttpClient正确释放资源。
- 异常被捕获后,输出错误信息,避免程序崩溃。
6.3 异步编程最佳实践
6.3.1 避免死锁与线程阻塞
在 UI 编程中,使用 .Result 或 .Wait() 强制等待异步任务完成,容易造成死锁。这是因为 await 默认会尝试回到原始上下文(如 UI 线程),而主线程正在等待任务完成,形成死锁。
示例:错误的使用方式
var result = GetStringFromUrlAsync("https://example.com").Result; // 可能死锁
正确做法:始终使用 await
var result = await GetStringFromUrlAsync("https://example.com"); // 推荐
避免死锁的建议:
| 建议 | 说明 |
|---|---|
避免在 UI 线程中调用 .Result |
会导致 UI 线程阻塞,可能引发死锁 |
不要混用 async/await 与 .Wait() |
容易破坏异步流 |
使用 ConfigureAwait(false) |
避免强制回到原始上下文 |
string result = await client.GetStringAsync(url).ConfigureAwait(false);
ConfigureAwait(false)表示任务完成后不需要回到原始上下文执行后续代码,适用于后台任务处理。
6.3.2 UI线程与后台线程的协调处理
在 WPF、WinForms 等 UI 应用中,UI 线程负责界面更新。异步操作完成后,如果需要更新 UI,必须回到 UI 线程执行。
private async void btnFetch_Click(object sender, EventArgs e)
{
string content = await FetchDataAsync();
txtResult.Text = content; // 自动回到 UI 线程
}
private async Task<string> FetchDataAsync()
{
using (HttpClient client = new HttpClient())
{
return await client.GetStringAsync("https://example.com");
}
}
逻辑说明:
-btnFetch_Click是一个 UI 事件处理函数。
- 异步获取网页内容后,txtResult.Text = content会自动回到 UI 线程更新控件。
- 若需手动控制线程,可使用Invoke或Dispatcher。
总结与延伸
本章系统讲解了 C# 中的异步编程机制,重点围绕 async/await 的使用展开。通过理解同步与异步的区别、掌握 Task 类与异常处理方式,并结合 UI 线程管理的最佳实践,开发者可以写出高效、稳定、响应良好的应用程序。
在后续章节中,我们将继续探讨 C# 中的泛型编程,深入理解如何通过泛型提高代码复用性与类型安全性,敬请期待。
7. 泛型编程与类型安全机制
7.1 泛型的基本概念
7.1.1 泛型类与泛型方法
在C#中, 泛型(Generics) 是一种允许我们在定义类、接口或方法时使用“类型参数”的机制。这意味着我们可以编写与具体数据类型无关的代码,从而实现更高的灵活性和代码复用性。
泛型类示例:
public class GenericList<T>
{
private List<T> items = new List<T>();
public void Add(T item)
{
items.Add(item);
}
public T Get(int index)
{
return items[index];
}
}
- 代码解释 :
T是一个类型参数,表示该类可以接受任何类型。Add方法接受一个类型为T的参数,将其添加到列表中。Get方法返回指定索引位置的T类型值。
泛型方法示例:
public class Utility
{
public static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
}
- 调用方式 :
int x = 10, y = 20;
Utility.Swap(ref x, ref y); // x becomes 20, y becomes 10
- 优点 :
- 避免了类型转换和装箱拆箱操作。
- 提高了代码的可读性和安全性。
7.1.2 泛型约束(where子句)
为了确保泛型代码中对类型参数的操作是安全的,C# 提供了 泛型约束(where 子句) 。它允许我们限制类型参数的种类。
常见的泛型约束包括:
| 约束 | 说明 |
|---|---|
where T : class |
类型参数必须是引用类型 |
where T : struct |
类型参数必须是值类型 |
where T : new() |
类型必须有一个无参数的构造函数 |
where T : <base class> |
类型必须是指定的基类或其派生类 |
where T : <interface> |
类型必须实现指定接口 |
带约束的泛型类示例:
public class Repository<T> where T : class, new()
{
public T CreateInstance()
{
return new T(); // 由于 new() 约束,这里可以安全调用构造函数
}
}
- 说明 :
- 此类只能接受引用类型(class)并具有无参构造函数(new)的类型。
- 保证了在
CreateInstance方法中能够安全地创建新实例。
7.2 泛型集合的使用
7.2.1 List 与Dictionary 的操作
C# 提供了丰富的泛型集合类,如 List<T> 和 Dictionary<TKey, TValue> ,它们相较于非泛型集合(如 ArrayList 、 Hashtable )具有更高的性能和类型安全性。
List 示例:
List<string> names = new List<string>();
names.Add("Alice");
names.Add("Bob");
foreach (string name in names)
{
Console.WriteLine(name);
}
- 说明 :
- 只能添加
string类型数据。 - 遍历时无需进行类型转换。
Dictionary 示例:
Dictionary<int, string> employees = new Dictionary<int, string>();
employees.Add(101, "John");
employees.Add(102, "Doe");
foreach (KeyValuePair<int, string> kvp in employees)
{
Console.WriteLine($"ID: {kvp.Key}, Name: {kvp.Value}");
}
- 说明 :
- 键值对的类型在定义时已确定。
- 提高了查找效率并避免类型错误。
7.2.2 泛型集合与非泛型集合的性能对比
| 集合类型 | 类型安全 | 性能(值类型) | 性能(引用类型) | 是否需要类型转换 |
|---|---|---|---|---|
List<T> |
是 | 高(无装箱拆箱) | 高 | 否 |
ArrayList |
否 | 低(频繁装箱拆箱) | 低 | 是 |
Dictionary<TKey, TValue> |
是 | 高 | 高 | 否 |
Hashtable |
否 | 低 | 低 | 是 |
- 结论 :
- 使用泛型集合可以显著提高程序的性能和安全性,特别是在处理值类型时,避免了装箱拆箱带来的开销。
7.3 泛型在实际项目中的应用
7.3.1 提高代码复用性
泛型允许我们编写一次代码,适用于多种类型,从而避免了重复代码的编写。
示例:通用数据访问层
public class Repository<T> where T : class
{
public void Save(T entity)
{
// 模拟保存逻辑
Console.WriteLine($"Saving {typeof(T).Name}");
}
public T GetById(int id)
{
// 模拟获取逻辑
Console.WriteLine($"Getting {typeof(T).Name} by ID: {id}");
return default(T);
}
}
- 使用方式 :
Repository<User> userRepository = new Repository<User>();
userRepository.Save(new User { Id = 1, Name = "Tom" });
Repository<Product> productRepository = new Repository<Product>();
Product product = productRepository.GetById(1001);
- 优势 :
- 适用于多种实体类型,无需为每个实体编写单独的类。
7.3.2 保证类型安全与编译时检查
泛型的另一个关键优势是 编译时类型检查 ,它能在编译阶段就发现类型不匹配的问题,而不是在运行时才报错。
对比示例:
非泛型(ArrayList) :
ArrayList list = new ArrayList();
list.Add("Hello");
list.Add(123); // 允许添加int类型,但运行时容易出错
string s = (string)list[1]; // 抛出异常:无法将int转换为string
泛型(List ) :
List<string> list = new List<string>();
list.Add("Hello");
// list.Add(123); // 编译错误:不能将int隐式转换为string
- 说明 :
- 泛型在编译阶段就限制了类型,提升了代码的健壮性和可维护性。
注:下一章节将继续深入泛型与反射、泛型与接口设计、以及高级泛型模式等内容,进一步探讨泛型在复杂项目架构中的实际应用。
简介:C#是由微软开发的面向对象编程语言,广泛应用于Windows平台、.NET框架、Unity游戏开发以及桌面和Web应用开发。本资源“程序集C#”是一个包含多个实践示例的集合,适合初学者和进阶开发者学习和使用。内容涵盖C#基础语法、面向对象编程、异常处理、LINQ、委托与事件、异步编程、泛型、集合操作、文件流处理、网络编程及单元测试等核心技术。通过实例操作,开发者可全面提升C#编程能力,掌握实际项目开发所需技能。
更多推荐


所有评论(0)