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

C# 试题 16:泛型约束与型变

0301 以下代码输出什么?

难度: 实战

static T Make<T>() where T : new() => new T();
Console.WriteLine(Make<DateTime>() == default);
  • A. 编译通过,输出 True,结构体天然满足 new() 约束
  • B. 编译通过,输出 False
  • C. 编译失败,DateTime 没有公共无参构造函数
  • D. 编译通过,运行抛 MissingMethodException
查看答案与解析

正确答案A

解析: 所有值类型都有公共无参构造函数(隐式或显式),天然满足 new() 约束;new DateTime()default,比较为 True。“结构体不满足 new()“是常见误区——只有抽象类、无无参构造的类、接口等才不满足。

关键点: new() 约束对值类型永远成立,new T() 得到零值。

0302 以下代码能否编译?

难度: 进阶

class Base
{
    public Base(int x) { }
}
class Box<T> where T : Base, new() { }
  • A. 编译通过
  • B. 编译失败,new() 与基类约束组合要求基类自身有公共无参构造函数(CS0310)
  • C. 编译通过,运行抛 MissingMethodException
  • D. 编译通过,但 Box<int> 无法使用
查看答案与解析

正确答案B

解析: 约束声明时编译器就要验证可满足性:where T : Base, new() 意味着任何满足 T : Base 的类型都必须有无参构造,而 Base 只有 Base(int),编译器在声明处直接报 CS0310。这不是运行时问题,是编译期错误。

关键点: 基类约束与 new() 同时出现时,基类自身必须有无参构造函数。

0303 在可空上下文(nullable enabled)中,以下代码结果是什么?

难度: 进阶

class C<T> where T : notnull { }
C<int> a = new();
C<string?> b = new();
C<int?> c = new();
  • A. 编译失败,C<string?> 报错
  • B. 编译失败,C<int?> 报错
  • C. 编译通过,但 bc 两行产生 CS8714 警告(可空实参不匹配 notnull 约束)
  • D. 编译通过且没有任何诊断
查看答案与解析

正确答案C

解析: notnull 与大多数约束不同:违反它只产生警告而不是错误。string?int? 都是可空类型,不满足 notnull,编译器对两行各报一条 CS8714 警告,但代码仍可编译运行。在可空模糊(oblivious)上下文中连警告都没有。

关键点: 违反 notnull 是警告,不是错误;string?int? 都会被警告。

0304 关于 notnull 约束,哪项正确?

难度: 进阶

  • A. 运行时会拦截传入的 null
  • B. 编译期保证调用方一定不会传 null
  • C. notnull 会把 T 变成值类型
  • D. notnull 只是编译期可空性注解,运行期仍可能收到 null(例如经反射或未启用可空上下文的调用方)
查看答案与解析

正确答案D

解析: notnull 不产生任何运行期检查:引用类型实参传 null 依然合法(default(T) 就是 null),约束只影响编译期可空性分析。跨程序集、经反射或旧代码调用时,“保证”会失效,别把它当运行期防御。

关键点: notnull 是编译期契约,不是运行期校验。

0305 以下代码输出什么?

难度: 实战

IEnumerable<object> e = new List<string> { "ok" };
foreach (var item in e)
{
    Console.WriteLine(item);
}
  • A. 编译通过,输出 okIEnumerable<out T> 协变转换成立
  • B. 编译失败,List<string> 不能转换为 IEnumerable<object>
  • C. 编译通过,运行抛 InvalidCastException
  • D. 编译通过,输出 List<string>
查看答案与解析

正确答案A

解析: List<string>IEnumerable<string>IEnumerable<object> 走的是接口协变(out T 只能出现在输出位置),引用类型之间的协变转换是隐式且安全的,运行期无异常。数组协变是危险的运行期检查,接口协变是编译期保证。

关键点: 只读接口协变安全且合法,List<T> 本身依然不变。

0306 以下代码能否编译?

难度: 实战

interface ISource<out T>
{
    void Add(T item);
}
  • A. 编译通过
  • B. 编译失败,协变类型参数不能出现在方法入参位置(CS1961)
  • C. 编译通过,但 Add 调用时抛异常
  • D. 编译失败,因为接口不能声明方法
查看答案与解析

正确答案B

解析: out T 只允许出现在输出位置(返回类型、只读属性等);入参是”消费”类型参数的位置,使用协变参数直接报 CS1961。这正是 List<T> 无法声明为 out 的原因——它有 Add(T)

关键点: 协变参数禁止出现在入参位置,否则编译失败。

0307 以下代码能否编译?

难度: 实战

List<object> objs = new List<string>();
  • A. 编译通过,与数组一样协变
  • B. 编译通过,运行抛异常
  • C. 编译失败,List<T> 不变,List<string> 不是 List<object>
  • D. 编译通过,但只能添加 string
查看答案与解析

正确答案C

解析: List<T> 没有 out/in 修饰,是 invariant(不变):List<string>List<object> 之间没有任何隐式或显式引用转换(显式强转也只能在运行期撞 InvalidCastException)。这与 IEnumerable<out T> 的协变形成对比。

