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

简介:本课件是一份系统化的C#编程语言教学资料,专为帮助学习者掌握C#基础语法与高级特性而设计。通过清晰的幻灯片形式,详细讲解了变量、控制流、面向对象编程、泛型、异常处理、LINQ、异步编程等核心知识点,并涵盖Windows Forms、WPF、ASP.NET等实际应用场景。适合初学者与进阶开发者学习和实践,结合实例提升编程能力,为开发桌面、Web及移动应用打下坚实基础。

1. C#基础语法讲解与练习

本章将带领读者从零开始掌握C#语言的基本语法结构,包括变量定义、数据类型、运算符、流程控制语句以及基本的输入输出操作。通过丰富的代码示例和练习,帮助初学者建立起对C#编程语言的整体认知,为后续深入学习打下坚实基础。

1.1 变量与数据类型

C#是一种静态类型语言,变量在使用前必须声明其类型。常见的数据类型包括整型(int)、浮点型(float、double)、布尔型(bool)和字符串(string)等。

int age = 25;               // 整型变量
double salary = 5000.50;    // 双精度浮点型
bool isEmployed = true;     // 布尔类型
string name = "John Doe";   // 字符串类型

变量声明语法为: 数据类型 变量名 = 值; 。C#会根据赋值自动进行类型推断(使用 var 关键字):

var count = 100;            // 编译器推断为int类型
var message = "Hello";      // 推断为string类型

2. 面向对象编程(OOP)详解

面向对象编程(Object-Oriented Programming,简称OOP)是现代软件开发的核心范式之一,它通过类与对象的机制,帮助开发者构建结构清晰、模块化强、可维护性高的程序。C#作为一门完全面向对象的语言,全面支持OOP的三大核心特性: 封装 继承 多态 。本章将深入探讨这些概念在C#中的实现方式,并结合代码示例与设计模式,帮助读者掌握如何构建高质量的面向对象程序。

2.1 面向对象的基本概念

面向对象编程的核心在于将现实世界抽象为程序中的对象,这些对象通过定义其状态(属性)和行为(方法)来描述其特征。理解OOP的基本概念是掌握其精髓的前提。

2.1.1 对象与类的区别

对象(Object) 是类(Class)的一个实例。类是对象的模板,定义了对象的属性和行为;而对象则是类的具体表现。

// 定义一个类
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    public void SayHello()
    {
        Console.WriteLine($"Hello, my name is {Name}, and I am {Age} years old.");
    }
}

// 创建对象
Person person1 = new Person();
person1.Name = "Alice";
person1.Age = 25;
person1.SayHello();

逻辑分析:

  • Person 是一个类,它定义了两个属性( Name Age )以及一个方法( SayHello )。
  • person1 Person 类的一个实例,即一个对象。
  • 类是抽象的模板,对象是具体的实例。多个对象可以基于同一个类创建,但每个对象拥有独立的状态。

2.1.2 封装的概念与实现方式

封装(Encapsulation) 是将对象的内部状态和行为隐藏起来,只对外暴露必要的接口。这样可以防止外部直接修改对象的状态,提高数据的安全性。

public class BankAccount
{
    private decimal balance;

    public void Deposit(decimal amount)
    {
        if (amount > 0)
            balance += amount;
    }

    public void Withdraw(decimal amount)
    {
        if (amount > 0 && amount <= balance)
            balance -= amount;
    }

    public decimal GetBalance()
    {
        return balance;
    }
}

逻辑分析:

  • balance 是私有字段,外部无法直接访问。
  • 提供了 Deposit() Withdraw() 方法用于修改余额,确保了操作的合法性。
  • 使用 GetBalance() 获取当前余额,避免外部直接读取字段。
成员类型 访问权限 作用
public 公有 任何位置可访问
private 私有 仅类内部可访问
protected 受保护 类及其派生类可访问
internal 内部 同一程序集可访问

2.1.3 OOP的设计优势

面向对象编程相比传统的过程式编程,具有以下显著优势:

优势 描述
模块化 将系统划分为多个独立模块(类),提高代码复用性
可维护性 代码结构清晰,易于扩展与维护
安全性 通过封装保护数据,避免非法访问
可扩展性 通过继承与多态实现灵活的结构扩展

2.2 C#中的类与对象

类是C#中构建对象的核心结构,它不仅包含字段、属性、方法,还支持构造函数、析构函数、静态成员等高级特性。

2.2.1 类的定义与成员变量

类的定义包括类名、访问修饰符、成员变量和方法。类的成员变量可以是字段、属性或常量。

public class Rectangle
{
    // 字段
    private int width;
    private int height;

    // 属性
    public int Width
    {
        get { return width; }
        set { if (value > 0) width = value; }
    }

    public int Height
    {
        get { return height; }
        set { if (value > 0) height = value; }
    }

    // 方法
    public int GetArea()
    {
        return width * height;
    }
}

逻辑分析:

  • width height 是私有字段,用于存储矩形的尺寸。
  • Width Height 是公开属性,提供安全的访问方式。
  • GetArea() 方法用于计算矩形的面积。

2.2.2 构造函数与析构函数

构造函数用于在创建对象时初始化成员变量,析构函数则用于释放资源。

public class Student
{
    public string Name { get; set; }
    public int Age { get; set; }

    // 构造函数
    public Student(string name, int age)
    {
        Name = name;
        Age = age;
    }

    // 析构函数
    ~Student()
    {
        Console.WriteLine($"{Name} 的对象已被销毁。");
    }
}

// 使用构造函数创建对象
Student student = new Student("Bob", 20);

逻辑分析:

  • 构造函数 Student(string name, int age) 在对象创建时自动调用,用于初始化属性。
  • 析构函数 ~Student() 在对象被垃圾回收器回收前调用,通常用于释放非托管资源。

2.2.3 静态类与静态方法

静态类和静态成员属于类本身,而不是类的实例,适用于工具类、辅助函数等场景。

public static class MathUtils
{
    public static int Add(int a, int b)
    {
        return a + b;
    }

    public static int Multiply(int a, int b)
    {
        return a * b;
    }
}

// 调用静态方法
int result = MathUtils.Add(5, 3);
Console.WriteLine(result);  // 输出:8

逻辑分析:

  • MathUtils 是一个静态类,不能实例化。
  • Add() Multiply() 是静态方法,可以直接通过类名调用。
  • 静态类通常用于封装与状态无关的通用方法。

2.3 继承与多态的实现

继承与多态是OOP的两大核心机制,它们共同构成了程序的可扩展性与灵活性。

2.3.1 基类与派生类的定义

继承允许一个类(派生类)从另一个类(基类)继承其成员,从而实现代码复用。

