1、谈谈你对协变和逆变的理解
协变和逆变能够实现数组类型、委托类型和泛型类型参数的隐式引用转换。
协变(out):适用于返回值,允许派生类型赋值给基类型。
逆变(in):适用于方法参数,允许基类型赋值给派生类型。
它们主要用于接口和委托,提供了更灵活的泛型类型兼容性。
static void Main(string[] args)
{
//协变委托 Func<out T> 只能作为返回值,子类返回给父类
Func<string> func = () => "";
Func<object> func1 = func;
//协变接口 IEnumerable<out T> 只能作为返回值,子类返回给父类
IEnumerable<string> covariant = new List<string>();
IEnumerable<object> covariant1 = covariant;
//协变数组
string[] strings = new string[10];
object[] objects = strings;
//逆变委托 T只能作为参数,父类赋值给子类
Action<object> action = (s) => { };
Action<string> action1 = action;
action1("");
//逆变接口 T只能作为参数,父类赋值给子类
IComparable<object> contravariant = default;
IComparable<string> contravariant1 = contravariant;
contravariant1.CompareTo("");
}
2、谈谈你对委托和事件的理解
委托 (Delegate) :委托是一种特殊的类型,表示对方法的引用,可以把委托是一个类型安全的函数指针,可以指向具有特定签名和返回类型的方法。
多播委托 (Multicast Delegate) :多播委托是指一个委托可以同时指向多个方法,并按顺序调用这些方法。
事件 (Event):事件是基于委托的高级封装,提供了一种发布-订阅模式,用于在对象之间发布消息和订阅消息。事件通常用于通知订阅者某些操作已经发生。EventHandler(object sender,EventArgs arg)就是GUI开发中最经典的事件模型。用于通知某些事件发生。
3、in、out、ref三个引用传递的关键字有什么区别
名称 | 作用 | 使用场景 | 是否需要提前初始化 |
in | 只读参数,按引用传递 | 高效传递大对象但不希望修改其值 | 不需要 |
ref | 输出参数,按引用传递 | 方法内部可以读取和修改参数值,调用前必须初始化 | 需要 |
out | 输出参数,按引用传递 | 方法内部必须赋值,调用前不需要初始化 | 从方法返回多个值 | 不需要 |
4、装箱和拆箱
装箱:值类型转换为引用类型。
拆箱:引用类型转换为值类型
装箱和拆箱会有性能上的损失,应尽量避免使用。
5、C#中哪些类型是值类型,哪些类型是引用类型
值类型:数值类型(int、double、float)、字符char、布尔类型Bool、结构体Struct。
引用类型:类、接口、委托、数组、数据相关结构类(List、LinkNodeList、Dictionary、Stack、Queue、Set、HashSet、SortedSet、SortedDictionary)
5、值类型和引用类型的区别
值类型的父类是ValueType,引用类型的父类是Object。ValueType也继承自Object。
值类型通常分配在栈上,引用类型通常分配在堆上。
5、简单介绍下List<Int>的内存分配
List<T> 是一个动态数组,内部使用T[]维护。
1、List<T> 的实例对象分配在堆上。
2、内部数组对象分配在堆上。
6、C#如何实现线程同步
7、单例模式有哪几种写法,各有什么优缺点
8、C#垃圾回收的机制是什么
9、如何减少垃圾回收器的压力
1、减少对象分配,可以使用对象池或者系统自带IOC容器维护对象。
2、对于需要频繁创建的对象,如果业务逻辑不是很复杂,可以使用结构体代替类,因为结构体分配在栈上,垃圾回收器不会回收栈上的内存。
3、减少装箱和拆箱,装箱和拆箱会在堆上创建临时对象,并且这些对象生命周期很短,大范围使用装箱和拆箱会给GC造成较大压力。
4、使用数组池:ArrayPool<T>
10、如何在C#中检测内存泄露
10、对耦合和解耦有什么理解吗
耦合:指的是两个或多个模块之间的直接依赖程度。耦合度越高,模块之间的依赖越紧密,修改一个模块可能会影响到其他模块。
解耦:解耦是指通过减少模块之间的直接依赖,使得模块更加独立。解耦的目标是提高代码的可维护性、可扩展性和可测试性。
如何解耦:
(1)单一职责原则:一个类尽量承担一类功能,避免在一个类中编写大量的功能模块。
(2)依赖注入:.NET框架提供了强大的依赖注入功能,可以避免在各个模块之间直接相互引用,通过注入的方式,可以减少模块间的依赖程度。(使用最广泛,适用于绝大多数场景)
(3)事件驱动:通过事件、消息队列、弱引用的方式来减少模块之间的直接调用,(通过事件和委托、弱引用、强引用等适用于发布订阅的场景)
(4)接口和抽象:面向接口编程,尽量少面向实例编程。当一个功能有多个实现的时候,就可以使用接口封装功能,比如一个打印机需要调用多个驱动程序打印,一个下载功能需要普通下载和断点下载,就可以接口提供抽象能力(适用于同一个功能有多个实现的场景)
(5)、通过设计模式:工厂模式、中介者模式实现解耦。
10、控制反转和依赖注入
控制反转:控制反转是一种设计原则,指将对象的创建和依赖关系的管理从对象本身转移到外部容器或框架中(工厂容器)。
通过这种方式,模块之间的依赖关系被反转,模块不再直接控制其依赖项,而是由外部容器来管理。模块对被依赖对象的创建交给了外部容器,而不是模块本身,这个就是控制权的转移。
依赖注入:依赖注入是控制反转的具体实现方式,可以通过构造函数、属性、方法等方式将对象注解注入一个模块。
对ABP框架有没有什么了解
Task 会开出一个线程吗? Task 是什么东西 ? 是否了解 async await
Task 不一定会创建线程。它的执行由 TaskScheduler 决定,默认情况下使用线程池。当线程池线中没有空闲线程的时候,可能会创建新的线程来执行任务。
对于CPU密集型任何,一般会使用线程池中的线程。对于IO密集型任务,比如文件读写、网络操作通常会使用操作系统的I/O异步机制来完成任务。async/await不一定会有线程,如果方法本身没有await关键字或者调用Task.Delay都不会用新线程执行任务。
对Task的ConfigurationAwaier方法是否有理解,这个方法的主要作用是什么
引用对象如何进行深拷贝和浅拷贝
Const和Static关键字有什么区别
值类型和引用类型的声明周期有什么区别
DI的生命周期 是什么, autoface 的生命周期
能否简单介绍一下线程池
C#中什么场景会导致内存泄漏,如何排查内存泄露问题。
(1)非托管资源未释放,实现IDispose接口的对象都需要手动调用Dispose方法或者使用using关键字确保非托管资源可以释放。
(2)事件未注销:订阅者必要的时候,没有通过-取消订阅,那么发布者会一直拥有对订阅者的引用,导致订阅者无法被GC回收。
(3)全局静态对象:静态对象的生命周期伴随整个应用程序的生命周期,有些全局的集合对象存放了一些无用的对象,会导致GC无法对对象进行回收。
(4)IDispose对象忘记释放、多次释放、释放顺序错误、多线程环境下释放错误都有可能导致内存泄漏。
能够自己实现一个分布式锁
C#弱引用和强引用的区别和优缺点
C# 内存泄漏排查方法
(1)Visual Studio 诊断工具
(2)专业工具:ANTS Memory Profiler、JetBrains dotMemory、PerfView (微软免费工具)
(3)句柄泄露:句柄对象(文件句柄、进程句柄、线程句柄、注册表句柄)、网络连接、COM组件句柄、线程同步对象、GDI+对象、窗体句柄。