C# 试题 45:C# 13 新特性
0881 以下代码输出什么?
难度: 实战
static void Print(params IEnumerable<int> xs)
{
Console.WriteLine(xs.Count());
}
Print();
Print(1, 2, 3);
- A. 编译失败,
params不能用于IEnumerable<T>类型 - B. 输出 0 和 3,无参调用时编译器构造一个空集合传给
params IEnumerable<int> - C. 第一行抛
NullReferenceException,第二行输出 3 - D. 输出 1 和 3,无参调用时使用默认参数 1
查看答案与解析
正确答案B
解析: C# 13 的 params 集合扩展允许 params 使用 IEnumerable<T>、Span<T> 等类型。无实参调用时编译器构造一个空集合(对 IEnumerable<T> 生成空数组),因此第一行输出 0,第二行输出 3。前提是这些调用点行为在 C# 13 及对应运行时下成立。
关键点: params 不再只允许数组,接口类型的空调用会构造空集合。
0882 以下代码哪一行编译失败?
难度: 进阶
static void Sum(params Span<int> xs)
{
Console.WriteLine(xs.Length);
}
int[] arr = { 1, 2, 3 };
List<int> list = new() { 1, 2 };
Sum(1, 2); // ①
Sum(arr); // ②
Sum(list); // ③
- A. 三行都能编译,分别输出 2、3、2
- B. ② 编译失败:数组不能传给
params Span<int> - C. ③ 编译失败:
List<int>既不能隐式转换为Span<int>,也不满足逐元素展开的条件 - D. ① 编译失败:
params Span<int>只能用已存在的Span实例调用
查看答案与解析
正确答案C
解析: params Span<int> 在调用点用集合表达式构造(Sum(1, 2) 走栈上分配,Sum(arr) 依赖 int[] → Span<int> 的隐式转换),但 List<int> 到 Span<int> 没有隐式转换,作为单实参也不可展开,故 ③ 编译失败。params 集合的参数形态直接影响调用点能否构造。
关键点: params Span<T> 不接受任意集合类型,只有能构造或能隐式转换的实参才合法。
0883 以下代码输出什么?
难度: 进阶
static void M(params int[] xs) => Console.WriteLine("array");
static void M(params IEnumerable<int> xs) => Console.WriteLine("ienum");
M(new List<int> { 1, 2 });
- A. 输出
ienum:List<int>作为单个实参按非展开形式直接转换为IEnumerable<int> - B. 输出
array:params int[]重载优先级更高 - C. 编译失败:两个
params重载存在歧义 - D. 运行抛
InvalidCastException
查看答案与解析
正确答案A
解析: 调用 M(new List<int>{1,2}) 时,数组重载无法把 List<int> 转成 int[],而 IEnumerable<int> 重载可用非展开形式直接接收该实参(引用转换,不逐元素复制),因此命中第二个重载。展开形式的转换仅在需要时才参与比较。
关键点: 传集合变量可能走非展开形式,直接把引用交给 IEnumerable<T> 参数。
0884 把自定义集合类型用作 params 集合参数时,哪项要求正确?
难度: 进阶
- A. 只要实现
IEnumerable<T>即可,与构造能力无关 - B. 必须派生自
List<T>或Collection<T> - C. 只有
IEnumerable<T>和Span<T>能作params,自定义类型一律不行 - D. 除
IEnumerable<T>/Span<T>有特判外,具体集合类型需满足集合表达式的要求:可访问的无参构造函数与Add方法,调用点据此构造实例
查看答案与解析
正确答案D
解析: params 集合的实参构造与集合表达式是同一套机制:接口类型和 Span 由编译器特判(生成数组或栈上 Span),具体类型则要求存在适用的无参构造函数与 Add 方法。缺少任一条件,调用点无法构造,代码不能编译。这一约束正是“调用点构造成本取决于目标集合和实参形态”的体现。
关键点: 自定义 params 集合 = 可构造 + 可 Add,不是“实现了接口就行”。
0885 以下代码中编译器为 lock 语句生成什么?
难度: 实战
Lock gate = new();
lock (gate)
{
Console.WriteLine("critical");
}
- A. 编译失败,
System.Threading.Lock是值类型 - B. 对已知的
Lock类型生成EnterScope()与Scope.Dispose()的专用作用域模式 - C. 与锁定
object一样生成Monitor.Enter/Monitor.Exit - D. 编译通过,但运行抛
SynchronizationLockException
查看答案与解析
正确答案B
解析: C# 13 中 lock 语句识别目标是否为 System.Threading.Lock:是则调用 EnterScope() 并在 finally 中 Dispose(),不再走 Monitor 的 Enter/Exit。只有把 Lock 换成其他类型(如 object)时才会退回通用 Monitor 代码并产生警告。
关键点: lock 对 Lock 类型生成专用代码,这是 C# 13 的性能改进点。
0886 以下代码能否编译?
难度: 实战
Lock gate = new();
lock (gate)
{
await Task.Yield();
}
- A. 编译失败 CS1996:
lock语句体内不能使用await - B. 编译通过,
Lock专门支持跨await保持临界区 - C. 编译通过,但运行抛
SynchronizationLockException - D. 编译通过,
await被编译器忽略
查看答案与解析
正确答案A
解析: 无论锁对象是 object 还是 Lock,C# 编译器都禁止在 lock 语句体内 await(CS1996)。锁由线程持有,await 后的续体可能跑在别的线程上,跨 await 持锁本身就不成立,Lock 类型也不例外。
关键点: “换用 Lock 就能跨 await 持锁”是常见误解。
0887 以下代码会产生什么编译诊断?
难度: 进阶
object gate = new Lock();
lock (gate)
{
// ...
}
- A. 编译失败,
Lock不能赋给object - B. 编译通过且无警告,行为与直接锁定
Lock变量完全一致 - C. 产生警告 CS9216:
Lock值被转换为其他类型后,lock走基于Monitor的通用锁定 - D. 运行抛
TypeInitializationException
查看答案与解析
正确答案C
解析: 编译器只有在 lock 目标的静态类型恰好是 System.Threading.Lock 时才生成专用代码;一旦被赋给 object,目标类型变成 object,只能退回 Monitor 语义,并发出 CS9216 警告提示“转换类型会导致可能非预期的基于 Monitor 的锁定”。运行时行为仍是可用的互斥,但拿不到 Lock 的专用路径。
关键点: 把 Lock 装箱成 object 等于放弃了专用锁语义。
0888 以下代码输出什么?
难度: 实战
Lock gate = new();
using (gate.EnterScope())
{
Console.WriteLine("in scope");
}
- A. 编译失败,
EnterScope只能配合lock语句使用 - B. 运行抛
SynchronizationLockException - C. 输出
in scope,但退出using后锁不会释放 - D. 输出
in scope:EnterScope()返回的ref structScope支持Dispose(),using结束时退出临界区
查看答案与解析
正确答案D
解析: Lock.EnterScope() 返回一个实现了 Dispose() 的 ref struct Lock.Scope,配合 using 在异常时也能释放,等价于 lock 语句生成的代码。Enter/TryEnter/Exit 是另一套需手写 try/finally 的 API,二者不要混用。
关键点: EnterScope() + using 是 Lock 的推荐编程模型之一。
0889 以下代码哪一行编译失败?
难度: 进阶
interface IGet { int Get(); }
ref struct R : IGet
{
public int Get() => 42;
}
IGet g = new R(); // ①
- A. ① 编译失败 CS0029:
ref struct不能装箱为接口引用,R无法转换为IGet - B. 编译通过,
g.Get()输出 42 - C. 运行抛
InvalidCastException - D. ① 之前就编译失败:
ref struct不允许实现接口
查看答案与解析
正确答案A
解析: C# 13 允许 ref struct 实现接口,但接口类型的变量是引用,把 R 赋给 IGet 需要装箱,而 ref struct 禁止装箱,因此编译失败(CS0029)。实现接口只意味着能参与带 allows ref struct 约束的泛型或 ref 安全上下文,不等于可以当作普通引用类型使用。
关键点: ref struct 实现接口 ≠ 可以装箱成接口引用。
0890 以下代码输出什么?
难度: 实战
interface IGet { int Get(); }
ref struct R : IGet
{
public int Get() => 42;
}
static int Use<T>(T t) where T : IGet, allows ref struct => t.Get();
Console.WriteLine(Use(new R()));
- A. 编译失败,
ref struct不能作为泛型实参 - B. 输出 42:
allows ref struct反约束允许ref struct实参,接口调用不发生装箱 - C. 运行抛
NotSupportedException - D. 编译失败,
allows ref struct不是合法语法
查看答案与解析
正确答案B
解析: allows ref struct 是 C# 13 的反约束,显式允许 ref struct 作为类型实参,同时禁止该类型实参被装箱。泛型内部通过接口约束调用 t.Get() 走约束调用(constrained call),不会装箱,因此输出 42。去掉 allows ref struct 则会报 CS9244。
关键点: 让 ref struct 参与泛型必须显式声明 allows ref struct。
0891 以下代码能否编译?
难度: 进阶
interface IGet { int Get(); }
ref struct R : IGet
{
public int Get() => 42;
}
var list = new List<R>();
- A. 编译通过,泛型容器可以存放
ref struct - B. 编译通过,但运行抛异常
- C. 编译失败 CS9244:
ref struct不能作为普通泛型类型实参 - D. 编译失败,但换用
R[]数组也不行
查看答案与解析
正确答案C
解析: List<T> 没有 allows ref struct 声明,不能接收 ref struct 实参(CS9244)。这与方法级泛型同理:只有显式声明 allows ref struct 的泛型才能接纳 ref struct。数组例外——R[] 是合法的,因为数组不是需要装箱的泛型容器。
关键点: “泛型容器放不下 ref struct”适用于所有未声明反约束的泛型类型。
0892 关于 ref struct 实现接口后的使用边界,哪项正确?
难度: 进阶
- A. 实现接口后即可像普通引用类型一样装箱、存字段
- B. 接口成员必须全部是静态成员才能被实现
- C. 必须在
unsafe上下文中才能实现接口 - D. 实现接口后仍受
ref安全规则约束:不能装箱、不能作普通泛型实参,泛型场景需显式allows ref struct
查看答案与解析
正确答案D
解析: ref struct 实现接口只是“契约能力”的扩展,运行时的 ref 安全限制不变:任何需要装箱的用法(转 object、转接口引用、进未声明的泛型容器)都会编译失败。需要泛型支持时必须在类型参数上写 allows ref struct。
关键点: 接口实现不豁免 ref struct 的装箱禁令。
0893 以下代码能否编译?
难度: 进阶
static async Task<int> M()
{
Span<int> s = stackalloc int[2];
s[0] = 42;
await Task.Yield();
Console.WriteLine(s[0]);
return 0;
}
- A. 编译失败,async 方法中不能声明任何
ref struct局部变量 - B. 编译失败 CS4007:
Span<int>类型的实例不能跨await边界保留 - C. 编译通过并输出 42
- D. 运行抛
InvalidOperationException
查看答案与解析
正确答案B
解析: C# 13 起 async 方法可以声明 ref struct 局部变量,但变量不能跨 await 边界存活。s 在 await 之后仍被访问,编译器判定其必须进入状态机,而 ref struct 不能成为状态机字段,故报 CS4007。栈上分配的 Span 本身就随方法栈帧消失,跨 await 使用在语义上也不安全。
关键点: 允许声明 ≠ 允许跨 await 使用。
0894 以下代码输出什么?
难度: 实战
static IEnumerable<int> Get()
{
int x = 5;
ref int r = ref x;
yield return r;
}
Console.WriteLine(Get().First());
- A. 输出 5:C# 13 起迭代器方法允许声明
ref局部变量 - B. 编译失败,迭代器中禁止使用
ref局部变量 - C. 编译失败,
yield与ref不能共存于同一方法 - D. 运行抛
InvalidOperationException
查看答案与解析
正确答案A
解析: C# 13 允许迭代器与 async 方法声明 ref 局部变量和 ref struct 局部变量。这里 r 是 x 的别名,在 yield return r 时求值返回 5;由于 r 没有跨 yield 边界使用,编译通过。若在 yield 之后仍引用 r,则与跨 await 一样报错。
关键点: 迭代器内 ref 局部可用,但不能跨 yield 边界存活。
0895 以下代码能否编译?
难度: 进阶
unsafe IEnumerable<int> M()
{
unsafe
{
int* p = stackalloc int[1];
yield return 1;
}
}
- A. 编译通过并输出 1
- B. 编译失败,迭代器方法不能标记
unsafe - C. 编译失败 CS9238:
yield return不能出现在unsafe块中 - D. 运行抛
AccessViolationException
查看答案与解析
正确答案C
解析: C# 13 允许迭代器方法带 unsafe 上下文并使用指针,但规定所有 yield return/yield break 必须位于安全上下文。把 yield 写进 unsafe 块会报 CS9238。正确写法是让 yield 语句留在安全区域,指针操作放在 unsafe 块内。
关键点: 迭代器可 unsafe,但 yield 语句必须在安全上下文。
0896 以下代码输出什么?
难度: 进阶
static async Task<int> M()
{
Span<int> s = stackalloc int[2];
s[0] = 42;
await Task.Yield();
return 1;
}
Console.WriteLine(M().Result);
- A. 编译失败 CS4007,
Span不能出现在 async 方法中 - B. 编译失败,
await之前不能分配Span - C. 输出 1,但这属于未定义行为
- D. 输出 1:
s在await之后不再被访问,编译器允许其存活到挂起点之前
查看答案与解析
正确答案D
解析: s 在 await Task.Yield() 之前最后一次被使用,之后不再引用,因此不需要把 s 保存进状态机,编译通过并输出 1。与 0893 对比:跨 await 使用 Span 报错,不跨则合法——编译器按逃逸分析精确判定。
关键点: 逃逸分析决定 ref struct 局部能否出现在 async 方法中。
0897 以下代码能否编译?
难度: 进阶
static int F(int x) => x;
static T F<T>(T t) => t;
var f = F;
Console.WriteLine(f(1));
- A. 编译通过:无类型实参的泛型候选被剪枝,自然类型由
F(int)决定 - B. 编译失败 CS8917,方法组无法推断委托类型
- C. 编译通过,
f是泛型方法 - D. 运行抛
AmbiguousMatchException
查看答案与解析
正确答案A
解析: C# 13 的方法组自然类型改进按作用域剪枝候选:var f = F; 未提供类型实参,泛型重载 F<T> 被剪枝,剩余非泛型候选 F(int) 唯一,方法组得到自然类型 Func<int,int>,因此能编译。剪枝使原本会失败或歧义的方法组获得自然类型。
关键点: 泛型候选在无类型实参时被剪枝,是 C# 13 的改进点。
0898 以下代码能否编译?
难度: 进阶
static T F<T>(T t) => t;
var f = F;
- A. 编译通过,推断为
Func<T, T> - B. 编译失败 CS8917:泛型候选在无类型实参时被剪枝,剩余候选为空,无法推断委托类型
- C. 编译通过,推断为
Action<T> - D. 编译通过,
f的类型在运行时确定
查看答案与解析
正确答案B
解析: 剪枝规则把没有提供类型实参的泛型候选全部去掉,而该方法组只剩泛型候选,剪枝后为空,方法组没有自然类型,报 CS8917。需要显式目标类型:Func<int, int> f = F; 或直接给出类型实参的调用。
关键点: 剪枝也可能把候选“剪空”,此时必须显式标注委托类型。
0899 以下代码能否编译?
难度: 实战
var write = Console.Write;
- A. 编译通过,推断为
Action<string> - B. 编译通过,推断为
Action<object> - C. 编译失败 CS8917:
Console.Write的重载参数签名各不相同,剪枝后仍无唯一签名,方法组没有自然类型 - D. 运行抛
MissingMethodException
查看答案与解析
正确答案C
解析: Console.Write 有大量参数不同的重载,且都不是泛型方法,剪枝不适用;这些候选没有共同签名,方法组无自然类型,var 无法推断,报 CS8917。C# 13 的改进只解决“可剪枝”的场景,不代表所有方法组都能 var。
关键点: 方法组自然类型不是“万能推断”,重载签名不同仍需显式委托类型。
0900 关于 C# 13「方法组自然类型」改进的核心规则与边界,哪项正确?
难度: 进阶
- A. C# 13 之后任意方法组都可以用
var推断 - B. 剪枝只看候选方法的返回类型
- C. 该方法组自然类型改进只对扩展方法生效
- D. 编译器按作用域逐步剪枝候选(无类型实参的泛型方法、约束不满足的候选),使更多方法组获得自然类型;但签名不同的重载仍可能要求显式委托类型
查看答案与解析
正确答案D
解析: 改进的核心是“作用域化 + 剪枝”:先在当前作用域构造候选集,剔除不可能成功的候选(泛型无实参、约束不满足、arity 不匹配等),再按扩展方法作用域逐层推进,更贴近普通重载决议。它扩大了可推断范围,但没有取消“候选签名必须一致”这一底线。
关键点: 剪枝扩大适用范围,显式委托类型仍是兜底手段。