前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >字符串优化处理

字符串优化处理

原创
作者头像
七七分享
修改于 2019-05-20 02:33:07
修改于 2019-05-20 02:33:07
7240
举报

原文:https://www.relaxheart.cn/to/master/blog?uuid=87

String对象及特点

String对象时Java语言中重要的数据类型,但它并不是Java的基本数据类型。在C语言中对字符串的处理通常做法时使用char[],但这这种方式的弊端很明显,数组本身无法封装字符串的操作所需的基本方法。如下图所示,Java的String类型基本实现由3部分组成:char数组、偏移量和String的长度。

String.png
String.png

这里有一个点需要提一下:String的真实长度还需要由偏移量和长度在这个char数组中进行定位和截取,因为待会会提到String.subString()方法导致内存泄漏的问题,它的根因就在这,先提前抛出来。

在Java语言中,Java的设计者对String对象进行了大量的优化,其主要表现在一下三点:

(1)不变性;

(2)针对常量池的优化;

(3)类的final定义

不变性

不变性是指String对象一旦生成,则不能再对它进行改变。String的这个特性可以泛化成不变模式,即一个对象的状态被创建之后就不再发生变化。不变模式的主要作用在于当一个对象需要被多线程共享,并且频繁访问时,可以省略同步和锁等待的时间,从而大幅提高系统性能。

针对常量池的优化

当两个String对象拥有相同 的值时,他们只引用常量池中的同一个拷贝。当同一个字符串反复出现时,这个计数可以大幅度节省内存空间。

代码语言:txt
AI代码解释
复制
    public static void main(String[] args) {
        String str1 = "aaa";
        String str2 = "aaa";
        String str3 = new String("aaa");

        System.out.println(str1 == str2);
        System.out.println(str1 == str3);
        System.out.println(str1 == str3.intern());
    }

运行结果:

代码语言:txt
AI代码解释
复制
true
false
true

Process finished with exit code 0

上述代码显示str1和str2引用了相同的地址,但是str3却重新开辟了一块内存空间。但即便如此,str3在常量池中的位置和str1还是一样的,也就是说,虽然str3单独占用了堆空间,但是它所指向的实体和str1完全一样。代码中最后一行的intern()方法返回的是String对象在常量池的引用.

String内存分配方式.png
String内存分配方式.png
类的final定义

除以上2点,final类型定义也是String对象的重要特点,作为final类的String对象在系统中不可能有任何子类,这是对系统安全行的保护。同时,在JDK1.5版本之前的任何环境使用final定义,有助于帮助JVM寻找机会,内联所有的final方法,从而提高系统环境。但是这种优化在JDK1.5之后,效果并不明显了。

subString()方法的内存泄露

截取字符串是字符串操作中最常用的操作之一,在Java中,String类提供了两个截取字符串的方法:

代码语言:txt
AI代码解释
复制
public String substring(int beginIndex)
public String substring(int beginIndex, int endIndex)

以第2个为例,它返回源字符串中的以beginIndex开始,endIndex为止的字符串。然而,这个方法在JDK中的实现存在严重的内存泄露问题。查看此方法的源码:

代码语言:txt
复制

