01-C#.Net-泛型-面试题

Q1:什么是泛型?为什么要用泛型?

出题意图:考察对泛型基本概念的理解,以及是否能说清楚它解决了什么问题。

泛型是一种"延迟声明类型"的机制。声明时用占位符 T 代替具体类型,调用时再指定。

引入泛型主要解决两个问题:

  1. 性能问题 :用 object 作为通用参数时,值类型会发生装箱/拆箱,有额外的堆分配和 CPU 开销。泛型在 CLR 层面对值类型生成专用代码,完全避免装箱,性能等同于专用方法。
  2. 类型安全问题object 参数在编译期无法检查类型,强制转换错误只能在运行时暴露。泛型在编译期就能检查类型,错误提前发现。
csharp 复制代码
// 泛型方法:一个方法,任意类型,无装箱,编译期安全
public static void Show<T>(T value)
{
    Console.WriteLine($"{typeof(T).Name}: {value}");
}

Show<int>(123);   // 显式指定
Show("hello");    // 类型推导,省略尖括号

Q2:泛型是语法糖吗?底层原理是什么?

出题意图:区分候选人是否真正理解泛型的实现机制,还是只会用。

泛型不是语法糖 。语法糖是编译器提供的便捷写法,编译后转换成等价的普通代码。泛型是由 CLR 在底层真正支持的,从 .NET Framework 2.0 开始引入,编译器和运行时都必须支持。

底层机制:

  • 编译器将泛型类/方法编译成带占位符的 IL 代码,例如 List\1[T]Dictionary`2[TKey,TValue]`
  • JIT 编译时:
    • 值类型 (intstruct 等):CLR 为每种类型生成一份独立的本地代码,不存在装箱
    • 引用类型:CLR 共享同一份本地代码(引用类型内存布局相同,都是指针大小)

这就是为什么泛型方法性能等同于专用方法,而远优于 object 方法。


Q3:泛型方法的性能为什么优于 object 参数方法?

出题意图:考察对装箱拆箱机制的理解,以及泛型性能优势的具体原因。

核心原因是装箱拆箱 。值类型(如 int)传入 object 参数时,需要在堆上分配新对象(装箱),取出时再拆箱,有额外的内存分配和 GC 压力。泛型方法对值类型生成专用代码,直接操作,无需装箱。

csharp 复制代码
int value = 123;

// Object方式:每次调用都装箱
ShowObject(value);  // int → object(堆分配)→ int

// 泛型方式:无装箱,直接操作 int
Show<int>(value);

性能差异在大量循环 + 值类型 场景下最明显,例如 1 亿次调用,object 方法耗时可能是泛型方法的数倍。


Q4:泛型约束有哪些?分别有什么作用?

出题意图:考察泛型约束的掌握程度,这是实际开发中经常用到的。

约束 语法 作用
基类约束 where T : People T 必须是 People 或其子类,方法内可访问 People 的成员
接口约束 where T : ISports T 必须实现该接口,方法内可调用接口方法
引用类型约束 where T : class T 只能是引用类型
值类型约束 where T : struct T 只能是值类型,且可以直接 new T()
无参构造约束 where T : new() T 必须有无参构造函数,方法内可 new T()
枚举约束 where T : Enum T 必须是枚举类型
父子关系约束 where T : S T 必须是 S 或 S 的子类

多个约束可以叠加:

csharp 复制代码
// T 必须是引用类型、实现 IEntity 接口、有无参构造函数
public class Repository<T> where T : class, IEntity, new()
{
    public T Create() => new T();
}

解答思路 :回答时结合实际场景,比如"基类约束让我在泛型方法里能访问特定属性,避免了强制转换;new() 约束让我可以在方法内部创建实例"。


Q5:以下代码有什么问题?如何用泛型约束解决?

csharp 复制代码
public class Repository<T>
{
    public T Create()
    {
        return new T(); // 编译错误
    }

    public void Save(T entity)
    {
        Console.WriteLine(entity.Id); // 编译错误
    }
}

出题意图:考察泛型约束的实际应用,以及如何通过约束解决编译时类型安全问题。

两个错误原因:

  1. new T():编译器不知道 T 是否有无参构造函数
  2. entity.Id:编译器不知道 T 是否有 Id 属性

解决方案:

csharp 复制代码
public interface IEntity
{
    int Id { get; set; }
}

// 加上约束:T 必须是引用类型、实现 IEntity、有无参构造函数
public class Repository<T> where T : class, IEntity, new()
{
    public T Create()
    {
        return new T(); // ✓
    }

    public void Save(T entity)
    {
        Console.WriteLine(entity.Id); // ✓ 可以访问 Id
    }
}

解答思路:识别编译错误的根本原因 → 选择对应约束 → 理解约束组合使用。


Q6:List<Animal>List<Cat> 有继承关系吗?为什么?

出题意图:考察对泛型类型系统的理解,引出协变逆变话题。

