C#编程语言系统化教学PPT课件
简介:本课件是一份系统化的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# 中具有以下几个核心优势:
- 类型安全 :泛型在编译阶段即可进行类型检查,避免运行时因类型不匹配导致的异常。
- 性能优化 :泛型避免了装箱拆箱操作,特别是在处理值类型时,可以显著提升性能。
- 代码复用 :泛型允许我们编写一次通用逻辑,适配多种类型,减少冗余代码。
- 可读性提升 :使用泛型可以让代码更清晰地表达意图,例如
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 语句块。其基本流程如下:
- try 块 :包含可能引发异常的代码。
- catch 块 :用于捕获和处理
try块中抛出的异常。 - 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 编写异步方法
异步方法的编写遵循以下规则:
- 方法使用
async修饰符。 - 方法返回类型为
Task、Task<T>或void(仅用于事件处理)。 - 使用
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 更新安全。
简介:本课件是一份系统化的C#编程语言教学资料,专为帮助学习者掌握C#基础语法与高级特性而设计。通过清晰的幻灯片形式,详细讲解了变量、控制流、面向对象编程、泛型、异常处理、LINQ、异步编程等核心知识点,并涵盖Windows Forms、WPF、ASP.NET等实际应用场景。适合初学者与进阶开发者学习和实践,结合实例提升编程能力,为开发桌面、Web及移动应用打下坚实基础。
更多推荐



所有评论(0)