public String subString(int beginIndex, int endIndex){

代码语言:txt
AI代码解释
复制
    if (beginIndex < 0){
代码语言:txt
AI代码解释
复制
        throw new StringIndexOutOfBoundsException(beginIndex);
代码语言:txt
AI代码解释
复制
    }
代码语言:txt
AI代码解释
复制
    if (endIndex > count){
代码语言:txt
AI代码解释
复制
        throw new StringIndexOutOfBoundsException(beginIndex);
代码语言:txt
AI代码解释
复制
    }
代码语言:txt
AI代码解释
复制
    if (beginIndex > endIndex){
代码语言:txt
AI代码解释
复制
        throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
代码语言:txt
AI代码解释
复制
    }
代码语言:txt
AI代码解释
复制
    return ((beginIndex == 0) && (endIndex == count)) ? this : new String(offset + beginIndex, endIndex - beginIndex, value);
代码语言:txt
AI代码解释
复制
}
代码语言:txt
AI代码解释
复制
在方法的最后一行,返回一个新建的String对象。查看String的构造函数:
```java

// Package private constructor which shares value array for speed.

String(int offset, int count, char[] value){

代码语言:txt
AI代码解释
复制
    this.value = value;
代码语言:txt
AI代码解释
复制
    this.offset = offset;
代码语言:txt
AI代码解释
复制
    this.count = count;
代码语言:txt
AI代码解释
复制
}
代码语言:txt
AI代码解释
复制
源码注释中说明,这是一个包作用域的构造函数,其目的是为了能高效且快速的共享String内的char数组对象。但在这种通过偏移量来截取字符串的方法中,String的原生内容value数组被复制到新的子字符串中。设想,如果原始字符串很大,截取的字符串长度却很短,那么截取的子字符串中包含了原生字符串的所有内容,并占据了响应的内存空间,而仅仅通过偏移量和长度来决定自己的实际取值。这种算法提高了运算速度却浪费了大量的内存空间。
(String的这个构造函数使用了以空间换时间的策略,浪费了内存空间,却提高了字符串的生成速度。)
以下以一个实例来说明该方法的弊端:
```java

public class StringTest {

代码语言:txt
AI代码解释
复制
public static void main(String[] args) {
代码语言:txt
AI代码解释
复制
    List<String> handler = new ArrayList<>();
代码语言:txt
AI代码解释
复制
    for (int i = 0; i < 10000; i++){
代码语言:txt
AI代码解释
复制
        HugeStr h = new HugeStr();
代码语言:txt
AI代码解释
复制
        //ImprovedHugeStr imp = new ImprovedHugeStr();
代码语言:txt
AI代码解释
复制
        handler.add(h.getSubString(1, 5));
代码语言:txt
AI代码解释
复制
    }
代码语言:txt
AI代码解释
复制
}
代码语言:txt
AI代码解释
复制
static class HugeStr{
代码语言:txt
AI代码解释
复制
    // 一个很长的String
代码语言:txt
AI代码解释
复制
    private String str = new String(new char[10000000]);
代码语言:txt
AI代码解释
复制
    // 截取字符串,有溢出
代码语言:txt
AI代码解释
复制
    public String getSubString(int begin, int end){
代码语言:txt
AI代码解释
复制
        return str.substring(begin, end);
代码语言:txt
AI代码解释
复制
    }
代码语言:txt
AI代码解释
复制
}
代码语言:txt
AI代码解释
复制
static class ImprovedHugeStr{
代码语言:txt
AI代码解释
复制
    private String str = new String(new char[10000000]);
代码语言:txt
AI代码解释
复制
    // 截取子字符串,并重新生成
代码语言:txt
AI代码解释
复制
    public String getSubString(int begin, int end){
代码语言:txt
AI代码解释
复制
        return new String(str.substring(begin, end));
代码语言:txt
AI代码解释
复制
    }
代码语言:txt
AI代码解释
复制
}

}

代码语言:txt
AI代码解释
复制
HugeStr.getSubString()在不到1000次的时候就出现了内存溢出。
而ImprovedHugeStr能够很好的工作的关键是因为它使用没有内存泄漏的String构造函数重新生成String对象,使得由subString()方法返回的,存在内存泄漏问题的String对象失去所有的强引用,从而被垃圾回收器识别为垃圾对象进行回收,保证了系统内存的稳定。

#### 字符串分割和查找
-------------------------------------------
字符串分割和查找也是字符串处理中最常用的方法之一。字符串分割将一个原始字符串,根据某个分隔符,切割成一组小字符串。String对象的split()方法便实现了这个功能:
```java

public String[] split(String regex)

代码语言:txt
AI代码解释
复制
以上代码是split()函数的原型,它提供了非常强大的字符串分割功能。传入参数可以是一个正则表达式,从而进行复杂逻辑的字符串分割。
比如字符串“a;b,c:d”,分别使用了分号、逗号、冒号分隔了各个字符串,这时,如果需要将这些分隔符去掉,只保留字母内容,只需要使用正则表达式即可:
```java

String[] array = "a;b,c:d".split(";|,|:]");

代码语言:txt
AI代码解释
复制
对正则表达式的支持,使split()函数本身具有强大的功能,强当地使用,可以起到事半功倍的效果。但是,就简单的字符串分割功能而言,它的性能表现却不尽人意。