没有继承关系。即使 Cat 继承自 AnimalList<Cat>List<Animal> 也是完全独立的两个类型:

csharp 复制代码
List<Animal> list = new List<Cat>(); // 编译报错

原因:泛型类用不同类型参数实例化后,得到的是不同的类型,彼此没有任何关系。这是 C# 类型系统的设计,目的是保证类型安全------如果允许上面的赋值,就可以往 listAdd(new Dog()),但实际底层是 List<Cat>,运行时会崩溃。

如果需要这种灵活性,应该使用协变(IEnumerable<out T>)。


Q7:什么是协变和逆变?请解释以下代码的编译结果。

csharp 复制代码
// 片段1
List<Animal> list1 = new List<Cat>();           // ?

// 片段2
IEnumerable<Animal> list2 = new List<Cat>();    // ?

// 片段3
public interface IReader<out T> { T Read(); }
IReader<Animal> reader = new Reader<Cat>();     // ?

// 片段4
public interface IWriter<in T> { void Write(T item); }
IWriter<Cat> writer = new Writer<Animal>();     // ?

出题意图:这是中高级 .NET 面试的高频考点,考察对协变逆变的真实理解。

  • 片段1:编译错误 ❌ --- List<T> 不支持协变
  • 片段2:编译成功 ✓ --- IEnumerable<out T> 支持协变
  • 片段3:编译成功 ✓ --- out T 协变,右边子类,左边父类
  • 片段4:编译成功 ✓ --- in T 逆变,右边父类,左边子类

协变(out):T 只能作为返回值,允许"右边用子类,左边用父类"。

csharp 复制代码
public interface ICustomerListOut<out T>
{
    T Get();           // ✓ 可以作为返回值
    // void Show(T t); // ✗ 不能作为参数
}

ICustomerListOut<Animal> list = new CustomerListOut<Cat>(); // ✓

逆变(in):T 只能作为参数,允许"右边用父类,左边用子类"。

csharp 复制代码
public interface ICustomerListIn<in T>
{
    void Show(T t);    // ✓ 可以作为参数
    // T Get();        // ✗ 不能作为返回值
}

ICustomerListIn<Cat> list = new CustomerListIn<Animal>(); // ✓

记忆口诀

  • out 协变:出去的(返回值),子类 → 父类,"出口放宽"
  • in 逆变:进来的(参数),父类 → 子类,"入口放宽"

注意:协变逆变只适用于泛型接口和泛型委托,不适用于泛型类。


Q8:协变逆变为什么只适用于接口和委托,不适用于泛型类?

出题意图:考察对协变逆变设计原理的深层理解,区分中高级候选人。

因为泛型类的类型参数既可以出现在参数位置,也可以出现在返回值位置,无法同时满足协变和逆变的安全约束。

List<T> 为例,它既有 Add(T item)(T 做参数),又有 T this[int index](T 做返回值)。如果允许协变:

csharp 复制代码
List<Animal> list = new List<Cat>(); // 假设合法
list.Add(new Dog()); // 往 List<Cat> 里加了一只 Dog,类型系统崩溃

而接口可以通过 in/out 约束,明确规定 T 只出现在参数或返回值的某一侧,编译器在定义接口时就能验证安全性。

IEnumerable<out T> 只有 GetEnumerator()(T 只做返回值),所以可以协变。IList<T> 因为有 Add(T)T this[int],所以不支持协变。


Q9:泛型缓存和字典缓存有什么区别?

出题意图:考察对泛型静态成员特性的理解,以及实际应用能力(ORM、框架开发场景)。

字典缓存 :用一个静态 Dictionary<Type, T> 存储,以类型为 key,所有类型共享一个字典。

csharp 复制代码
public class DictionaryCache
{
    private static Dictionary<Type, string> _cache = new Dictionary<Type, string>();

    public static string GetCache<T>()
    {
        Type type = typeof(T);
        if (!_cache.ContainsKey(type))
            _cache[type] = $"{typeof(T).FullName}_{DateTime.Now:yyyyMMddHHmmss.fff}";
        return _cache[type];
    }
}

缺点:每次调用都要查字典(哈希查找),多线程下需要加锁。

泛型缓存:利用泛型类的特性------每种类型参数对应一个独立的类副本,静态字段也是独立的。

csharp 复制代码
public class GenericCache<T>
{
    private static readonly string _typeTime;

    static GenericCache()
    {
        // 每种 T 的静态构造函数只执行一次,CLR 保证线程安全
        _typeTime = $"{typeof(T).FullName}_{DateTime.Now:yyyyMMddHHmmss.fff}";
    }

    public static string GetCache() => _typeTime;
}

// GenericCache<int> 和 GenericCache<string> 是完全不同的类,各有独立的静态字段
GenericCache<int>.GetCache();
GenericCache<string>.GetCache();

优点:直接访问静态字段,无需字典查找,性能更高;CLR 保证静态构造函数线程安全,无需手动加锁。