classDiagram
    class Animal {
        +string Name
        +void MakeSound()
    }
    class Dog {
        +void Bark()
    }
    class Cat {
        +void Meow()
    }
    Animal <|-- Dog
    Animal <|-- Cat
public class Animal
{
    public string Name { get; set; }

    public virtual void MakeSound()
    {
        Console.WriteLine("Animal sound");
    }
}

public class Dog : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("Woof!");
    }
}

逻辑分析:

  • Animal 是基类,定义了通用属性和方法。
  • Dog 是派生类,继承自 Animal 并重写其 MakeSound() 方法。
  • 使用 virtual override 关键字实现方法的多态行为。

2.3.2 方法重写与虚方法

方法重写是指在派生类中重新定义基类的方法,实现不同的行为。

public class Shape
{
    public virtual void Draw()
    {
        Console.WriteLine("Drawing a shape");
    }
}

public class Circle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a circle");
    }
}

// 多态调用
Shape shape = new Circle();
shape.Draw();  // 输出:Drawing a circle

逻辑分析:

  • Draw() 在基类中被标记为 virtual ,表示可以被派生类重写。
  • Circle 类使用 override 关键字重新定义了 Draw() 方法。
  • 通过基类引用调用派生类的方法,体现了多态的特性。

2.3.3 多态的应用场景与优势

多态使得程序可以在运行时根据对象的实际类型决定调用哪个方法,从而提高程序的灵活性和可扩展性。

场景 描述
插件系统 通过接口或基类统一调用不同插件
图形系统 统一绘制不同形状对象
事件处理 不同对象对同一事件做出不同响应

2.4 接口与抽象类

接口与抽象类是实现抽象化的重要手段,它们为类定义行为规范,但不提供具体实现。

2.4.1 接口的定义与实现

接口是一种契约,定义了类必须实现的方法和属性。

public interface IAnimal
{
    void MakeSound();
    string GetName();
}

public class Cat : IAnimal
{
    public string Name { get; set; }

    public void MakeSound()
    {
        Console.WriteLine("Meow!");
    }

    public string GetName()
    {
        return Name;
    }
}

逻辑分析:

  • IAnimal 接口定义了两个方法: MakeSound() GetName()
  • Cat 类实现了 IAnimal 接口,并提供具体实现。
  • 接口支持多继承,一个类可以实现多个接口。

2.4.2 抽象类与接口的比较

特性 抽象类 接口
构造函数 支持 不支持
实现 可以有具体实现 不能有实现
成员访问权限 支持访问修饰符 默认 public
多继承 不支持 支持
状态 可以有字段和状态 只能定义行为
public abstract class Vehicle
{
    public abstract void StartEngine();

    public void StopEngine()
    {
        Console.WriteLine("Engine stopped.");
    }
}

public class Car : Vehicle
{
    public override void StartEngine()
    {
        Console.WriteLine("Car engine started.");
    }
}

逻辑分析:

  • Vehicle 是一个抽象类,包含一个抽象方法 StartEngine() 和一个具体方法 StopEngine()
  • Car 类继承自 Vehicle ,并实现抽象方法。
  • 抽象类适合定义具有部分实现的公共行为。

3. 类、对象、继承与多态实现

本章进一步深入讲解类与对象在实际开发中的应用,重点介绍继承与多态的高级实现技巧,涵盖抽象类、接口、访问修饰符、继承链中的构造函数调用等内容,提升读者对面向对象设计的理解和运用能力。

3.1 类的成员详解

在C#中,类不仅仅是一个数据结构的容器,更是面向对象编程的核心。类的成员包括字段、属性、方法、索引器、事件、委托等。本节将重点讲解属性与索引器的设计与使用、运算符重载的实现机制,以及事件与委托的基础应用场景。

3.1.1 属性与索引器

属性(Property)是封装字段访问逻辑的一种方式,它通过 get set 访问器提供对类内部数据的安全访问。索引器(Indexer)则允许类像数组一样通过索引进行访问。

属性示例
public class Person
{
    private string name;

    public string Name
    {
        get { return name; }
        set
        {
            if (string.IsNullOrEmpty(value))
                throw new ArgumentException("Name cannot be null or empty.");
            name = value;
        }
    }
}

逐行分析:

  • private string name; :定义一个私有字段,用于存储实际的名称值。
  • public string Name :定义公共属性,对外暴露。
  • get { return name; } :读取属性时调用,返回字段值。
  • set { ... } :设置属性值时调用,包含验证逻辑,确保名称不为空。
索引器示例
public class StringList
{
    private string[] items = new string[10];

    public string this[int index]
    {
        get
        {
            if (index < 0 || index >= items.Length)
                throw new IndexOutOfRangeException();
            return items[index];
        }
        set
        {
            if (index < 0 || index >= items.Length)
                throw new IndexOutOfRangeException();
            items[index] = value;
        }
    }
}

逐行分析:

  • public string this[int index] :定义索引器,允许使用 [] 操作符。
  • get set 中包含边界检查,避免数组越界异常。
  • 通过 this 关键字,类实例可以像数组一样被访问。
属性与索引器对比
特性 属性 索引器
命名方式 使用明确的名称 使用 this 关键字
访问方式 通过属性名访问 通过索引访问
参数数量 无参数 至少一个参数
是否可重载 不可重载 可重载
应用场景 数据封装与验证 集合类的访问封装

3.1.2 运算符重载

运算符重载(Operator Overloading)允许我们为类定义自定义的运算符行为,例如 + == != 等。C#中通过 operator 关键字实现。

示例:重载加法运算符
public class Complex
{
    public double Real { get; set; }
    public double Imaginary { get; set; }

    public Complex(double real, double imaginary)
    {
        Real = real;
        Imaginary = imaginary;
    }

    public static Complex operator +(Complex c1, Complex c2)
    {
        return new Complex(c1.Real + c2.Real, c1.Imaginary + c2.Imaginary);
    }
}

逐行分析:

  • public static Complex operator +(Complex c1, Complex c2) :定义加法运算符的重载方法。
  • 该方法必须是 static 并以 operator 开头,参数为两个操作数。
  • 返回一个新的 Complex 实例,表示两个复数相加的结果。
常见可重载运算符
运算符 说明 是否可重载
+ 加法
- 减法
== 等于
!= 不等于
++ 自增
[] 索引访问 ❌(使用索引器)
= 赋值
注意事项:
  • 必须成对重载相等运算符(如 == != )。
  • 运算符重载应保持语义清晰,避免引起歧义。

3.1.3 事件与委托的初步应用

委托(Delegate)是类型安全的函数指针,而事件(Event)是基于委托的封装,用于实现观察者模式。

定义委托与事件
public delegate void NotifyEventHandler(string message);

public class Publisher
{
    public event NotifyEventHandler Notify;

    public void DoSomething()
    {
        Console.WriteLine("Doing something...");
        OnNotify("Operation completed.");
    }