##### 最原始的字符串分割
使用以下代码生成一个String对象,并存放在str变量中。
```java

String str = null;

代码语言:txt
AI代码解释
复制
    StringBuffer sb = new StringBuffer();
代码语言:txt
AI代码解释
复制
    for (int i = 0; i < 1000; i++){
代码语言:txt
AI代码解释
复制
        sb.append(i);
代码语言:txt
AI代码解释
复制
        sb.append(";");
代码语言:txt
AI代码解释
复制
    }
代码语言:txt
AI代码解释
复制
    str = sb.toString();
代码语言:txt
AI代码解释
复制
然后,使用split根据";"对字符串进行分割:

long start = System.currentTimeMillis();

代码语言:txt
AI代码解释
复制
    for (int i = 0; i < 10000; i++){
代码语言:txt
AI代码解释
复制
        str.split(";");
代码语言:txt
AI代码解释
复制
    }
代码语言:txt
AI代码解释
复制
    System.out.println("对str进行10000次分割用时cost:" + (System.currentTimeMillis() - start));
代码语言:txt
AI代码解释
复制
执行结果:
```java

对str进行10000次分割用时cost:462

代码语言:txt
AI代码解释
复制
在我的计算机上执行上述分割,共用时462毫秒。是否有更快的发方法完成类似的简单字符串分割呢?来看一个StringTokenizer类。

##### 使用效率更高的StringTokenizer类分割字符串
StringTokenizer类时JDK中提供的专门用来处理字符串分割字串的工具类。它的典型构造函数如下:
```java

public StringTokenizer(String str, String delim)

代码语言:txt
AI代码解释
复制
其中str参数是要分割处理的字符串,delim是分割符号。当一个StringTokenizer对象生成后,可以通过nextToken()方法便可以得到下一个分割的字符串。通过hasMoreTokens()方法可以得到是否有更多的子字符串需要处理。使用StringTokenizer完成上述例子中的分割任务:
```java

long start2 = System.currentTimeMillis();

代码语言:txt
AI代码解释
复制
    StringTokenizer stringTokenizer = new StringTokenizer(str, ";");
代码语言:txt
AI代码解释
复制
    for (int i = 0; i < 10000; i++){
代码语言:txt
AI代码解释
复制
        while (stringTokenizer.hasMoreTokens()) {
代码语言:txt
AI代码解释
复制
            stringTokenizer.nextToken();
代码语言:txt
AI代码解释
复制
        }
代码语言:txt
AI代码解释
复制
    }
代码语言:txt
AI代码解释
复制
    stringTokenizer = new StringTokenizer(str, ";");
代码语言:txt
AI代码解释
复制
    System.out.println("StringTokenizer对str进行10000次分割用时cost:" + (System.currentTimeMillis() - start2));
代码语言:txt
AI代码解释
复制
运行结果:
```java

StringTokenizer对str进行10000次分割用时cost:13

Process finished with exit code 0

代码语言:txt
AI代码解释
复制
同样在我的计算机上,使用StringTokenizer分割用时只有13毫秒。即使在这段代码中StringTokenizer对象被不断创建并销毁,但其效率仍然明显高于split()方法。

##### 更优化字符串分割方法
字符串分割是否还能有继续优化的余地呢?有的!那就是自己手动完成字符串分割的算法。为了完成这个算法,我们需要使用String的两个方法indexOf()和subString().在前面我们提到过,subString()是采用时间换取空间技术,因此它的执行速度相对会很快,只要处理好内存溢出问题,便可大胆使用。
而indexOf()函数是一个执行速度非常快的方法,它的原型如下:
```java

public int indexOf(int ch)

