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. 编译通过,但
b、c两行产生 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. 编译通过,输出
ok,IEnumerable<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. 编译通过,输出
hi,Action<in T>逆变允许”更宽参数”赋给”更窄参数”
查看答案与解析
正确答案D
解析: Action<in T> 逆变:能处理 object 的委托当然能处理 string,Action<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>:参数位置逆变(object → string 合法),返回位置协变(string → object 合法),所以 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.
True、True - B.
False、False - C.
False、True,泛型内==只做引用比较,不调用string的重载 - D.
True、False
查看答案与解析
正确答案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 : notnull 与 where T : class 的区别,哪项正确?
难度: 进阶
- A. 两者完全等价
- B.
notnull允许值类型实参,class只允许引用类型;在可空上下文中,两者对可空引用实参都只是警告 - C.
notnull只能用于接口约束 - D.
class允许int?作为实参
查看答案与解析
正确答案B
解析: class 排除值类型,notnull 放行所有非空类型(含值类型);两者在可空上下文中遇到可空引用类型实参都产生警告(class 也警告,notnull 是 CS8714)。class? 则显式允许可空引用类型,三者语义各不同。
关键点: notnull ≠ class,前者允许值类型,后者不允许。
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. 编译通过,输出
True,default得到空 Span - D. 编译通过,输出
False
查看答案与解析
正确答案C
解析: 泛型方法同样支持反约束:M<Span<int>>() 合法,default(Span<int>) 是长度为零的空 Span,IsEmpty 为 True。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> 协变,Dog → Animal 引用转换合法;② List<T> 无 out/in,List<Dog> 不能赋给 List<Animal>。同一对类型,接口与类容器行为相反,是型变最经典的对照。
关键点: 派生→基类的转换只对只读接口/委托成立,对容器类不成立。