    protected virtual void OnNotify(string message)
    {
        Notify?.Invoke(message);
    }
}

public class Subscriber
{
    public void HandleNotification(string message)
    {
        Console.WriteLine($"Received: {message}");
    }
}

逐行分析:

  • public delegate void NotifyEventHandler(string message); :定义一个委托类型,用于事件通知。
  • public event NotifyEventHandler Notify; :定义一个事件,只能通过 += -= 来订阅或取消订阅。
  • OnNotify 方法调用事件,并使用 ?.Invoke 安全地处理空引用。
  • HandleNotification 是事件处理函数,由订阅者注册。
使用示例
var publisher = new Publisher();
var subscriber = new Subscriber();

publisher.Notify += subscriber.HandleNotification;
publisher.DoSomething();

输出:

Doing something...
Received: Operation completed.
事件与委托关系图(mermaid流程图)
graph LR
    A[Publisher] -- Notify事件 --> B(Subscriber)
    B -- HandleNotification --> A

3.2 对象生命周期管理

在C#中,对象的生命周期由CLR(Common Language Runtime)管理,但开发者仍需理解内存分配、垃圾回收以及资源释放的机制,以避免内存泄漏等问题。

3.2.1 内存分配与垃圾回收机制

C#使用自动垃圾回收机制(Garbage Collection),CLR负责内存的分配与释放。对象在堆(Heap)上分配,当对象不再被引用时,GC会自动回收其占用的内存。

垃圾回收流程图
graph TD
    A[对象创建] --> B[内存分配]
    B --> C[对象使用]
    C --> D{是否引用}
    D -- 是 --> C
    D -- 否 --> E[标记为垃圾]
    E --> F[GC回收]
关键点:
  • GC分为三代(Generation 0, 1, 2),频繁回收的是第0代。
  • 使用 using 语句可显式释放资源。
  • 对大对象(Large Object Heap)回收效率较低,需谨慎使用。

3.2.2 IDisposable接口与资源释放

对于非托管资源(如文件句柄、数据库连接),必须显式释放。C#提供了 IDisposable 接口和 using 语句来帮助资源释放。

实现IDisposable接口
public class ResourceHolder : IDisposable
{
    private bool disposed = false;

    ~ResourceHolder()
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                // 释放托管资源
            }
            // 释放非托管资源
            disposed = true;
        }
    }
}

逐行分析:

  • Dispose() 方法由用户显式调用。
  • ~ResourceHolder() 是析构函数,作为最终保障。
  • GC.SuppressFinalize(this) 防止重复释放。
  • Dispose(bool disposing) 是模板方法,区分托管与非托管资源释放。
使用using语句
using (var resource = new ResourceHolder())
{
    // 使用资源
} // 自动调用 Dispose()

3.3 继承关系的深入分析

继承是面向对象编程的核心机制之一,但在实际开发中需要深入理解其特性与限制,包括多重继承的替代方案、 sealed 关键字的用途,以及继承与组合的选择。

3.3.1 多重继承的实现与限制

C#不支持类的多重继承,但可以通过接口实现类似功能。

接口模拟多重继承
public interface ILoggable
{
    void Log();
}

public interface ISerializable
{
    void Serialize();
}

public class MyClass : ILoggable, ISerializable
{
    public void Log() => Console.WriteLine("Logging...");
    public void Serialize() => Console.WriteLine("Serializing...");
}

分析:

  • MyClass 同时实现了 ILoggable ISerializable 接口,模拟了多重继承的效果。
  • 接口提供方法定义,实现类负责具体逻辑。

3.3.2 sealed关键字的作用

sealed 关键字可以阻止类被继承,或阻止方法被重写。

密封类示例
public sealed class FinalClass
{
    public void DoSomething()
    {
        Console.WriteLine("Can't be overridden.");
    }
}
密封方法示例
public class BaseClass
{
    public virtual void Show()
    {
        Console.WriteLine("Base class method");
    }
}

public class DerivedClass : BaseClass
{
    public sealed override void Show()
    {
        Console.WriteLine("Sealed override");
    }
}

说明:

  • sealed override 表示该方法不能在派生类中再次重写。

3.3.3 继承与组合的选择

特性 继承 组合
代码复用方式 父类继承 对象包含
灵活性 有限(仅继承一个类) 高(可组合多个对象)
耦合度
维护性 修改父类影响所有子类 修改组件不影响整体结构
适用场景 层次结构明确 动态组合行为
组合示例
public class Engine
{
    public void Start() => Console.WriteLine("Engine started.");
}

public class Car
{
    private Engine engine;

    public Car(Engine engine)
    {
        this.engine = engine;
    }

    public void Start()
    {
        engine.Start();
        Console.WriteLine("Car started.");
    }
}

3.4 多态的高级实践

多态是面向对象编程的三大支柱之一,它允许对象在运行时根据实际类型执行不同的行为。本节将介绍多态在设计模式中的应用,以及如何使用多态构建插件式架构。

3.4.1 多态在设计模式中的应用

多态是策略模式、工厂模式、模板方法等设计模式的基础。

策略模式示例
public interface IPaymentStrategy
{
    void Pay(double amount);
}

public class CreditCardPayment : IPaymentStrategy
{
    public void Pay(double amount)
    {
        Console.WriteLine($"Paid {amount} via Credit Card.");
    }
}

public class PayPalPayment : IPaymentStrategy
{
    public void Pay(double amount)
    {
        Console.WriteLine($"Paid {amount} via PayPal.");
    }
}

public class ShoppingCart
{
    private IPaymentStrategy strategy;

    public void SetPaymentStrategy(IPaymentStrategy strategy)
    {
        this.strategy = strategy;
    }

    public void Checkout(double amount)
    {
        strategy.Pay(amount);
    }
}

使用方式:

var cart = new ShoppingCart();
cart.SetPaymentStrategy(new CreditCardPayment());
cart.Checkout(100.0);

3.4.2 使用多态实现插件式架构

插件式架构允许应用程序在运行时动态加载功能模块。多态为此提供了基础支持。

插件接口定义
public interface IPlugin
{
    string Name { get; }
    void Execute();
}
插件实现示例
public class LoggerPlugin : IPlugin
{
    public string Name => "Logger";

    public void Execute()
    {
        Console.WriteLine("Logging plugin executed.");
    }
}
主程序动态加载插件
public class PluginLoader
{
    public static void LoadAndExecute(string pluginPath)
    {
        var assembly = Assembly.LoadFrom(pluginPath);
        foreach (var type in assembly.GetTypes())
        {
            if (typeof(IPlugin).IsAssignableFrom(type))
            {
                var plugin = (IPlugin)Activator.CreateInstance(type);
                plugin.Execute();
            }
        }
    }
}

