当前位置: 首页 > news >正文

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方法。
    • 遇到 awaitExampleMethodAsync方法​​在此处暂停​​。GetStringAsync​启动​​网络请求(去微软网站下载内容)。
    • StartButton_Click方法在 await ExampleMethodAsync()这里也暂停了(因为它也在 await)。控制权最终交还给系统的​​消息循环​​(负责处理界面点击、刷新的核心部分)。
      • 关键点:此时,网络请求在后台进行,但你的程序界面是完全自由的!你可以拖动窗口、点其他按钮!​
    • 经过一段时间,网络请求完成了。系统安排程序​​回到暂停的地方继续执行​​:
      • 首先回到 ExampleMethodAsync中 await后面:拿到下载的网页内容,计算长度 (Length),赋值给 exampleInt,然后returnExampleMethodAsync完成。
    • 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;}
}

学到了这里,咱俩真棒,记得按时吃饭(废寝可以,忘食不行~)

【本篇结束,新的知识会不定时补充】

感谢你的阅读!如果内容有帮助,欢迎 ​​点赞❤️ + 收藏⭐ + 关注​​ 支持! 😊

http://www.dtcms.com/a/389566.html

相关文章:

  • Linux 下逆向解析 VNC Server 密码文件为明文密码(逆向解析passwd)
  • Linux dma_resv机制原理、实现与应用详解
  • LangGraph 进阶学习
  • Alibaba Cloud Linux与 RHEL/CentOS版本对应关系
  • Python实现PDF文本与表格转换
  • 医疗行业数字化转型:构建安全合规、高效协同的智慧医疗文档管理新范式
  • 怎么看一个网址是否安全?
  • 【LLM】RAG架构如何重塑大模型
  • 企业级数据库管理实战(四):从 C/S 到 B/S架构,数据库管理工具的演进
  • 基于AI的PDF复杂表格结构识别与智能解析(方案1)
  • CS336第三课
  • 云蝠智能大模型呼叫对话延迟无限接近1秒
  • Datax-web安装 | 配置环境
  • 算法<java>——查找(顺序、二分、插值、分块、斐波那契)
  • Mysql杂志(十九)——InnoDB的索引结构
  • CrowdStrike推出AI驱动新工具 聚焦补丁管理与威胁情报短板
  • 收集飞花令碎片——C语言指针
  • MySQL 初识:架构定位与整体组成
  • 【开发者导航】规范驱动且开源的 AI 时代开发流程工具:GitHub Spec-Kit
  • 区块链加速器:Redis优化以太坊交易池性能方案
  • 资源分布的均衡性(Poisson Disk Sampling)探索
  • STM32开发(中断模式)
  • Qt QPieSlice详解
  • C++多线程编程
  • LangChain 父文档检索器:解决 “文档块匹配准” 与 “信息全” 的矛盾
  • COI实验室技能:基于几何光学的物空间与像空间的映射关系
  • springboot-security安全插件使用故障解析
  • 企业移动化管理(EMM)实战:如何一站式解决设备、应用与安全管控难题?
  • 高频面试题——深入掌握栈和队列的数据结构技巧
  • 【C++ qml】qml页面加载配置文件信息的两种方式