关键点: 可变容器(有写入方法)一律不变,别拿数组协变套泛型。

0308 以下代码输出什么?

难度: 进阶

Action<object> act = o => Console.Write(o);
Action<string> act2 = act;
act2("hi");
  • A. 编译失败
  • B. 编译通过,运行抛 InvalidCastException
  • C. 编译通过,什么都不输出
  • D. 编译通过,输出 hiAction<in T> 逆变允许”更宽参数”赋给”更窄参数”
查看答案与解析

正确答案D

解析: Action<in T> 逆变:能处理 object 的委托当然能处理 stringAction<string> = Action<object> 合法。逆变的直觉是”参数类型向基类方向放宽”,与协变的”返回值向派生方向收紧”相反,方向记反是高频错误。

关键点: 逆变允许父类参数委托赋给子类参数委托。

0309 以下代码能否编译?

难度: 实战

interface ISink<in T>
{
    T Get();
}
  • A. 编译失败,逆变类型参数不能出现在返回位置(CS1960)
  • B. 编译通过
  • C. 编译通过,但 Get 调用时抛异常
  • D. 编译失败,因为 in 不是合法关键字
查看答案与解析

正确答案A

解析: in T 只允许出现在输入位置;返回值是”生产”类型参数的位置,使用逆变参数报 CS1960。若接口既要 Get() 又要 Set(T),该类型参数只能保持不变。

关键点: 逆变参数禁止出现在返回位置。

0310 以下代码输出什么?

难度: 进阶

Func<object, string> f = o => o.ToString()!;
Func<string, object> g = f;
Console.WriteLine(g("x"));
  • A. 编译失败,Func 参数与返回方向不允许这种转换
  • B. 编译通过,输出 x,参数逆变、返回协变同时成立
  • C. 编译通过,运行抛 InvalidCastException
  • D. 编译通过,输出空行
查看答案与解析

正确答案B

解析: Func<in T, out TResult>:参数位置逆变(objectstring 合法),返回位置协变(stringobject 合法),所以 Func<string, object> = Func<object, string> 成立。若把 T 也放返回位置(Func<object, object>Func<string, string> 转换)就会失败。

关键点: 同一个 Func 里参数逆变与返回协变可以同时成立。

0311 以下代码哪些行能编译?

难度: 实战

class Wrapper<T> where T : allows ref struct
{
    public T Value;
}
Wrapper<Span<int>> a = new();
Wrapper<string> b = new();
  • A. 两行都编译通过
  • B. 只有 Wrapper<string> 编译通过
  • C. 只有 Wrapper<Span<int>> 编译通过,allows ref struct 只放行 ref struct 实参
  • D. 两行都编译失败
查看答案与解析

正确答案C

解析: C# 13 引入 allows ref struct 反约束,声明后 Span<int>ReadOnlySpan<int> 等 ref struct 才能作为类型实参;string 不是 ref struct,不满足该约束,编译失败。注意该反约束并不能让非 ref struct 通过,它只是”额外放行”。

关键点: allows ref struct 是反约束:放行 ref struct,但仍排除普通类型。

0312 以下代码能否编译?

难度: 进阶

class C<T> where T : class, allows ref struct { }
  • A. 编译通过
  • B. 编译通过,但 T 只能是普通类
  • C. 编译通过,运行抛异常
  • D. 编译失败,allows ref struct 不能与 class 约束组合
查看答案与解析

正确答案D

解析: 反约束 allows ref struct 不能与 class/class? 约束组合(ref struct 是值类型,与”必须是引用类型”语义冲突),编译器直接报错;它也不能与接口等要求装箱能力的约束共存,且必须写在所有约束之后。

关键点: 反约束与 class 互斥,位置必须在约束列表末尾。

0313 以下代码能否编译?

难度: 实战

class C<T> { }
C<Span<int>> c = new();
  • A. 编译失败,没有 allows ref struct 时 ref struct 不能作为类型实参
  • B. 编译通过
  • C. 编译通过,运行抛异常
  • D. 编译失败,因为 Span<int> 不是引用类型
查看答案与解析

正确答案A

解析: C# 13 之前 ref struct 完全不能当类型实参;C# 13 起仍需 allows ref struct 反约束才放行。C<Span<int>> 在无约束下报 CS0306(“may not be used as a type argument”),与”是不是引用类型”无关——C<int> 是合法的。

关键点: ref struct 当类型实参必须显式声明反约束。

0314 以下代码输出什么?

难度: 实战

static bool Same<T>(T a, T b) where T : class => a == b;
string s1 = "hi";
string s2 = new string(s1.ToCharArray());
Console.WriteLine(Same(s1, s2));
Console.WriteLine(s1 == s2);
  • A. TrueTrue
  • B. FalseFalse
  • C. FalseTrue,泛型内 == 只做引用比较,不调用 string 的重载
  • D. TrueFalse
查看答案与解析

正确答案C

解析: 泛型方法中编译器只知道 T : class,对 T== 只能发出引用比较(ceq),即使实参是重载了 ==string 也不调用其重载;块外 s1 == s2 则正常走 string 的值比较。两字符串内容相同但引用不同(new string 不会驻留)。