说明:

  • 使用反射加载插件DLL。
  • 通过接口 IPlugin 实现多态调用。
  • 插件可在运行时动态添加,无需重新编译主程序。

4. 泛型编程与类型安全机制

4.1 泛型的基本概念

4.1.1 为什么需要泛型

在传统的非泛型编程中,我们通常需要为每种数据类型编写重复的代码,例如一个简单的栈类(Stack),如果我们希望它支持整型、字符串、浮点型等不同类型的数据,就需要分别为这些类型编写各自的实现类,这不仅增加了代码量,还降低了可维护性和类型安全性。

泛型(Generics)的出现正是为了解决这一问题。通过泛型,我们可以编写一次代码,并将其应用于多种数据类型。例如,我们可以定义一个 Stack<T> 类,其中 T 是一个类型参数,表示任意合法的 .NET 类型。这样,无论我们使用 Stack<int> Stack<string> 还是 Stack<DateTime> ,编译器都会自动为我们生成特定类型的代码,确保类型安全和性能优化。

泛型不仅提高了代码复用性,还增强了程序的类型检查能力。例如,在非泛型集合中,当我们从集合中取出一个对象时,需要进行强制类型转换,这可能导致运行时错误。而泛型集合会自动进行类型检查,避免了这种风险。

4.1.2 泛型的优势与应用场景

泛型在 C# 中具有以下几个核心优势:

  1. 类型安全 :泛型在编译阶段即可进行类型检查,避免运行时因类型不匹配导致的异常。
  2. 性能优化 :泛型避免了装箱拆箱操作,特别是在处理值类型时,可以显著提升性能。
  3. 代码复用 :泛型允许我们编写一次通用逻辑,适配多种类型,减少冗余代码。
  4. 可读性提升 :使用泛型可以让代码更清晰地表达意图,例如 List<string> List 更具语义。

泛型广泛应用于集合类、算法类、接口、委托、事件等多个方面。例如:

  • List<T> Dictionary<TKey, TValue> 是泛型集合的典型应用。
  • Func<T, TResult> Action<T> 是泛型委托的代表。
  • 设计模式中,如工厂模式、策略模式等,也常使用泛型实现更灵活的架构。

4.2 泛型的使用方式

4.2.1 泛型类与泛型方法

泛型类的定义方式是在类名后添加类型参数 <T> ,并在类体内使用该类型参数。例如,一个简单的泛型栈类如下:

public class Stack<T>
{
    private List<T> items = new List<T>();

    public void Push(T item)
    {
        items.Add(item);
    }

    public T Pop()
    {
        if (items.Count == 0)
            throw new InvalidOperationException("Stack is empty.");

        T item = items[items.Count - 1];
        items.RemoveAt(items.Count - 1);
        return item;
    }
}

在这个例子中:

  • T 是一个类型参数,表示任意合法的 .NET 类型。
  • Push 方法接受一个 T 类型的参数。
  • Pop 方法返回一个 T 类型的值。

使用方式如下:

Stack<int> intStack = new Stack<int>();
intStack.Push(10);
int value = intStack.Pop(); // 安全获取 int 类型

泛型方法则是在方法级别定义类型参数。例如:

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); // 正确交换 int 类型

string s1 = "hello", s2 = "world";
Utility.Swap(ref s1, ref s2); // 正确交换 string 类型

4.2.2 泛型接口与泛型委托

泛型接口用于定义通用的行为契约,使得不同类型的类可以实现相同的接口方法。例如:

public interface IRepository<T>
{
    void Add(T item);
    T GetById(int id);
    IEnumerable<T> GetAll();
}

实现该接口的具体类如下:

public class EmployeeRepository : IRepository<Employee>
{
    public void Add(Employee item)
    {
        // 添加员工逻辑
    }

    public Employee GetById(int id)
    {
        // 查询员工逻辑
        return new Employee();
    }

    public IEnumerable<Employee> GetAll()
    {
        // 获取所有员工逻辑
        return new List<Employee>();
    }
}

这样, EmployeeRepository 可以专门处理 Employee 类型,而 ProductRepository 可以处理 Product 类型。

泛型委托则允许我们定义通用的回调方法。例如, Func<T, TResult> 是一个泛型委托,表示接受一个 T 类型参数并返回 TResult 类型结果的函数。使用方式如下:

Func<int, string> converter = x => x.ToString();
string result = converter(123); // 输出 "123"

4.3 泛型约束机制

4.3.1 类型参数约束(where子句)

虽然泛型提供了灵活性,但在某些情况下我们需要限制类型参数的范围,以确保代码的正确性和安全性。C# 提供了 where 子句来实现类型约束。

常见的约束包括:

  • where T : class :指定类型参数必须是引用类型。
  • where T : struct :指定类型参数必须是值类型。
  • where T : new() :指定类型参数必须具有无参构造函数。
  • where T : IComparable :指定类型参数必须实现某个接口。
  • where T : BaseClass :指定类型参数必须是某个基类或其派生类。

例如:

public class MyGenericClass<T> where T : class, IComparable, new()
{
    public T CreateInstance()
    {
        T instance = new T(); // 调用无参构造函数
        return instance;
    }
}

在这个例子中, T 必须是引用类型、实现 IComparable 接口,并具有无参构造函数。

4.3.2 构造函数约束与引用类型约束

构造函数约束 new() 确保类型参数具有无参构造函数,这对于泛型工厂模式尤为重要。例如:

public class Factory<T> where T : new()
{
    public T Create()
    {
        return new T();
    }
}

如果传入的类型没有无参构造函数,编译器将报错。

引用类型约束 class 用于确保类型参数是引用类型,防止误用值类型。例如:

public class Cache<T> where T : class
{
    private Dictionary<string, T> _cache = new Dictionary<string, T>();

    public void Add(string key, T value)
    {
        _cache[key] = value;
    }

    public T Get(string key)
    {
        return _cache.TryGetValue(key, out var value) ? value : null;
    }
}

这个 Cache<T> 只接受引用类型,因为值类型不能为 null,无法返回默认值。

4.4 协变与逆变

4.4.1 协变与逆变的基本概念

协变(Covariance)和逆变(Contravariance)是 C# 中用于处理泛型接口和委托类型转换的高级特性。它们允许我们进行更灵活的类型赋值,提高代码的兼容性。

协变(Covariance)

协变允许将泛型接口或委托的返回类型从基类转换为派生类。例如:

IEnumerable<Animal> animals = new List<Cat>(); // 协变

这里, List<Cat> 被赋值给 IEnumerable<Animal> ,因为 Cat Animal 的子类。协变通过 out 关键字实现。

逆变(Contravariance)

逆变允许将泛型接口或委托的参数类型从派生类转换为基类。例如:

