返回题库高级 .NET 刷题C# 选择题 · 第 35 / 50 篇

C# 试题 35:ValueTask 与异步流

0681 关于返回 ValueTask<int> 与返回 Task<int> 的选择,哪项正确?

难度: 进阶

  • A. 只要方法经常同步完成,返回 ValueTask 就一定比 Task 快,公共 API 也应默认使用
  • B. ValueTask 是引用类型,可以安全地多次 await
  • C. ValueTask 能省去高频同步完成路径上的 Task 分配,但它是值类型、体积更大且只能消费一次;公共 API 默认返回 Task 通常更简单,改用 ValueTask 需要测量证明收益
  • D. ValueTask 只能包装已经同步完成的结果,无法包装真正的异步操作
查看答案与解析

正确答案C

解析: ValueTask<T> 是结构体,通常比 Task 引用更大,且契约上只能消费一次;它在高频同步完成路径上避免 Task 分配,但约束更多。公共 API 默认返回 Task 更简单,只有测量证明分配敏感时才值得换用 ValueTask

关键点: ValueTask 用“更省分配”换“更多约束”,公共 API 默认 Task。

0682 以下代码哪项正确?

难度: 实战

public ValueTask<int> GetAsync(bool hit)
{
    if (hit) return new ValueTask<int>(42);
    return LoadAsync();
}

static async Task<int> LoadAsync()
{
    await Task.Delay(10);
    return 7;
}
  • A. 编译通过;hit 为 true 时返回包装同步结果 42 的 ValueTask,await 时不需要分配 Task
  • B. 编译失败:两个 return 分支类型不一致
  • C. 编译失败:ValueTask<int> 不能从 Task<int> 隐式转换
  • D. 编译通过,但 hit 分支 await 时仍会分配 Task
查看答案与解析

正确答案A

解析: new ValueTask<int>(42) 是同步结果构造;Task<int>ValueTask<int> 存在隐式转换,return LoadAsync() 合法。hit 分支完全同步,等待该 ValueTask 不产生 Task 分配。

关键点: 隐式转换 + 同步结果构造是 ValueTask 省分配的两个前提。

0683 以下代码哪项正确?

难度: 实战

static ValueTask<string> Describe(bool flag) =>
    flag ? new ValueTask<string>("ok") : FetchAsync();

static async Task<string> FetchAsync()
{
    await Task.Yield();
    return "fetched";
}
  • A. 编译失败:条件运算符要求两个分支类型完全一致
  • B. 编译失败:async 方法不能返回 ValueTask
  • C. 编译通过,但 flag 为 true 时也会先分配 Task 再包装
  • D. 编译通过;两个分支统一为 ValueTask<string>flag 为 true 时同步返回,不分配 Task
查看答案与解析

正确答案D

解析: Task<string> 隐式转换为 ValueTask<string>,条件运算符据此把公共类型确定为 ValueTask<string>new ValueTask<string>("ok") 走同步结果路径,await 时零 Task 分配。

关键点: 条件运算符允许一侧类型隐式转换为另一侧,公共类型取目标类型。

0684 以下代码哪项正确?

难度: 进阶

async Task<int> Demo()
{
    ValueTask<int> vt = GetAsync();
    int first = await vt;
    int second = await vt;
    return first + second;
}
  • A. 编译失败:ValueTask 声明为局部变量后不允许 await
  • B. 编译通过;契约上 ValueTask 只能消费一次,第二次 await 依赖底层实现(Task 支撑时通常仍能工作,IValueTaskSource 支撑时可能抛异常或得到错误结果)
  • C. 编译通过且总是返回两倍结果,ValueTask 可安全多次 await
  • D. 编译失败:await 必须紧跟异步方法调用
查看答案与解析

正确答案B

解析: 编译器不阻止重复 await 值类型,但 ValueTask 的契约是“一个实例只消费一次”。AsyncValueTaskMethodBuilder 等 IValueTaskSource 实现会检测重复消费并抛异常,因此把 ValueTask 存下来二次 await 是典型误用。