实际应用:在手写 ORM 框架时,用泛型缓存存储实体类的反射信息(属性列表、表名等),避免每次操作都重复反射,大幅提升性能。


Q10:泛型方法调用时,什么情况下可以省略类型参数?

出题意图:考察对编译器类型推断的理解。

当编译器能从传入的参数推断出类型参数时,可以省略尖括号:

csharp 复制代码
public static void Show<T>(T value) { }

Show<int>(123);   // 显式指定
Show(123);        // 省略,编译器推断 T = int
Show("hello");    // 省略,编译器推断 T = string

如果类型参数只出现在返回值中,无法从参数推断,则必须显式指定:

csharp 复制代码
public static T Create<T>() where T : new() => new T();

var obj = Create<MyClass>(); // 必须指定,无法推断

Q11:泛型类的子类继承有哪些方式?

出题意图:考察对泛型继承的理解,实际项目中经常遇到。

csharp 复制代码
public abstract class GenericBase<T>
{
    public void Show(T t) { }
}

// 方式一:子类固定类型参数,子类本身不是泛型类
public class ChildA : GenericBase<int> { }

// 方式二:子类继续保持泛型,把类型参数传递给父类
public class ChildB<S> : GenericBase<S> { }

两种方式各有适用场景:固定类型适合具体业务类(如 UserRepository : Repository<User>),保持泛型适合通用基础设施类(如通用仓储基类)。


Q12:default(T) 有什么用?

出题意图:考察在泛型方法中处理默认值的能力。

在泛型方法中,无法直接写 return nullreturn 0,因为 T 可能是值类型也可能是引用类型。default(T) 返回 T 的默认值:

  • 引用类型:返回 null
  • 值类型:返回对应的零值(int0boolfalseDateTime0001-01-01)
csharp 复制代码
public T GetDefault<T>()
{
    return default(T); // C# 7.1 之后可简写为 default
}

综合应用题:设计通用仓储

题目 :设计一个通用仓储接口和实现类,要求:实体必须有 Id 属性、必须是引用类型、支持基本 CRUD、用泛型缓存优化反射性能。

出题意图:综合考察泛型约束、泛型缓存、接口设计,是中高级面试常见的大题形式。

csharp 复制代码
// 实体接口
public interface IEntity
{
    int Id { get; set; }
}

// 泛型缓存:缓存实体类型的反射信息,每种类型只反射一次
public class EntityCache<T> where T : class
{
    private static readonly string _tableName;
    private static readonly PropertyInfo[] _properties;

    static EntityCache()
    {
        _tableName = typeof(T).Name;
        _properties = typeof(T).GetProperties();
    }

    public static string TableName => _tableName;
    public static PropertyInfo[] Properties => _properties;
}

// 仓储接口
public interface IRepository<T> where T : class, IEntity, new()
{
    T GetById(int id);
    IEnumerable<T> GetAll();
    void Add(T entity);
    void Delete(int id);
}

// 仓储实现
public class Repository<T> : IRepository<T> where T : class, IEntity, new()
{
    private readonly List<T> _store = new();

    public T GetById(int id)
    {
        Console.WriteLine($"查询表: {EntityCache<T>.TableName}");
        return _store.FirstOrDefault(e => e.Id == id);
    }

    public IEnumerable<T> GetAll() => _store;

    public void Add(T entity)
    {
        entity.Id = _store.Count + 1;
        _store.Add(entity);
    }

    public void Delete(int id)
    {
        var entity = GetById(id);
        if (entity != null) _store.Remove(entity);
    }
}

// 实体类
public class User : IEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

// 使用
var repo = new Repository<User>();
repo.Add(new User { Name = "张三" });
var user = repo.GetById(1);

设计要点

  • where T : class, IEntity, new() 三重约束保证类型安全
  • 泛型缓存避免重复反射,每种实体类型只初始化一次
  • 接口与实现分离,遵循依赖倒置原则
相关推荐
⑩-1 小时前
为什么要用消息队列?使用场景?
java·rabbitmq
leonkay2 小时前
Golang语言闭包完全指南
开发语言·数据结构·后端·算法·架构·golang
Allnadyy2 小时前
【C++项目】从零实现高并发内存池(一):核心原理与设计思路
java·开发语言·jvm
雅欣鱼子酱2 小时前
Type-C供电PD协议取电Sink芯片ECP5702,可二端头分开供电调整亮度,适用于LED灯带户外防水超亮灯条方案
c语言·开发语言
浑水摸鱼仙君2 小时前
SpringSecurity和Flux同时使用报未认证问题
java·ai·flux·springsecurity·springai
一叶飘零_sweeeet2 小时前
Java 线程模型底层解密:从内核原理到生产级架构选型,全链路实战指南
java· java线程模型
似水明俊德2 小时前
07-C#
开发语言·c#
am心3 小时前
企业开发项目流程记录
java
浩子智控3 小时前
python程序打包的文件地址处理
开发语言·python·pyqt