Action<Animal> action1 = (Animal a) => Console.WriteLine(a);
Action<Cat> action2 = action1; // 逆变

这里, action1 是接受 Animal 的委托,被赋值给接受 Cat action2 ,因为 Cat Animal 的子类。逆变通过 in 关键字实现。

4.4.2 in/out关键字的使用场景

out in 是用于泛型接口和委托中的关键字,用于声明协变和逆变。

out 示例(协变)
public interface IProducer<out T>
{
    T Produce();
}

public class AnimalProducer : IProducer<Animal>
{
    public Animal Produce() => new Animal();
}

public class CatProducer : IProducer<Cat>
{
    public Cat Produce() => new Cat();
}

此时可以:

IProducer<Animal> producer = new CatProducer(); // 协变成立
in 示例(逆变)
public interface IConsumer<in T>
{
    void Consume(T item);
}

public class AnimalConsumer : IConsumer<Animal>
{
    public void Consume(Animal item) => Console.WriteLine("Consuming animal");
}

public class CatConsumer : IConsumer<Cat>
{
    public void Consume(Cat item) => Console.WriteLine("Consuming cat");
}

此时可以:

IConsumer<Cat> consumer = new AnimalConsumer(); // 逆变成立
协变与逆变在 LINQ 中的应用

在 LINQ 查询中, IEnumerable<T> 接口是协变的,因此我们可以将 List<Cat> 当作 IEnumerable<Animal> 使用:

List<Cat> cats = new List<Cat>();
IEnumerable<Animal> animals = cats; // 协变

这使得我们可以使用统一的接口处理不同类型的集合。

协变与逆变的总结对比

特性 关键字 用途 示例
协变 out 返回类型从基类到子类转换 IEnumerable<Animal> animals = new List<Cat>();
逆变 in 参数类型从子类到基类转换 Action<Animal> action = (Animal a) => {}; Action<Cat> catAction = action;

使用场景分析

  • 协变 适用于只读集合或返回值场景,如 IEnumerable<T> Func<out T>
  • 逆变 适用于消费型接口或参数输入场景,如 IComparer<in T> Action<in T>

通过合理使用 in out ,我们可以在泛型编程中实现更灵活、更安全的类型转换,提升代码的可复用性和扩展性。

5. 异常处理机制与try-catch应用

在软件开发中,异常处理是确保程序健壮性和稳定性的关键组成部分。C# 提供了一套完整的异常处理机制,通过 try-catch-finally 结构,开发者可以有效地捕获并处理运行时错误,从而防止程序崩溃。本章将从异常处理的基础知识入手,逐步深入到自定义异常类的设计与使用,并最终探讨异常处理的最佳实践,帮助开发者在实际项目中构建更稳定、更健壮的应用程序。

5.1 异常处理基础

5.1.1 异常处理的流程

在 C# 中,异常处理的核心机制是 try-catch-finally 语句块。其基本流程如下:

  1. try 块 :包含可能引发异常的代码。
  2. catch 块 :用于捕获和处理 try 块中抛出的异常。
  3. finally 块 (可选):无论是否发生异常,都会执行的代码块,通常用于资源清理。

异常处理流程可以用如下 Mermaid 流程图表示:

graph TD
    A[开始执行try代码块] --> B{是否发生异常?}
    B -- 是 --> C[跳转到catch代码块]
    C --> D[处理异常]
    B -- 否 --> E[继续执行try代码块]
    E --> F[执行finally代码块]
    D --> F
    F --> G[结束异常处理流程]

通过这一流程,开发者可以确保即使在异常发生时,程序也能有条不紊地处理错误并继续执行后续逻辑。

5.1.2 try-catch-finally 的使用

以下是一个典型的 try-catch-finally 使用示例,演示如何读取文件内容并处理可能出现的异常:

using System;
using System.IO;

class Program
{
    static void Main()
    {
        string filePath = "example.txt";

        try
        {
            string content = File.ReadAllText(filePath);
            Console.WriteLine("文件内容:\n" + content);
        }
        catch (FileNotFoundException ex)
        {
            Console.WriteLine("错误:文件未找到。");
            Console.WriteLine("异常信息:" + ex.Message);
        }
        catch (IOException ex)
        {
            Console.WriteLine("错误:读取文件时发生 I/O 异常。");
            Console.WriteLine("异常信息:" + ex.Message);
        }
        finally
        {
            Console.WriteLine("无论是否发生异常,都会执行 finally 块。");
        }
    }
}
代码逻辑分析:
  • try 块 :尝试使用 File.ReadAllText() 方法读取文件内容。如果文件不存在或读取过程中出现 I/O 错误,会抛出异常。
  • catch 块
  • 第一个 catch 捕获 FileNotFoundException ,即文件不存在时抛出的异常。
  • 第二个 catch 捕获更通用的 IOException ,处理其他 I/O 错误。
  • finally 块 :无论是否发生异常,都会执行,通常用于释放资源或输出日志信息。
参数说明:
  • FileNotFoundException :当尝试访问不存在的文件时抛出。
  • IOException :在读写文件时遇到系统级 I/O 错误时抛出。
  • ex.Message :获取异常的详细描述信息。

通过合理使用多个 catch 块,开发者可以对不同类型的异常进行精细化处理,提升程序的容错能力。

5.2 异常类型与继承结构

5.2.1 常见内置异常类型

C# 提供了丰富的内置异常类型,均继承自 System.Exception 类。常见的异常类型包括:

异常类型 描述
Exception 所有异常的基类
SystemException 系统级异常的基类,如 IOException OutOfMemoryException
ApplicationException 应用程序逻辑异常的基类,通常用于自定义异常
IndexOutOfRangeException 访问数组越界时抛出
NullReferenceException 尝试访问空对象的成员时抛出
DivideByZeroException 除以零时抛出

这些异常类型构成了 C# 异常体系的基础,开发者应熟悉这些常见异常,以便在调试和开发过程中快速定位问题。

5.2.2 Exception 类的继承关系

C# 的异常类结构是一个典型的继承体系,其核心继承关系如下:

classDiagram
    Exception <|-- SystemException
    Exception <|-- ApplicationException
    SystemException <|-- IOException
    SystemException <|-- OutOfMemoryException
    SystemException <|-- IndexOutOfRangeException
    SystemException <|-- NullReferenceException
    SystemException <|-- DivideByZeroException
    ApplicationException <|-- MyCustomException

在这个继承体系中, Exception 是所有异常的根类, SystemException 表示由运行时引发的严重错误,而 ApplicationException 通常用于应用程序逻辑错误,推荐开发者自定义异常时继承该类。

例如,若需要自定义一个表示“用户未授权”的异常类,可以这样定义:

public class UnauthorizedAccessException : ApplicationException
{
    public UnauthorizedAccessException(string message) : base(message)
    {
    }
}