关键点: “能编译”不等于“符合契约”,ValueTask 只能消费一次。

0685 以下代码哪项正确?

难度: 进阶

async Task<int> Demo()
{
    ValueTask<int> vt = GetAsync();
    Task<int> t1 = vt.AsTask();
    Task<int> t2 = vt.AsTask();
    return await t1 + await t2;
}
  • A. 编译失败:调用 AsTask 后不能再使用 vt
  • B. 一定抛 InvalidOperationException
  • C. 行为取决于底层实现:Task 支撑时两次 AsTask 拿到同一 Task,IValueTaskSource 支撑时第二次 AsTask 会抛 InvalidOperationException;契约上 ValueTask 只能消费一次
  • D. 编译通过且总是返回正确结果
查看答案与解析

正确答案C

解析: AsTask() 属于消费操作。Task 支撑的实例直接返回内部 Task,重复调用无害;IValueTaskSource 支撑的实例首次调用后缓存,第二次调用抛 InvalidOperationException。因此不能依赖“一定成功”。

关键点: AsTask 也是消费,重复消费行为依底层实现而定。

0686 以下代码哪项正确?

难度: 进阶

ValueTask<int> vt = GetAsync();
int x = await vt;
int y = vt.Result;   // await 之后再次消费
Console.WriteLine(x + y);
  • A. 编译失败:Result 与 await 不能混用
  • B. 一定输出两倍值
  • C. 一定抛 InvalidOperationException
  • D. 行为取决于底层实现:IValueTaskSource 支撑时,await 后再读 Result 会抛 InvalidOperationException;契约上每个实例只能消费一次
查看答案与解析

正确答案D

解析:Result 同样是消费操作。Task 支撑的实例重复读取通常返回相同值;IValueTaskSource 支撑的实例第二次获取结果会抛异常。不要在 await 之后再次通过 Result/GetResult 消费同一个 ValueTask。

关键点: await、Result、GetResult、AsTask 都属于“消费”,只能发生一次。

0687 以下代码的结果是什么?

难度: 实战

static async ValueTask<IEnumerable<int>> RangeAsync(int n)
{
    for (int i = 0; i < n; i++)
        yield return i;
}
  • A. 编译失败:含 yield 的 async 方法必须返回 IAsyncEnumerable<T>(异步迭代器),不能返回 ValueTask
  • B. 编译通过,返回惰性异步序列
  • C. 编译通过,但每次迭代都分配新数组
  • D. 运行抛 InvalidOperationException
查看答案与解析

正确答案A

解析: 异步迭代器(async iterator)的返回类型只能是 IAsyncEnumerable<T>IAsyncEnumerator<T>;返回 ValueTask 的方法不能包含 yield。异步流与 ValueTask 是两种不同的异步设施。

关键点: 异步迭代器返回 IAsyncEnumerable,不能返回 ValueTask。

0688 以下代码哪项正确?

难度: 进阶

ValueTask<int> vt = GetAsync();

if (vt.IsCompletedSuccessfully)
    Console.WriteLine("fast");

int result = await vt;
Console.WriteLine(result);
  • A. 编译失败:IsCompletedSuccessfully 只能在 await 后读取
  • B. 读取 IsCompleted/IsCompletedSuccessfully 等属性不属于“消费”,可重复读取;只有 await、Result、GetResult、AsTask 才是消费操作,且只能发生一次
  • C. 读取 IsCompletedSuccessfully 后再 await 必然抛 InvalidOperationException
  • D. 这段代码一定死锁
查看答案与解析

正确答案B

解析: 只读状态属性不会消耗 ValueTask,也不会使底层 IValueTaskSource 失效;真正“消费”的是 await、Result、GetResult、AsTask。先检查完成状态再 await 是允许的,也常用在同步快路径上。