关键点: 泛型约束下的 == 与具体类型的运算符重载是两回事。

0315 为什么 List<T> 不变,而 IEnumerable<out T> 可以协变?

难度: 进阶

  • A. 因为 List<T> 是类而 IEnumerable<T> 是接口
  • B. 因为 List<T> 有写入元素的方法(如 Add(T)),out 只允许出现在输出位置,可变容器无法协变
  • C. 因为值类型不能协变
  • D. 因为接口天生可以任意声明 out/in
查看答案与解析

正确答案B

解析: 型变安全性的根源是”能否把 T 当作参数消费”:Add(T) 把 T 用在输入位置,若 List<string>List<object> 的子类型,就能往 string 列表里塞 object,破坏类型安全,所以必须不变。只有不暴露写入入口的只读抽象才能安全协变。

关键点: 可变(有写入口)⇒ 不变;只读 ⇒ 才可能协变。

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

难度: 实战

string[] arr = { "a" };
object[] objs = arr;
objs[0] = 42;
  • A. 编译失败
  • B. 编译通过,正常运行
  • C. 编译通过,运行抛 InvalidCastException
  • D. 编译通过,运行抛 ArrayTypeMismatchException
查看答案与解析

正确答案D

解析: 数组协变是”不安全协变”:编译期允许 string[] 赋给 object[],但写入时运行期检查元素类型,42 不是 string,抛 ArrayTypeMismatchException。泛型 List<T> 用不变避免了这类运行期爆炸,这也是它取代数组的原因之一。

关键点: 数组协变是运行期检查的坑,泛型协变才是编译期保证。

0317 以下代码输出什么?

难度: 进阶

IEnumerable<object> objs = new List<string> { "a" };
var list = (List<string>)objs;
Console.WriteLine(list.Count);
  • A. 编译通过,输出 1,运行时对象就是 List<string>,强转成功
  • B. 编译失败
  • C. 编译通过,运行抛 InvalidCastException
  • D. 编译通过,输出 0
查看答案与解析

正确答案A

解析: 强转检查的是对象运行期实际类型:objs 背后就是 List<string>,它实现了 IEnumerable<string>,向 List<string> 强转成功,Count 为 1。若实际类型不符(比如真是 List<object>)才会抛 InvalidCastException——协变只影响静态类型,不影响运行期对象。

关键点: 协变后的强转是否成功取决于运行期实际类型。

0318 关于 where T : notnullwhere T : class 的区别,哪项正确?

难度: 进阶

  • A. 两者完全等价
  • B. notnull 允许值类型实参,class 只允许引用类型;在可空上下文中,两者对可空引用实参都只是警告
  • C. notnull 只能用于接口约束
  • D. class 允许 int? 作为实参
查看答案与解析

正确答案B

解析: class 排除值类型,notnull 放行所有非空类型(含值类型);两者在可空上下文中遇到可空引用类型实参都产生警告(class 也警告,notnull 是 CS8714)。class? 则显式允许可空引用类型,三者语义各不同。

关键点: notnullclass,前者允许值类型,后者不允许。

0319 以下代码输出什么?

难度: 进阶

static T M<T>() where T : allows ref struct => default;
Span<int> s = M<Span<int>>();
Console.WriteLine(s.IsEmpty);
  • A. 编译失败,泛型方法不能使用 allows ref struct
  • B. 编译通过,运行抛异常
  • C. 编译通过,输出 Truedefault 得到空 Span
  • D. 编译通过,输出 False
查看答案与解析

正确答案C

解析: 泛型方法同样支持反约束:M<Span<int>>() 合法,default(Span<int>) 是长度为零的空 Span,IsEmptyTrue。ref struct 实参按值类型专门化、不装箱,但该类型参数不能用于 async/迭代器等要求逃逸到堆的上下文。

关键点: ref struct 泛型实参的 default 是空实例,不装箱。

0320 以下代码哪些行能编译?

难度: 实战

class Animal { }
class Dog : Animal { }
IEnumerable<Dog> dogs = new List<Dog>();
IEnumerable<Animal> a1 = dogs;     // ①
List<Dog> dogs2 = new List<Dog>();
List<Animal> a2 = dogs2;           // ②
  • A. ①②都编译通过
  • B. ①编译失败,②编译通过
  • C. ①②都编译失败
  • D. ①编译通过(接口协变),②编译失败(List<T> 不变)
查看答案与解析

正确答案D

解析:IEnumerable<out T> 协变,DogAnimal 引用转换合法;② List<T>out/inList<Dog> 不能赋给 List<Animal>。同一对类型,接口与类容器行为相反,是型变最经典的对照。

关键点: 派生→基类的转换只对只读接口/委托成立,对容器类不成立。

官方资料

当前分类

C# 选择题

查看全部分类 →
  1. 14C# 试题 14:模式匹配20 题
  2. 15C# 试题 15:泛型基础20 题
  3. 16C# 试题 16:泛型约束与型变20 题
  4. 17C# 试题 17:委托与 Lambda20 题
  5. 18C# 试题 18:事件20 题
ESC

输入关键词开始搜索