5.3 自定义异常类

5.3.1 自定义异常的定义与抛出

在实际开发中,使用内置异常虽然可以覆盖大多数情况,但在某些特定业务逻辑中,使用自定义异常能提供更清晰的错误信息和更好的可维护性。

以下是一个自定义异常类的完整示例:

using System;

public class InvalidUserInputException : ApplicationException
{
    public InvalidUserInputException(string message) : base(message)
    {
    }

    public InvalidUserInputException(string message, Exception innerException) 
        : base(message, innerException)
    {
    }
}

class Program
{
    static void ValidateInput(string input)
    {
        if (string.IsNullOrEmpty(input))
        {
            throw new InvalidUserInputException("输入不能为空。");
        }
    }

    static void Main()
    {
        try
        {
            ValidateInput(null);
        }
        catch (InvalidUserInputException ex)
        {
            Console.WriteLine("捕获到自定义异常:" + ex.Message);
        }
    }
}
代码逻辑分析:
  • InvalidUserInputException :继承自 ApplicationException ,用于封装用户输入无效时的异常信息。
  • ValidateInput 方法 :检查输入是否为空,若为空则抛出 InvalidUserInputException
  • Main 方法 :调用 ValidateInput 并捕获自定义异常。
参数说明:
  • message :异常的描述信息。
  • innerException :用于封装原始异常,便于调试时查看异常链。

通过这种方式,开发者可以将业务逻辑错误与系统错误区分开来,使日志和调试信息更具可读性。

5.3.2 异常信息的封装与传递

在复杂系统中,异常往往需要在多个层级之间传递。通过封装异常信息,可以保留原始错误上下文,方便排查问题。

以下是一个异常传递的示例:

using System;

public class DatabaseException : ApplicationException
{
    public DatabaseException(string message, Exception innerException) 
        : base(message, innerException)
    {
    }
}

class Program
{
    static void ConnectToDatabase()
    {
        try
        {
            // 模拟数据库连接失败
            throw new IOException("数据库连接失败。");
        }
        catch (IOException ex)
        {
            throw new DatabaseException("无法连接数据库,请检查网络配置。", ex);
        }
    }

    static void Main()
    {
        try
        {
            ConnectToDatabase();
        }
        catch (DatabaseException ex)
        {
            Console.WriteLine("捕获到数据库异常:" + ex.Message);
            Console.WriteLine("原始异常信息:" + ex.InnerException.Message);
        }
    }
}
代码逻辑分析:
  • ConnectToDatabase 方法 :模拟数据库连接失败,并捕获 IOException ,然后抛出自定义的 DatabaseException ,将原始异常作为 innerException 传入。
  • Main 方法 :捕获 DatabaseException ,并通过 InnerException 属性访问原始异常信息。
参数说明:
  • InnerException :用于存储导致当前异常的原始异常对象,形成异常链。

这种机制在调试时非常有用,开发者可以通过 InnerException 查看完整的错误路径。

5.4 异常处理的最佳实践

5.4.1 避免过度捕获异常

在编写异常处理代码时,应避免使用“通吃”式的 catch 块,例如:

try
{
    // 可能抛出异常的代码
}
catch
{
    Console.WriteLine("发生异常");
}

这种方式虽然可以捕获所有异常,但会隐藏具体的错误信息,不利于调试和维护。应尽量指定具体的异常类型,以便针对性处理。

例如:

try
{
    // 文件读取操作
}
catch (FileNotFoundException ex)
{
    Console.WriteLine("文件未找到:" + ex.Message);
}
catch (IOException ex)
{
    Console.WriteLine("读取文件时发生 I/O 错误:" + ex.Message);
}

5.4.2 记录异常信息与日志管理

在生产环境中,记录异常信息是排查问题的重要手段。建议将异常信息写入日志文件或日志系统,而不是仅输出到控制台。

以下是一个使用 StreamWriter 记录异常信息的简单示例:

using System;
using System.IO;

class Program
{
    static void LogException(Exception ex)
    {
        using (StreamWriter writer = new StreamWriter("error.log", true))
        {
            writer.WriteLine("发生异常:" + ex.Message);
            writer.WriteLine("堆栈跟踪:" + ex.StackTrace);
            writer.WriteLine(new string('-', 40));
        }
    }

    static void Main()
    {
        try
        {
            int result = 10 / int.Parse("0");
        }
        catch (Exception ex)
        {
            LogException(ex);
            Console.WriteLine("异常已记录,请查看日志文件。");
        }
    }
}
代码逻辑分析:
  • LogException 方法 :将异常信息写入 error.log 文件,使用 StreamWriter 并设置 true 参数表示追加模式。
  • Main 方法 :模拟除以零的错误,并调用 LogException 记录异常信息。
参数说明:
  • StreamWriter :用于向文件写入文本。
  • StackTrace :获取异常发生时的调用堆栈信息,有助于定位错误源。

通过日志记录机制,开发者可以在系统运行时收集异常信息,便于后续分析与优化。


本章详细讲解了 C# 中的异常处理机制,涵盖从基础的 try-catch 使用,到自定义异常类的设计与异常信息的封装传递,再到异常处理的最佳实践。通过这些内容,读者将能够构建出更健壮、更稳定的 C# 应用程序。

6. 集合框架与LINQ查询技术

在现代应用程序开发中,数据的存储、处理和查询是核心任务之一。C# 提供了强大的集合框架(Collection Framework)和语言集成查询(LINQ)技术,使得开发者能够以高效、简洁的方式进行数据操作。本章将深入讲解 C# 中的集合类型与 LINQ 查询技术,帮助开发者掌握从数据存储到高效查询的完整流程。

6.1 集合类型概览

C# 中的集合类主要分为两类: 非泛型集合 泛型集合 。泛型集合由于具备类型安全性,在现代 C# 开发中被广泛使用。

6.1.1 非泛型集合与泛型集合对比

非泛型集合主要存在于 System.Collections 命名空间中,如 ArrayList HashTable Stack Queue 等。它们可以存储任意类型的对象,但存在性能和类型安全问题。

泛型集合位于 System.Collections.Generic 命名空间中,如 List<T> Dictionary<TKey, TValue> Queue<T> Stack<T> 等,它们在定义时就明确了数据类型,避免了类型转换和装箱拆箱操作。

特性 非泛型集合 泛型集合
类型安全
性能 低(涉及装箱拆箱) 高(直接操作类型)
命名空间 System.Collections System.Collections.Generic
使用场景 旧代码兼容 推荐使用

代码示例:

// 非泛型集合示例
ArrayList arrayList = new ArrayList();
arrayList.Add(100);         // 添加整数
arrayList.Add("hello");     // 添加字符串