关键点: 区分“读状态”与“消费”,属性可重复读、消费只能一次。

0689 关于 ValueTask 的“同步结果”路径,哪项正确?

难度: 进阶

  • A. ValueTask 只能包装尚未完成的结果
  • B. new ValueTask<int>(result) 会在堆上分配一个新的 Task
  • C. new ValueTask<int>(result) 包装一个已就绪的同步结果,await 该 ValueTask 不再分配 Task;异步结果则通过 Task/IValueTaskSource 支撑
  • D. 同步结果路径与直接返回 Task 完全等价
查看答案与解析

正确答案C

解析: ValueTask<T> 有从结果值构造的重载,把结果内联在结构体字段里,等待时无需 Task;异步结果才由 Task<T>IValueTaskSource<T> 支撑。这正是“同步完成省分配”的机制来源。

关键点: 同步结果内联存储,是 ValueTask 省分配的核心。

0690 以下代码哪项正确?

难度: 实战

static ValueTask<int> Maybe(int x)
{
    if (x == 0) return new ValueTask<int>(0);
    return new ValueTask<int>(Task.FromResult(x));
}
  • A. x == 0 分支是同步结果路径,不创建 Task;x != 0 分支包装显式创建的 Task
  • B. 两个分支都会在堆上分配
  • C. new ValueTask<int>(int) 构造函数不存在,编译失败
  • D. 运行抛异常
查看答案与解析

正确答案A

解析: 两个重载都存在:ValueTask<T>(T result) 内联同步结果,ValueTask<T>(Task<T>) 包装任务。只有 x != 0 分支因 Task.FromResult 产生一个已完成 Task,x == 0 分支零分配。

关键点: 同步结果与 Task 包装是两条不同的构造路径。

0691 以下代码输出什么?

难度: 实战

static async ValueTask<int> GetZero() => 0;