代码语言:txt
AI代码解释
复制
它返回指定字符在String对象中的位置。
下面我们开始完成自定义分割算法,并同样对str对象进行处理。方法同样执行1万次。
```java

long start = System.currentTimeMillis();

代码语言:txt
AI代码解释
复制
    for (int i = 0; i < 2; i++){
代码语言:txt
AI代码解释
复制
        while (true){
代码语言:txt
AI代码解释
复制
            String splitStr = null;
代码语言:txt
AI代码解释
复制
            // 找分割符的位置
代码语言:txt
AI代码解释
复制
            int j = str.indexOf(";");
代码语言:txt
AI代码解释
复制
            // 没有分隔符存在
代码语言:txt
AI代码解释
复制
            if (j<0)
代码语言:txt
AI代码解释
复制
                break;
代码语言:txt
AI代码解释
复制
            // 找到分隔符,截取字符串
代码语言:txt
AI代码解释
复制
            splitStr = str.substring(0, j);
代码语言:txt
AI代码解释
复制
            // 剩下需要处理的字符串
代码语言:txt
AI代码解释
复制
            str = str.substring(j+1);
代码语言:txt
AI代码解释
复制
        }
代码语言:txt
AI代码解释
复制
    }
代码语言:txt
AI代码解释
复制
    System.out.println("自定义分割算法对str进行10000次分割用时cost:" + (System.currentTimeMillis() - start));
代码语言:txt
AI代码解释
复制
执行结果:
```java

自定义分割算法对str进行10000次分割用时cost:2

Process finished with exit code 0

代码语言:txt
AI代码解释
复制
看到我们自定义的分割算法耗时只用了2毫秒。这也说明了indexOf()和subString()执行速度非常之快,很适合作为高频函数使用。

#### 高效率的charAt()方法
-----------------------------------------------
在上述例子中indexOf()方法是用于检索字符串中index位置的字符,它具有很高的效率,String还提供了一个同样高效,但是作用相反的方法,通过位置检索字符:charAt(),其原型如下:
```java

public char charAt(int index)

代码语言:txt
AI代码解释
复制
在软件开发过程中,经常会遇到这样的问题:判断一个字符串的开始和结束子串是否等于某个字串。如:判断字符串str,是否以“Java”开头,以“JDK”结尾。
判断以什么开头:
```java

public boolean startsWith(String prefix)

代码语言:txt
AI代码解释
复制
判断以什么结尾:
```java

public boolean startsWith(String prefix)

代码语言:txt
AI代码解释
复制
但即使是这样的内置函数,其效率也是远远低于charAt()方法的。来验证下:
```java