// 泛型集合示例
List<int> intList = new List<int>();
intList.Add(100);           // 只能添加整数
// intList.Add("hello");    // 编译错误,类型不匹配

逐行分析:
- ArrayList 是非泛型集合,可以添加任意类型对象。
- List<int> 是泛型集合,仅允许添加 int 类型的数据。
- 使用泛型集合时,若尝试添加错误类型,编译器会报错,提升了类型安全性。

6.1.2 常用集合类的使用场景

不同集合适用于不同场景,以下是几种常见集合及其典型用途:

List
  • 特点 :动态数组,支持快速索引访问。
  • 适用场景 :需要频繁访问元素、动态增删数据。
List<string> names = new List<string>();
names.Add("Alice");
names.Add("Bob");
names.Remove("Alice");
Dictionary
  • 特点 :键值对集合,通过键快速查找。
  • 适用场景 :需要通过唯一键快速访问值。
Dictionary<int, string> idToName = new Dictionary<int, string>();
idToName.Add(1, "Alice");
idToName.Add(2, "Bob");
string name = idToName[1];  // 获取键为1的值
Queue
  • 特点 :先进先出(FIFO)队列。
  • 适用场景 :任务调度、消息队列。
Queue<int> numbers = new Queue<int>();
numbers.Enqueue(1);
numbers.Enqueue(2);
int first = numbers.Dequeue();  // 弹出第一个元素
Stack
  • 特点 :后进先出(LIFO)栈。
  • 适用场景 :撤销操作、递归模拟。
Stack<string> history = new Stack<string>();
history.Push("Page1");
history.Push("Page2");
string lastPage = history.Pop();  // 弹出最近页面

流程图:

graph TD
    A[数据结构选择] --> B{是否需要索引访问}
    B -->|是| C[List<T>]
    B -->|否| D{是否需要快速查找}
    D -->|是| E[Dictionary<TKey, TValue>]
    D -->|否| F{是否先进先出}
    F -->|是| G[Queue<T>]
    F -->|否| H[Stack<T>]

6.2 LINQ基础语法

LINQ(Language Integrated Query)是 C# 提供的一种统一的数据查询语法,支持从集合、数据库、XML 等多种数据源中进行查询。

6.2.1 查询表达式与方法语法

LINQ 提供两种书写方式: 查询表达式 方法语法 。查询表达式类似于 SQL,可读性高;方法语法则更灵活,适合链式调用。

List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };

// 查询表达式
var query = from n in numbers
            where n % 2 == 0
            select n;

// 方法语法
var methodSyntax = numbers.Where(n => n % 2 == 0);

逐行分析:
- from n in numbers 表示从 numbers 列表中遍历每个元素。
- where n % 2 == 0 是筛选条件,保留偶数。
- select n 指定返回的元素。
- 方法语法中使用了 Where 扩展方法,传入 lambda 表达式作为筛选条件。

6.2.2 常用LINQ操作符

以下是一些常用的 LINQ 操作符及其用途:

操作符 用途 示例
Where 筛选元素 list.Where(x => x > 10)
Select 投影变换 list.Select(x => x * 2)
OrderBy 排序 list.OrderBy(x => x)
GroupBy 分组 list.GroupBy(x => x % 3)
FirstOrDefault 获取第一个元素或默认值 list.FirstOrDefault()
Count 统计数量 list.Count()
Any 判断是否有元素满足条件 list.Any(x => x > 5)

代码示例:

List<string> names = new List<string> { "Alice", "Bob", "Charlie", "David" };

// 获取长度大于3的名字
var filtered = names.Where(n => n.Length > 3);

// 按名字长度排序
var sorted = names.OrderBy(n => n.Length);

// 分组并统计每组数量
var grouped = names.GroupBy(n => n.Length)
                   .Select(g => new { Length = g.Key, Count = g.Count() });

6.3 LINQ与集合的结合使用

LINQ 与集合结合使用可以实现更复杂的数据处理逻辑,如排序、分组、投影等。

6.3.1 对List与Dictionary的查询

查询 List
List<int> numbers = new List<int> { 10, 20, 30, 40, 50 };

// 查询大于25的数字
var result = numbers.Where(n => n > 25);

// 输出结果
foreach (var n in result)
{
    Console.WriteLine(n);  // 输出:30, 40, 50
}
查询 Dictionary
Dictionary<string, int> scores = new Dictionary<string, int>
{
    { "Alice", 90 },
    { "Bob", 78 },
    { "Charlie", 85 }
};

// 查询分数大于80的学生
var highScorers = scores.Where(s => s.Value > 80);

// 输出结果
foreach (var entry in highScorers)
{
    Console.WriteLine($"{entry.Key}: {entry.Value}");  // Alice:90, Charlie:85
}

6.3.2 数据排序与分组操作

排序操作
List<Person> people = new List<Person>
{
    new Person { Name = "Alice", Age = 25 },
    new Person { Name = "Bob", Age = 30 },
    new Person { Name = "Charlie", Age = 20 }
};

// 按年龄升序排序
var sortedByAge = people.OrderBy(p => p.Age);

// 按姓名降序排序
var sortedByNameDesc = people.OrderByDescending(p => p.Name);
分组操作
// 按年龄段分组
var groupedByAge = people.GroupBy(p => p.Age / 10 * 10);  // 每10岁为一组

foreach (var group in groupedByAge)
{
    Console.WriteLine($"Group {group.Key}:");
    foreach (var person in group)
    {
        Console.WriteLine($"- {person.Name} ({person.Age})");
    }
}

Person 类定义:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

6.4 LINQ进阶应用

6.4.1 延迟执行与立即执行

LINQ 查询分为 延迟执行 立即执行 两种模式。延迟执行意味着查询不会立即执行,而是在遍历结果时才真正执行。

延迟执行示例:
List<int> numbers = new List<int> { 1, 2, 3 };
var query = numbers.Select(n => n * 2);  // 延迟执行

numbers.Add(4);  // 修改原数据

foreach (var n in query)
{
    Console.WriteLine(n);  // 输出:2, 4, 6, 8
}
立即执行示例:
var immediate = numbers.Select(n => n * 2).ToList();  // 立即执行

numbers.Add(4);  // 修改原数据不影响结果

foreach (var n in immediate)
{
    Console.WriteLine(n);  // 输出:2, 4, 6
}

总结:
- ToList() ToArray() Count() 等方法会触发立即执行。
- Where Select 等操作默认是延迟执行。

6.4.2 使用LINQ进行XML与数据库查询