Console.WriteLine(await GetZero());
  • A. 编译失败:async 方法不能返回 ValueTask
  • B. 输出 0
  • C. 运行抛 InvalidOperationException
  • D. 输出 System.Threading.Tasks.ValueTask1[System.Int32]`
查看答案与解析

正确答案B

解析: async 方法可以返回 ValueTask<T>=> 0 表示同步完成,返回包装结果 0 的 ValueTask,await 后输出 0。同步完成时该方法不产生 Task 分配。

关键点: async 方法返回 ValueTask 合法,同步完成的表达式体直接给结果。

0692 以下代码输出什么?

难度: 实战

var vt = new ValueTask<int>(42);
var t = vt.AsTask();

Console.WriteLine(t.IsCompletedSuccessfully);
  • A. False
  • B. 抛 InvalidOperationException:同步结果不能转 Task
  • C. 输出 42
  • D. True:AsTask 把已就绪的同步结果包装为已完成 Task,IsCompletedSuccessfully 为 true
查看答案与解析

正确答案D

解析: 同步结果路径的 ValueTask 调用 AsTask() 会得到已完成的 Task(等价于 Task.FromResult(42)),IsCompletedSuccessfully 为 true。注意 AsTask 本身是消费操作,之后不应再消费该实例。

关键点: AsTask 把 ValueTask 转成可多次 await 的 Task,同时消耗原实例。

0693 以下代码的运行结果是什么?

难度: 实战

static async IAsyncEnumerable<int> Count(CancellationToken token)
{
    for (int i = 0; i < 10; i++)
    {
        yield return i;
        await Task.Delay(100, token);
    }
}

var cts = new CancellationTokenSource();
cts.Cancel();

await foreach (var n in Count(cts.Token))
{
    Console.WriteLine(n);
}
  • A. 抛 OperationCanceledException:带 CancellationToken 参数的异步迭代器,编译器在每次 MoveNextAsync 开始时自动调用 ThrowIfCancellationRequested,token 已取消则第一次迭代即抛
  • B. 输出 0 到 9
  • C. 输出空,正常结束
  • D. 编译失败:异步迭代器不能带 CancellationToken 参数
查看答案与解析

正确答案A

解析: 只要异步迭代器方法声明了 CancellationToken 参数,编译器生成的 MoveNextAsync 会在每次迭代开始时自动执行 token.ThrowIfCancellationRequested()。token 已取消,第一次调用即抛 OperationCanceledException,0 到 9 都不会输出。

关键点: 声明了 token 参数的异步迭代器会自动检查取消,无需手写检查。

0694 以下代码的运行结果是什么?

难度: 实战

static async IAsyncEnumerable<int> Produce(CancellationToken token)
{
    for (int i = 0; i < 5; i++)
    {
        token.ThrowIfCancellationRequested();
        yield return i;
    }
}

var cts = new CancellationTokenSource();
cts.Cancel();

await foreach (var n in Produce(cts.Token).WithCancellation(cts.Token))
{
    Console.WriteLine(n);
}
  • A. 输出 0 到 4
  • B. 输出 0,然后抛 OperationCanceledException
  • C. 第一次 MoveNextAsync 即抛 OperationCanceledException,0 到 4 都不会输出
  • D. 编译失败:同一个 token 不能既传参又用 WithCancellation
查看答案与解析

正确答案C

解析: 方法体内显式 ThrowIfCancellationRequested() 在第一次迭代前执行,token 已取消,异常直接从第一次 MoveNextAsync 抛出。WithCancellation 只是把 token 交给 GetAsyncEnumerator,与参数传 token 不冲突,可同时使用。

关键点: 显式检查与 WithCancellation 可以并用,取消发生在第一次 MoveNextAsync。

0695 以下代码输出什么?

难度: 实战

static async IAsyncEnumerable<int> Produce()
{
    try
    {
        for (int i = 0; i < 100; i++)
        {
            yield return i;
            await Task.Delay(10);
        }
    }
    finally
    {
        Console.WriteLine("cleanup");
    }
}

await foreach (var n in Produce())
{
    Console.WriteLine(n);
    if (n == 1) break;
}
Console.WriteLine("done");
  • A. 0\n1\ndone:break 会跳过异步释放,finally 不执行
  • B. 0\n1\ncleanup\ndone:break 退出时 await foreach 调用 DisposeAsync,迭代器 finally 执行
  • C. 0\n1\ndone\ncleanup
  • D. 抛 OperationCanceledException
查看答案与解析

正确答案B

解析: await foreach 在正常结束、break、return 或异常时都会调用枚举器的 DisposeAsync;异步迭代器生成的 DisposeAsync 会运行其 finally 块。因此 break 后先输出 cleanup,再输出 done。

关键点: break 不跳过异步释放,finally 一定执行。

0696 以下代码输出什么?

难度: 进阶

static async IAsyncEnumerable<int> Produce(CancellationToken token)
{
    try
    {
        for (int i = 0; ; i++)
        {
            await Task.Delay(10, token);
            yield return i;
        }
    }
    finally
    {
        Console.WriteLine("cleanup");
    }
}

var cts = new CancellationTokenSource();
cts.Cancel();

try
{
    await foreach (var n in Produce(cts.Token))
    {
        Console.WriteLine(n);
    }
}
catch (OperationCanceledException)
{
    Console.WriteLine("canceled");
}
  • A. canceled,finally 不执行
  • B. canceled\ncleanup
  • C. 什么都不输出
  • D. cleanup\ncanceled:MoveNextAsync 抛 OperationCanceledException 后,await foreach 的 finally 仍会调用 DisposeAsync,迭代器 finally 先执行
查看答案与解析

正确答案D

解析: await foreach 生成 try/finally,即使 MoveNextAsync 抛异常,finally 中的 DisposeAsync 也会执行,从而运行迭代器自身的 finally(输出 cleanup)。之后异常向外传播,catch 输出 canceled。

关键点: 取消/异常路径同样触发异步释放。

0697 关于异步释放的契约,哪项正确?

难度: 进阶

  • A. await using 在离开作用域(正常退出、return、异常)时调用 DisposeAsync;实现 IAsyncDisposable 或提供可访问 DisposeAsync 方法的类型都可用
  • B. 对象实现了 IAsyncDisposable 后,普通 using 会自动调用 DisposeAsync
  • C. await using 只对实现 IDisposable 的类型有效
  • D. await using 必须显式调用 DisposeAsync,否则不会释放
查看答案与解析

正确答案A

解析: await usingusing 一样支持模式化释放:类型实现 IAsyncDisposable 或提供公开的 DisposeAsync 方法即可。离开作用域时无论正常退出还是抛异常都会调用 DisposeAsync;普通 using 只调用 Dispose

关键点: 异步资源用 await using,释放时机与 using 一致但调用的是 DisposeAsync。

0698 以下代码输出什么?

难度: 实战

class Resource : IAsyncDisposable
{
    public async ValueTask DisposeAsync()
    {
        Console.WriteLine("async dispose");
        await Task.Yield();
    }
}

async Task Demo()
{
    await using var r = new Resource();
    Console.WriteLine("using");
}
  • A. async dispose\nusing
  • B. using:局部声明式 await using 不在作用域结束时释放
  • C. using\nasync dispose
  • D. 编译失败:IAsyncDisposable 不能用于 await using
查看答案与解析

正确答案C

解析: await using var 把释放推迟到所在作用域(Demo 方法体)结束,先输出 using,再异步调用 DisposeAsync 输出 async dispose。异步释放本身可以在内部继续 await。

关键点: 释放发生在作用域退出时,异步释放方法自身可以异步。

0699 以下代码输出什么?

难度: 进阶

class Resource : IDisposable, IAsyncDisposable
{
    public void Dispose() => Console.WriteLine("sync");
    public ValueTask DisposeAsync()
    {
        Console.WriteLine("async");
        return ValueTask.CompletedTask;
    }
}

await using var r = new Resource();
  • A. sync
  • B. async:await using 只调用 DisposeAsync,不会同时调用 Dispose
  • C. sync\nasync
  • D. 编译失败:一个类型不能同时实现两个接口
查看答案与解析

正确答案B

解析: await using 选择异步释放路径,只调用 DisposeAsyncDispose 不会被连带调用。反过来,普通 using 只调用 Dispose。一个类型可以同时实现两个接口,由使用方式决定走哪条路径。

关键点: 用哪种 using 决定调用哪个释放方法,二者不会都执行。

0700 以下代码输出什么?

难度: 实战

class Resource : IAsyncDisposable
{
    public ValueTask DisposeAsync()
    {
        Console.WriteLine("disposed");
        return default;
    }
}

async Task Demo()
{
    var r = new Resource();
    await using (r)
    {
        Console.WriteLine("body");
    }
}
  • A. body:释放被跳过
  • B. disposed\nbody
  • C. 编译失败:await using 的变量必须在语句中声明
  • D. body\ndisposed:await using 可以引用预先声明的变量,离开块时调用 DisposeAsync
查看答案与解析

正确答案D

解析: await using (r) 语句允许使用预先声明的变量,块结束时调用 DisposeAsync,因此先输出 body 再输出 disposed。与 using (r) 的形式对称。

关键点: 预先声明的变量也能用于 await using 语句块。

官方资料

当前分类

C# 选择题

查看全部分类 →
  1. 33C# 试题 33:Task 与 async/await20 题
  2. 34C# 试题 34:取消、异常与上下文20 题
  3. 35C# 试题 35:ValueTask 与异步流20 题
  4. 36C# 试题 36:线程同步20 题
  5. 37C# 试题 37:并行、PLINQ 与 Channel20 题
ESC

输入关键词开始搜索