本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介: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 或其子类。

步骤如下:

  1. 定义一个类,继承自 Exception
  2. 提供构造函数,支持传递消息和内部异常;
  3. 可选:添加自定义属性以提供更多上下文信息。
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[在代码中使用实体类进行查询]
查询优化技巧:
  1. 延迟执行(Deferred Execution)
    - LINQ 查询默认是延迟执行的,直到遍历结果集(如 foreach ToList() )才会真正执行。
    - 示例:

    csharp var query = from c in db.Customers where c.Country == "USA"; // 此时查询未执行 var list = query.ToList(); // 执行查询

  2. 避免 N+1 查询问题
    - 使用 DataLoadOptions 预加载关联表数据。

    csharp var options = new DataLoadOptions(); options.LoadWith<Customer>(c => c.Orders); db.LoadOptions = options;

  3. 使用 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
    }
}
逻辑分析:
  1. 委托定义 CalculateDelegate 是一个委托类型,它描述了一个接受两个 int 参数并返回 int 的方法。
  2. 委托实例化 :通过 new CalculateDelegate(Add) 创建一个委托实例,指向 Add 方法。
  3. 调用方式 :委托实例可以像方法一样调用,例如 calc(3, 4)
  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.
逻辑分析:
  1. += 运算符 :用于将多个方法绑定到同一个委托实例上。
  2. 调用顺序 :多播委托调用时,方法按照绑定的顺序依次执行。
  3. 注意事项
    - 如果委托返回值类型不是 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.
逻辑分析:
  1. 事件定义 :使用 event 关键字声明一个 EventHandler 类型的事件。
  2. 事件触发 :在 OnClick 方法中调用 Click?.Invoke(...) ,其中 ?. 是空条件运算符,防止空引用异常。
  3. 事件处理 :通过 += 操作符将方法绑定到事件上。

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
逻辑分析:
  1. 自定义 EventArgs :继承 EventArgs ,添加自定义属性(如文件名、时间戳)。
  2. 泛型 EventHandler :使用 EventHandler<FileEventArgs> 可以传递更丰富的信息。
  3. 事件生命周期
    - 发布者定义事件;
    - 订阅者绑定处理方法;
    - 事件被触发时自动调用所有订阅的方法。

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!");
}
分析:
  1. 事件绑定 :在 XAML 中通过 Click="Button_Click" 绑定事件处理方法。
  2. 运行时机制 :点击按钮时,WPF 框架自动调用 Button_Click 方法。
  3. 优势
    - 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.
分析:
  1. 发布-订阅模式
    - DataProvider 作为发布者;
    - DataConsumer 作为订阅者;
    - 当数据更新时,所有订阅者都会收到通知。
  2. 模块解耦
    - 消费者不需要知道数据更新的具体实现;
    - 提高了系统的可扩展性与灵活性。
  3. 应用场景
    - 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
  • 说明
  • 泛型在编译阶段就限制了类型,提升了代码的健壮性和可维护性。

注:下一章节将继续深入泛型与反射、泛型与接口设计、以及高级泛型模式等内容,进一步探讨泛型在复杂项目架构中的实际应用。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:C#是由微软开发的面向对象编程语言,广泛应用于Windows平台、.NET框架、Unity游戏开发以及桌面和Web应用开发。本资源“程序集C#”是一个包含多个实践示例的集合,适合初学者和进阶开发者学习和使用。内容涵盖C#基础语法、面向对象编程、异常处理、LINQ、委托与事件、异步编程、泛型、集合操作、文件流处理、网络编程及单元测试等核心技术。通过实例操作,开发者可全面提升C#编程能力,掌握实际项目开发所需技能。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

更多推荐