public static void main(String[] args) {

代码语言:txt
AI代码解释
复制
    // 初始化一个大字符串
代码语言:txt
AI代码解释
复制
    String str = null;
代码语言:txt
AI代码解释
复制
    StringBuffer sb = new StringBuffer();
代码语言:txt
AI代码解释
复制
    for (int i = 0; i < 100000; i++){
代码语言:txt
AI代码解释
复制
        sb.append(i);
代码语言:txt
AI代码解释
复制
    }
代码语言:txt
AI代码解释
复制
    str = sb.toString();
代码语言:txt
AI代码解释
复制
    // 使用startWith() & endWith()
代码语言:txt
AI代码解释
复制
    long start1 = System.currentTimeMillis();
代码语言:txt
AI代码解释
复制
    for (int i=0; i <100000; i++){
代码语言:txt
AI代码解释
复制
        str.startsWith("Java");
代码语言:txt
AI代码解释
复制
        str.endsWith("Jdk");
代码语言:txt
AI代码解释
复制
    }
代码语言:txt
AI代码解释
复制
    System.out.println("使用startWith() & endWith()执行100000用时cost:" + (System.currentTimeMillis() - start1));
代码语言:txt
AI代码解释
复制
    // 使用charWith()方法
代码语言:txt
AI代码解释
复制
    long start2 = System.currentTimeMillis();
代码语言:txt
AI代码解释
复制
    int len = str.length();
代码语言:txt
AI代码解释
复制
    for (int i=0; i <100000; i++){
代码语言:txt
AI代码解释
复制
        // charAt()判断是否“Java”开头
代码语言:txt
AI代码解释
复制
        if (str.charAt(0) == 'J' && str.charAt(1) == 'a' && str.charAt(2) == 'v' && str.charAt(3) == 'a' );
代码语言:txt
AI代码解释
复制
        // charAt()判断是否“Jdk”结尾
代码语言:txt
AI代码解释
复制
        if (str.charAt(len-1) == 'J' && str.charAt(len-2) == 'd' && str.charAt(len-3) == 'k');
代码语言:txt
AI代码解释
复制
    }
代码语言:txt
AI代码解释
复制
    System.out.println("使用charWith()执行100000用时cost:" + (System.currentTimeMillis() - start2));
代码语言:txt
AI代码解释
复制
}
代码语言:txt
AI代码解释
复制
执行结果:
```java

使用startWith() & endWith()执行100000用时cost:13

使用charWith()执行100000用时cost:6

Process finished with exit code 0

代码语言:txt
复制

可以看出来,依然是charAt()的效率更高些。

总结

不论选择哪一种实现,对功能来说都是可以满足的。但是如果需要考虑性能问题,那么就需要我们开学人员在我们自己的业务场景下选择更优的实现方案。

更多个人学习笔记


https://www.relaxheart.cn/to/master/blog

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
理解Task和和async await
本文将详解C#类当中的Task,以及异步函数async await和Task的关系
ryzenWzd
2020/11/12
2.4K0
理解Task和和async await
C#多线程和异步(二)——Task和async/await详解
  同步和异步主要用于修饰方法。当一个方法被调用时,调用者需要等待该方法执行完毕并返回才能继续执行,我们称这个方法是同步方法;当一个方法被调用时立即返回,并获取一个线程执行该方法内部的业务,调用者不用等待该方法执行完毕,我们称这个方法为异步方法。
zls365
2021/02/26
7.2K0
Thread、ThreadPool、Task、Parallel、Async和Await基本用法、区别以及弊端
ThreadPool是Thread的一个升级版,ThreadPool是从线程池中获取线程,如果线程池中又空闲的元素,则直接调用,如果没有才会创建,而Thread则是会一直创建新的线程,要知道开启一个线程就算什么事都不做也会消耗大约1m的内存,是非常浪费性能的,接下来我们写一个例子来看一下二者的区别:
AI.NET 极客圈
2019/08/14
1.8K0
C#学习笔记 线程同步
多个线程同时操作一个数据的话,可能会发生数据的错误。这个时候就需要进行线程同步了。线程同步可以使用多种方法来进行。下面来逐一说明。本文参考了《CLR via C#》中关于线程同步的很多内容。
乐百川
2022/05/05
5850
.NET 各版本多线程使用原理与实践
多线程编程是现代应用程序开发中的核心技术,尤其是在需要并发处理或提升性能的场景中。本文将以 .NET 各版本为背景,详细探讨多线程技术的发展、底层原理以及实践方法。
Michel_Rolle
2024/11/19
2.1K0
C#13中线程同步的作用域锁
C# 13 引入了新的功能,旨在让编码变得更简单、更高效。其中的一个亮点是通过 System.Threading.Lock 类引入的作用域锁功能。这让线程同步变得更加简单,并减少了多线程程序中的错误。
郑子铭
2025/02/18
650
C#13中线程同步的作用域锁
异步与多线程——c#
异步这概念刚开始接触的时候,不是那么容易接受,但是需要用的地方还真的挺多的,刚学习的时候,也很懵逼走了不少弯路,所以这里有必要总结一下。 msdn文档:https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/ 官方的简介: *.NET Framework提供了执行异步操作的三种模式: 异步编程模型(APM)模式(也称为IAsyncResult的模式),其中异步操作要求Begin和End方法(例如,BeginWrite和EndWrite异步写入操作)。这种模式不再被推荐用于新开发。有关更多信息,请参阅异步编程模型(APM)。
vv彭
2021/01/07
1.7K0
异步与多线程——c#
【深入浅出C#】章节 9: C#高级主题:多线程编程和并发处理
多线程编程和并发处理的重要性和背景 在计算机科学领域,多线程编程和并发处理是一种关键技术,旨在充分利用现代计算机系统中的多核处理器和多任务能力。随着计算机硬件的发展,单一的中央处理单元(CPU)已经不再是主流,取而代之的是多核处理器,这使得同时执行多个任务成为可能。多线程编程允许开发人员将一个程序拆分成多个线程,这些线程可以并行执行,从而提高程序的性能和响应速度。 为什么多线程在现代应用中至关重要?
喵叔
2023/08/26
4.9K0
C# 多线程编程入门教程
随着硬件性能的提升,尤其是多核CPU的广泛应用,多线程编程已经成为现代软件开发中的核心技能之一。多线程可以让程序在多个核心上并发运行,提高效率和性能。然而,编写多线程程序并不是一件简单的事情,尤其是要处理线程间的同步问题,以避免数据竞争和死锁等问题。
Michel_Rolle
2024/09/23
2.6K0
C#学习笔记 线程同步问题
生产者消费者问题大体是这样的:有几个生产者和几个消费者,共享一个缓冲区。生产者会向缓冲区中添加数据;消费者会从缓冲区中将数据取走。需要处理这两者之间的同步问题。
乐百川
2022/05/05
3600
.NET Thread、Task或Parallel实现多线程的使用总结
多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
郑子铭
2023/08/30
3360
.NET Thread、Task或Parallel实现多线程的使用总结
浅谈.Net异步编程的前世今生----TPL篇
我们在此前已经介绍了APM模型和EAP模型,以及它们的优缺点。在EAP模型中,可以实时得知异步操作的进度,以及支持取消操作。但是组合多个异步操作仍需大量工作,编写大量代码方可完成。
独立观察员
2022/12/06
4710
浅谈.Net异步编程的前世今生----TPL篇
C# dotnet 自己实现一个线程同步上下文
昨天鹏飞哥问了我一个问题,为什么在控制台程序的主线程等待某个线程执行完成之后回来,是在其他线程执行的。而 WPF 在等待某个线程执行完成之后,可以回到主线程执行。其实这是因为在 WPF 和 WinForms 和 ASP.NET 框架里面都自己实现了线程同步上下文,通过线程同步上下文做到调度线程执行。本文就来和小伙伴聊一下如何自己实现一个线程同步上下文
林德熙
2020/05/12
1.1K0
C# 中的线程与任务 — 有什么区别?
在C#编程中,类(class)是一种让我们可以同时执行任务的方式,允许我们在程序的其他部分继续运行时执行代码。尽管现代C#开发人员通常使用Task来管理并发性,但Thread类提供了更多的线程行为控制,这使得它在需要进行低级别线程操作时非常有用。
郑子铭
2025/01/07
1760
C# 中的线程与任务 — 有什么区别?
为什么 Random.Shared 是线程安全的
在多线程环境中使用 Random 类来生成伪随机数时,很容易出现线程安全问题。例如,当多个线程同时调用 Next 方法时,可能会出现种子被意外修改的情况,导致生成的伪随机数不符合预期。
newbe36524
2023/08/23
2830
c#线程-线程同步
如果有多个线程同时访问共享数据的时候,就必须要用线程同步,防止共享数据被破坏。如果多个线程不会同时访问共享数据,可以不用线程同步。 线程同步也会有一些问题存在: 1、性能损耗。获取,释放锁,线程上下文建切换都是耗性能的。 2、同步会使线程排队等待执行。
苏州程序大白
2021/08/13
7700
从 ThreadLocal 到 AsyncLocal
前些天跟大佬们在群里讨论如何在不使用构造函数,不增加方法参数的情况下把一个上下文注入到方法内部使用,得出的结论是 AsyncLocal 。感叹自己才疏学浅,居然才知道有 AsyncLocal 这种神器。于是赶紧恶补一下。
MJ.Zhou
2021/11/18
5540
C#多线程(14):任务基础②
上一篇,我们学习了任务的基础,学会多种方式场景任务和执行,异步获取返回结果等。上一篇讲述的知识比较多,这一篇只要是代码实践和示例操作。
痴者工良
2021/04/26
7390
使用C#封装一个多线程测试工具
这个工具可以帮助开发者测试多线程程序的性能、线程安全性和并发问题。我们将实现以下功能:
软件架构师Michael
2025/01/24
1080
[C#] Task.CompletedTask和Task.Result什么时候用?
在学习C#中的Task方法时,可以知道Task启动一个异步线程方法可以用Task.Run()进行,具体可以参看附录部分。
科控物联
2023/09/01
2.2K0
[C#] Task.CompletedTask和Task.Result什么时候用?
相关推荐
理解Task和和async await
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档