查询 XML
XElement xml = XElement.Parse(@"
    <Students>
        <Student Id='1'>
            <Name>Alice</Name>
        </Student>
        <Student Id='2'>
            <Name>Bob</Name>
        </Student>
    </Students>");

var studentNames = from s in xml.Elements("Student")
                   select s.Element("Name").Value;

foreach (var name in studentNames)
{
    Console.WriteLine(name);  // Alice, Bob
}
查询数据库(Entity Framework 示例)
using (var context = new SchoolContext())
{
    var students = from s in context.Students
                   where s.Age > 20
                   select s;

    foreach (var student in students)
    {
        Console.WriteLine(student.Name);
    }
}

说明:
- 该查询使用 Entity Framework 的 LINQ to Entities,生成 SQL 语句在数据库中执行。
- 查询在 foreach 遍历时才真正执行。

流程图:

graph TD
    A[LINQ查询] --> B{是否立即执行}
    B -->|是| C[ToList, ToArray, Count]
    B -->|否| D[Where, Select]
    D --> E[延迟执行,遍历时触发]
    C --> F[数据库查询]
    D --> G[内存中查询]

本章从集合框架入手,深入讲解了泛型集合的优势、常用集合类型及其适用场景,并详细介绍了 LINQ 的基础语法、操作符以及与集合的结合使用。通过示例代码与流程图的辅助,帮助读者掌握如何高效地进行数据操作与查询,为后续的数据库访问、异步编程等高级主题打下坚实基础。

7. 异步编程模型(async/await)

在现代应用程序开发中,异步编程已成为提升性能与用户体验的关键技术。C# 提供了强大的异步编程模型,其中 async await 关键字极大地简化了异步代码的编写与维护。本章将深入讲解异步编程模型的原理与使用方式,帮助开发者构建高效、响应迅速的应用程序。

7.1 异步编程基础

7.1.1 同步与异步的区别

在同步编程中,程序会按顺序依次执行任务,当前任务未完成前,后续任务必须等待。这种方式虽然逻辑清晰,但在执行耗时操作(如网络请求、文件读取)时会导致程序“卡顿”,影响用户体验。

异步编程则允许程序在执行耗时任务时,不阻塞主线程,从而保持应用程序的响应性。C# 中通过 Task 类和 async/await 语法实现异步操作。

同步与异步对比示例:

// 同步方式
public void DownloadFile()
{
    using (var client = new WebClient())
    {
        string content = client.DownloadString("https://example.com");
        Console.WriteLine("下载完成");
    }
}

// 异步方式
public async Task DownloadFileAsync()
{
    using (var client = new WebClient())
    {
        string content = await client.DownloadStringTaskAsync("https://example.com");
        Console.WriteLine("下载完成");
    }
}

7.1.2 Task类与异步方法的定义

Task 是 C# 异步编程的核心类,表示一个异步操作。它封装了任务的状态、返回值、异常等信息。

  • Task :表示没有返回值的异步操作。
  • Task<T> :表示返回值为 T 的异步操作。

定义异步方法:

public async Task<int> CalculateSumAsync(int a, int b)
{
    await Task.Delay(1000); // 模拟耗时操作
    return a + b;
}
  • 方法名通常以 Async 结尾。
  • 返回类型为 Task Task<T>
  • 使用 await 调用异步操作。

7.2 async/await的使用方式

7.2.1 编写异步方法

异步方法的编写遵循以下规则:

  1. 方法使用 async 修饰符。
  2. 方法返回类型为 Task Task<T> void (仅用于事件处理)。
  3. 使用 await 关键字等待异步操作完成。

示例:

public async Task<int> GetDataFromWebAsync()
{
    var client = new HttpClient();
    string result = await client.GetStringAsync("https://example.com/data");
    return result.Length;
}

7.2.2 await表达式的执行流程

await 表达式会挂起当前方法的执行,直到被等待的异步操作完成。在此期间,控制权会返回给调用者,从而避免阻塞主线程。

执行流程图示:

graph TD
    A[调用GetDataFromWebAsync] --> B[进入方法体]
    B --> C[调用GetStringAsync]
    C --> D[await等待响应]
    D --> E[释放线程,控制权返回]
    E --> F[响应到达]
    F --> G[继续执行后续代码]
    G --> H[返回结果]

7.3 异步编程中的异常处理

7.3.1 异步方法中异常的捕获

在异步方法中,异常不会立即抛出,而是封装在 Task 对象中。可以通过 try-catch 捕获 await 表达式抛出的异常。

示例:

public async Task HandleExceptionAsync()
{
    try
    {
        string result = await GetStringFromWebAsync();
        Console.WriteLine(result);
    }
    catch (Exception ex)
    {
        Console.WriteLine($"发生异常:{ex.Message}");
    }
}

private async Task<string> GetStringFromWebAsync()
{
    // 模拟异常
    throw new InvalidOperationException("网络请求失败");
}

7.3.2 多个异步任务的异常聚合

当并发执行多个异步任务时,可以使用 Task.WhenAll 并结合 try-catch 来处理多个任务的异常。

public async Task HandleMultipleExceptionsAsync()
{
    var task1 = GetStringFromWebAsync("https://example.com/data1");
    var task2 = GetStringFromWebAsync("https://example.com/data2");

    try
    {
        string[] results = await Task.WhenAll(task1, task2);
        Console.WriteLine($"结果1长度:{results[0].Length},结果2长度:{results[1].Length}");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"发生异常:{ex.Message}");
    }
}

private async Task<string> GetStringFromWebAsync(string url)
{
    var client = new HttpClient();
    return await client.GetStringAsync(url);
}

7.4 异步编程最佳实践

7.4.1 避免死锁与线程池饥饿

在使用 Result Wait() 方法等待异步任务完成时,容易引发死锁问题,尤其是在 UI 线程中。

错误示例:

var result = GetDataAsync().Result; // 可能导致死锁

应始终使用 await 来等待异步操作,避免阻塞线程。

7.4.2 异步UI更新与后台任务管理

在 WPF 或 WinForms 应用中,异步操作完成后更新 UI 必须确保在 UI 线程上执行。

示例:

private async void btnDownload_Click(object sender, EventArgs e)
{
    string content = await DownloadDataAsync();
    txtResult.Text = content; // 自动回到UI线程
}

private async Task<string> DownloadDataAsync()
{
    using (var client = new WebClient())
    {
        return await client.DownloadStringTaskAsync("https://example.com");
    }
}

await 表达式会自动捕获当前同步上下文(如 UI 线程),并在任务完成后恢复执行环境,确保 UI 更新安全。

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

简介:本课件是一份系统化的C#编程语言教学资料,专为帮助学习者掌握C#基础语法与高级特性而设计。通过清晰的幻灯片形式,详细讲解了变量、控制流、面向对象编程、泛型、异常处理、LINQ、异步编程等核心知识点,并涵盖Windows Forms、WPF、ASP.NET等实际应用场景。适合初学者与进阶开发者学习和实践,结合实例提升编程能力,为开发桌面、Web及移动应用打下坚实基础。


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

更多推荐