C#知识学习-014(修饰符_3)
1.async
1.1 用途
目的是为了让程序在等待耗时操作(如下载)时,界面还能保持流畅响应。
async
关键字:标记“我会等”
- 你把
async
放在方法声明前面(比如public async void
ExampleDemo()
)。 - 这就像给这个方法贴了个标签:“我这个方法里面,可能会用到
await
去等一些耗时的事情做完。” - 编译器看到
async
,就知道这个方法内部可能有等待点,需要特殊处理(生成状态机)。 - 重要:加了
async
的方法,本身并不会让方法变异步!它只是允许你在方法内部使用await
。
await
关键字:真正的“等待点”
- 你在
async
方法内部,在需要花时间的操作(如httpClient.GetStringAsync(...)
)前加上await
。 async
方法在遇到第一个await
前是同步执行的。- 当程序执行到
await
这一行时:- 启动耗时操作: 比如开始下载网页内容。
- 交出控制权: 程序不会傻等!它会立刻返回到调用这个
async
方法的地方。 - 不阻塞: 在耗时操作进行的同时,你的程序界面完全不受影响!你可以点其他按钮,窗口可以拖动。
- 等待完成: 程序在后台默默地等那个耗时操作(下载)完成。
- 完成后继续: 一旦下载完成,程序会神奇地回到
await
语句后面那行代码继续执行,并且拿到下载的结果。
返回类型:
Task
: 表示这个异步方法最终会完成,但不返回具体值。调用者可以用await
等它完成。Task<int>
:表示这个异步方法最终会完成,并且会返回一个int
值。调用者用await
等它完成并拿到int
结果。void
:通常只用在事件处理程序(比如按钮点击StartButton_Click
)。因为事件处理程序的签名是固定的(返回void
)。- 其他(如
ValueTask<T>,
GetAwaiter方法
):高级优化,暂时不用管。
1.2 举例
private async void StartButton_Click(object sender, RoutedEventArgs e)
{try{int length = await ExampleMethodAsync();}catch (Exception){// 处理异常}
}public async Task<int> ExampleMethodAsync()
{var httpClient = new HttpClient();int exampleInt = (await httpClient.GetStringAsync("http://msdn.microsoft.com")).Length;return exampleInt;
}
执行流程:
- 用户点击按钮
StartButton
,触发StartButton_Click
方法(async
方法)。 - 调用
ExampleMethodAsync()方法(
async
方法)。先同步执行var httpClient = new HttpClient();
然后执行到await httpClient.GetStringAsync(...)
:- 控制权立刻交还给
StartButton_Click
方法。 - 遇到
await
!ExampleMethodAsync
方法在此处暂停。GetStringAsync
启动网络请求(去微软网站下载内容)。 StartButton_Click
方法在await ExampleMethodAsync()
这里也暂停了(因为它也在await
)。控制权最终交还给系统的消息循环(负责处理界面点击、刷新的核心部分)。- 关键点:此时,网络请求在后台进行,但你的程序界面是完全自由的!你可以拖动窗口、点其他按钮!
- 经过一段时间,网络请求完成了。系统安排程序回到暂停的地方继续执行:
- 首先回到
ExampleMethodAsync
中await
后面:拿到下载的网页内容,计算长度 (Length
),赋值给exampleInt,然后
return
,ExampleMethodAsync
完成。
- 首先回到
ExampleMethodAsync
返回结果后,StartButton_Click
中await ExampleMethodAsync()
也完成了,拿到返回值length
。完成
- 控制权立刻交还给
1.3 补充
我们来重点解释一下 “调用 async void
方法的人无法 await
它,也很难捕获它内部的异常”
先来看一下普通(同步)方法:
当 B
调用 A
时,如果 A
内部抛出异常,这个异常会“冒泡”到 B
的 catch
块,可以被捕获和处理。这是标准的异常处理流程。
void A()
{throw new Exception("Oops!"); // 抛出一个异常
}void B()
{try{A(); // 调用可能抛出异常的方法}catch (Exception ex){Console.WriteLine($"{ex.Message}"); // 可以在这里捕获异常}
}
现在,换成 async void
方法:
async void A()
{await Task.Delay(1000); // 模拟一些异步操作throw new Exception("Big Oops in Async Void!"); // 在异步操作后抛出异常
}void B()
{try{A(); // 调用 async void 方法}catch (Exception ex){Console.WriteLine($"{ex.Message}"); // 这行代码能捕获到异常吗?}
}
问题出在哪里?
无法 await
:
B
调用A()
。因为A
返回void
,而不是Task
或Task<T>
,所以B
没有办法在调用后面写await
。A()
调用会立即返回(在遇到第一个await
之前或之后不久)。B
的try
块瞬间就执行完毕了,catch
块也瞬间就过去了。- 此时,
A
内部的异步操作(Task.Delay
)还在后台进行。
异常发生在“未来”:
- 异常是在
A
内部的await Task.Delay(1000);
完成之后才抛出的(throw new Exception...
)。 - 这时,
B
的try/catch
块早就执行完了!它已经“不在现场”了,无法捕获这个发生在未来的异常。
异常去了哪里?
- 因为调用者(
B
)无法await
并因此无法关联到A
最终完成的状态(成功或失败),这个异常就丢失了正常的传播路径。 - 在 GUI 应用程序(如 WPF, WinForms)中,这类未捕获的异步异常通常会触发应用程序级别的全局异常事件(例如
Application.Current.DispatcherUnhandledException
)。如果你没有处理这个全局事件,应用程序可能会直接崩溃。 - 在 ASP.NET Core 中,未捕获的异步异常会导致服务器记录错误,并可能终止当前请求的处理,返回 500 错误给客户端。
- 在 控制台应用程序 中,这样的异常通常会导致进程崩溃。
为什么 async Task
就没有这个问题?
async Task A()
{await Task.Delay(1000);throw new Exception("Ooops in Async Task!");
}async Task B()
{try{await A(); // 关键:这里可以 await!}catch (Exception ex){Console.WriteLine($"{ex.Message}"); // 可以在这里捕获异常}
}
A
返回一个Task
。这个Task
对象代表了整个异步操作的状态(进行中、已完成、出错)。B
使用await A();
。await
会挂起B
,直到A
返回的Task
完成(无论是成功完成还是出错)。- 如果
A
内部抛出了异常,这个异常会被“打包”进它返回的那个Task
对象中,标记为“失败”状态。
- 当
await
发现它等待的Task
是失败状态时,它会解包这个Task
,并将其中包含的异常重新抛出在B
的上下文中。 - 因为
await
语句位于B
的try
块内,所以这个重新抛出的异常会被下面的catch
块捕获到。
2.static
2.1 用途
它主要解决一个问题:这个东西是属于“模板”本身的,还是属于用“模板”做出来的“东西”?
想象一下做饼干:
饼干模具(类 - class
): 这就是模板,定义了饼干是什么样子(有什么形状、花纹)。
做出来的饼干(对象 - object
): 用模具压出来的一个个具体的饼干。每个饼干有自己的位置、被谁吃了,但它们的花纹都一样(来自模具)。
现在,static
的作用就是标记那些属于模具本身的东西,而不是属于某个具体的饼干。
核心概念:
非static
(实例成员)
- 属于对象:每个对象都有自己的副本
- 需要先创建对象才能使用
- 存储对象的状态(如人的身高、体重)
- 通过对象访问:
对象.成员名
static
(静态成员)
- 属于类本身:只有一份副本
- 不需要创建对象就能使用
- 存储类的共享状态(如公司员工总数)
- 通过类名访问:
类名.成员名
2.2 示例
2.2.1 基本静态字段和方法
Count
属于模板(记录所有对象的计数器)→static
InstanceId
属于具体对象(每个对象的唯一ID)→ 非static
DisplayTotal
属于模板(显示类级别的信息)→static
Counter.DisplayTotal(); // 输出: 总实例数: 0var c1 = new Counter();
var c2 = new Counter();
var c3 = new Counter();Counter.DisplayTotal(); // 输出: 总实例数: 3Console.WriteLine($"c1 ID: {c1.InstanceId}"); // 输出: c1 ID: 1
Console.WriteLine($"c2 ID: {c2.InstanceId}"); // 输出: c2 ID: 2
Console.WriteLine($"c3 ID: {c3.InstanceId}"); // 输出: c3 ID: 3public class Counter
{// 静态字段 - 属于类本身,所有实例共享public static int Count = 0;// 实例字段 - 每个对象有自己的副本public int InstanceId;public Counter(){// 每次创建新对象时增加计数器Count++;InstanceId = Count;}// 静态方法 - 通过类名访问public static void DisplayTotal(){Console.WriteLine($"总实例数: {Count}");}
}
2.2.2 静态类(工具类)
MathUtils
是静态类,不能创建实例所有方法都是静态的,通过类名直接调用
适合创建工具类,不需要对象状态
double area = MathUtils.CircleArea(5);
Console.WriteLine($"圆面积: {area}"); // 输出: 圆面积: 78.54...// 静态类 - 不能实例化,只能包含静态成员
public static class MathUtils
{// 静态方法 - 计算圆的面积public static double CircleArea(double radius){return Math.PI * radius * radius;}
}
2.2.3 静态构造函数
静态构造函数在类首次使用前自动执行
用于初始化静态字段
整个程序生命周期只执行一次
// 第一次使用类时触发静态构造函数
ConfigLoader.ShowConfig(); // 输出: 加载配置... 从配置文件读取的数据public class ConfigLoader
{// 静态字段public static readonly string ConfigData;// 静态构造函数 - 在类首次使用前自动执行static ConfigLoader(){Console.WriteLine("加载配置...");ConfigData = "从配置文件读取的数据";}public static void ShowConfig(){Console.WriteLine(ConfigData);}
}
2.2.4 静态局部函数
Multiply
是静态局部函数,只能使用传入的参数Add
是非静态局部函数,可以访问外部变量静态局部函数更安全,不依赖外部状态
int a = 10;
int b = 5;// 静态局部函数 - 不能访问外部变量a和b
static int Multiply(int x, int y) => x * y;// 非静态局部函数 - 可以访问外部变量
int Add() => a + b;Console.WriteLine($"乘法: {Multiply(3, 4)}"); // 输出: 乘法: 12
Console.WriteLine($"加法: {Add()}"); // 输出: 加法: 15
2.2.5 静态字段共享问题
var acc1 = new BankAccount(1000);
var acc2 = new BankAccount(2000);acc1.ApplyInterest();
Console.WriteLine($"账户1余额: {acc1.Balance}"); // 1030// 修改静态字段会影响所有实例
BankAccount.InterestRate = 0.05m; // 改为5%acc2.ApplyInterest();
Console.WriteLine($"账户2余额: {acc2.Balance}"); // 2100
public class BankAccount
{// 静态字段 - 所有账户共享// decimal是 C# 中用于高精度十进制计算的类型,特别适合处理财务和货币计算// 必须使用 m后缀表示 decimal字面量public static decimal InterestRate = 0.03m; // 3%// 实例字段public decimal Balance { get; private set; }public BankAccount(decimal initial){Balance = initial;}public void ApplyInterest(){Balance += Balance * InterestRate;}
}
学到了这里,咱俩真棒,记得按时吃饭(废寝可以,忘食不行~)
【本篇结束,新的知识会不定时补充】
感谢你的阅读!如果内容有帮助,欢迎 点赞❤️ + 收藏⭐ + 关注 支持! 😊