1.值类型和引用类型的相同和不同
都是c#的数据类型
值类型:bool,int,float,struct,enum,byte
引用类型:class,interface,delegate,string,Array
存储方式:值类型存储数据的实际值,存在栈上面,引用类型存储数据的引用(指向数据内存地址),数据本身存储在堆上。
赋值行为:
值类型变量赋值时会创建数据的副本,两个变量互不影响。
引用类型变量赋值时会赋值存储的对象引用,指向了同1个堆空间中的地址,对1个对象进行修改会影响另外的1个对象。
2.如何理解静态
静态包括静态类,静态函数,静态成员变量
1 静态成员属于类本身,而不是类的对象(实例)。通过静态成员,可以在没有创建类实例的情况下访问和使用它们。
2 静态构造函数用于初始化类的静态成员。它在类第一次被访问时自动调用(创建第一个实例,访问类的静态成员),并且只会调用一次。
3 静态字段在程序的整个生命周期内存在,直到程序结束。 它们在类第一次被访问时初始化,并且在类的生命周期内保持其值。
意义:共享数据和函数,方便函数的调用,帮助实现单例模式。
3.String引用类型的特殊性
一旦创建,string 对象的内容不可更改。在修改string对象时其实是创建了新的string类型变量。
字符串常量池(hash表):在给一个string对象赋值时,会先在字符串常量池里查询是否存在对应的字符串,不存在就会在池子里创建新的键值对。value指代这个创建的对象引用,再把引用赋给string对象。
如果存在,就会给这个string对象赋值常量池里的引用。通过拼接、格式化、输入等方式动态创建的字符串不会自动进入常量池,即使它们的内容与常量池中的某个字符串相同。这些字符串会被分配到堆上,并作为新的实例存在。
4.stringbuilder介绍
目的:解决大量字符串拼接而产生的的内存消耗问题.每次字符串拼接都会产生垃圾,旧的字符串增加gc压力。
StringBuilder 是支持扩容的(char类型)数组,在每次空间不足时,会开辟原先数组大小的容量,类似于链表,新建的数组指向上一个已经用完的数组,本身不会产生gc。
5.装箱和拆箱
装箱:把值类型转换为引用类型或者值类型实现的接口类型。把栈中内容迁移到堆中去
拆箱:object类型或者接口类型到值类型的显示转换。把堆中内容迁移到栈中去
影响:装箱拆箱操作会生成新的对象,每次装箱时要在堆中new一个新的对象,当量特别大是肯定会大大影响程序的效率,会不断分配内存,会造成内存碎片,增加性能消耗。所以应该尽量减少装箱拆箱。
优化:使用泛型,结构体使用重载函数。
6.泛型和泛型约束有哪些?
类型参数化——在定义时不指定具体类型,而是在使用时再确定类型。也可以理解为:多个类型共享一组代码。泛型类不是实际的类,而是类的模板。
泛型不会进行装箱拆箱,所以性能很高,且规定了变量类型的限制,编译器可以在一定程度上验证类型的假设,提高了程序类型的安全性,因此在使用容器时多使用带有泛型的容器例如(ArrayList与List)。
泛型约束:1.值类型约束 T:struct
2.引用类型约束 T:class
3.公共无参构造约束 T:new()
4.类约束 T:类名
5.接口约束 T:接口名
6.另一个泛型约束 T:U
7.c#常用数据结构和使用场景
1数组(Array):固定大小,元素类型一致。存储在连续的内存地址上。常用于快速访问元素和数据量固定的场景下。
2列表(List):大小不固定,基于数组实现。提供增删查改的操作。用于需要经常访问,操作数据的场景。
3字典(Dictionary<TKey, TValue>):以键值对形式存储数据,键必须唯一,值可以重复。查找,插入速度快。
4队列(Queue):先进先出,用于任务队列、进程调度。
5栈(Stack):先进后出。使用于递归计算、括号匹配、撤销操作。
8.Object.Equals和Object.ReferenceEquals方法和“==”的区别
== : 对于值类型,如果值相等,则返回 true,否则返回false。 对于string 以外的引用类型,如果两个操作数引用同一个对象,则== 返回true。
equals:对于string 类型,== 比较字符串的值。因为重载了string的运算符。
值类型,equals方法比较值。引用类型,equals方法比较引用对象。对于string类型,equals也比较字符串的值,因为重写了equals方法。
ReferenceEquals:只比较两个对象的引用。
9.重载和重写的区别
重载:重载是指在同一个类中不同参数列表的同一个方法名的多个方法。 方法名需要一致,但参数列表不一致
重写:重写是派生类对基类的虚方法的重新实现。且方法签名必须一致(返回类型,参数列表)
10.New关键字的用途
new运算符:new 运算符创建类型的新实例,调用构造函数(如果继承了基类,同时会调用基类的构造函数)。
new声明修饰符:new 关键字可以显式隐藏从基类继承的成员。派生类如果定义了和基类同名的成员变量/方法,同样会隐藏基类的同名变量/方法,但编译器会警告。 如果使用 new 来修饰,将不显示此警告。
new约束:new 约束指定泛型类或方法声明中的类型实参必须有公共的无参数构造函数。 若要使用 new 约束,则该类型不能为抽象类型。
11.面向对象思想
封装:封装是将数据和操作数据的方法结合,形成独立的单元(类),属性是C#封装实现的最好体现。将一些复杂的逻辑包装起来,程序员不管内部是如何实现的,只负责使用里面的数据或者逻辑,目的是保护或者防止代码被无意修改。
继承:通过继承,一个类可以使用其他类的方法,属性来创造其他类,达成代码的复用和拓展。
多态:指同一个方法在被不同的对象调用时做出行为,让代码更加灵活可以适应不同的对象。一个方法被不同的实例以不同方式实现了。编译时动态(方法重载),运行时重载(方法重写,根据实际指向的类调用方法)
12.密封类
1密封类不能被其他类继承,封类可以继承其他类。
2密密封方法:防止重写父类中的方法。在 C# 中,可以通过 sealed 关键字来密封一个方法,通常与 override 一起使用,表示该方法不能在继承类中被重写。
13.结构体和类的区别
内存分配:结构体存储在栈上,类存储在堆上
类型传递:当你将结构体变量传递给方法时,会复制整个结构体的值。这意味着在方法内部操作的是结构体的副本,不会影响原始结构体。而类对象传递给方法时,传递的是对象引用,对它的修改会影响原始对象。
使用场景:结构体适合轻量级的数据传输和封装,类适合比较复杂的面向对象编程。例如表示点的坐标使用结构体,表示动物时使用类。
面向对象:结构体不能被继承,而类可以。
构造函数:结构体需要在构造函数中初始化所有成员变量,而类随意。
成员变量:成员是不能初始化的,只能在构造函数,或者实例化后赋值。
静态:结构体不能被静态static修饰(不存在静态结构体),而类可以。
14.抽象类
抽象类是 C# 中一种专门为继承设计的类,作为基类提供通用行为和属性的定义。它的主要目的是为派生类提供一个框架或模板,同时可以包含部分实现和抽象成员。
特点:
1不能直接实例化,但有构造函数,可以间接实例化。
2可以包含抽象成员和具体成员和字段:
抽象成员(方法、属性、事件等)不包含实现,派生类必须实现这些成员。(抽象方法只能定义方法签名,不能实现。抽象属性只有声明,不能包含具体实现。)
具体成员可以包含实现逻辑,派生类可以直接继承使用。
3可以实现接口:抽象类可以实现接口中的成员,并可以选择将其作为抽象成员保留,让派生类实现。
4抽象成员在派生类里面只有用override关键字才能实现它们。
15.事件和委托
委托是一种自定义类型,允许你存储方法的引用或指针。通过委托,你可以将一个方法的地址传递给其他代码,从而允许动态调用该方法。
事件:是对委托的一种包装,限制外界对委托内部的访问,外界只能访问它的+=,-=操作
委托可以用“=”来赋值,事件不可以。
委托可以在声明它的类外部进行调用,而事件只能在类的内部进行调用。
委托是一个类型,事件修饰的是一个对象。
Action和Func是System命名空间下 C#为我们提供的两个写好的委托Action本身是一个无参无返回值的委托
对应的Action<>泛型委托支持最多16个参数
Func本身是一个无参有返回值的委托
对应的Func<>泛型委托支持最多16个参数,并且有返回值
16.ref和out
使用:当变量作为函数参数传递,函数内的操作可以改变外面的原始变量。注意ref需要提前赋值,out则需要在方法内赋值
用途:当方法需要多个返回值时,可以使用ref,out。
原理:ref和out会把参数自身的引用赋给参数,达到修改外界原始变量的目的。
17.内存泄漏是什么,有哪些情况?
内存泄漏指的就是对象超过生命周期后而不能被GC回收,一般指不会再使用的引用对象由于某些操作而不能被GC垃圾回收,而一直占用着内存。
常见的内存泄漏有:
1.静态引用
2.不使用的引用对象没有置null,一直被引用
3.文件操作时,没有使用using或者没有进行Dispose()
4.委托或事件注册后没有解除注册(有加就有减)
18.c#GC垃圾回收机制
在c#中,垃圾回收是由CLR来执行的。CLR负责管理托管代码的执行,其中包含内存分配和回收。c#中垃圾回收是基于代的,
即将托管堆中的对象分为不同的代,每个代具有不同生命周期。垃圾回收根据对象的代进行不同的回收策略,通常会优先回收那些生命周期较短的对象。
详细流程:
1.当新建立引用类型对象时,检查0代储存空间是否有充足的空间使得新的引用类型对象存储。若没有,将0代对象进行遍历检查,是否有被调用(激活),没有被调用的对象被标记“可回收”。(使用标记-清除算法去检测每一个对象是否可被标记解决循环引用问题)
2.遍历完成后,将所有被“可回收”的对象进行垃圾回收,释放的空间返回给0代储存区,其他的对象的对象 迁移 到1代储存区,标记为“1代对象”,此时该对象是分散分布的,要进行 压缩 操作,使得1代对象顺序紧密排列。新对象存储于0代储存空间,标记为0代对象。
3.当1代空间满了时,将1代对象按照上述操作遍历,迁移,压缩到2代储存区,标记为2代对象,同时0代迁移压缩到1代。
分代算发的优点:分代算法通过将垃圾回收限制在年轻代内,减少了需要检查的对象数量。由于年轻代中的大多数对象存活时间较短,它们被快速回收,减少了不必要的工作。年长代的回收较少,但回收过程较为复杂,减少了频繁的长时间 GC 开销。
避免GC:
1当有大量字符串拼接操作时用stringbuilder代替string
2避免容器频繁的扩容(尽量指定容器大小)、
3用对象池优化技术
4避免频繁的装箱和拆箱,使用泛型
19.闭包是什么?
闭包是一个函数,它记住并可以访问它定义时的外部作用域中的变量。即使外部函数已经结束执行,闭包仍然可以引用并修改这些外部变量。
闭包发生的典型情况是:当一个函数捕获并访问其外部作用域中的变量时,就会形成闭包。
C# 的闭包捕获机制会让外部变量的引用存在于堆上,而不是回收它。这是为了保证闭包仍然能够访问和使用这个变量。
20.接口
只能包含 事件,属性,索引器,方法作为成员。接口有默认实现。可以认为接口是把行为抽象了。
接口和抽象类的区别:
1接口不是一个类,不能实例化,抽象类有构造函数,可以间接实例化。
2抽象类只能继承一个,接口可以实现多个。
3抽象类成员可以是私有的,接口只能是公共的。
4接口里的成员必须被实现,抽象类只有标记关键字abstract的成员需要被实现
21.虚方法和抽象方法的区别
1虚方法:可以在基类中提供实现,派生类可以选择重写(Override)该方法。
抽象方法:在声明时没有实现,派生类必须提供实现。
2非抽象类中的使用:虚方法可以在任何类中声明,无论该类是否被声明为 abstract。
抽象方法:只能在抽象类中声明。
3重写虚方法:派生类可以使用 override 关键字重写基类中的虚方法。
抽象方法:派生类必须实现抽象方法,这不是重写,因为抽象方法没有实现。
21.new和override的区别(隐藏方法和重写的区别)
1new用于隐藏成员,override用于重写成员
2new 不支持多态,override支持多态(例子:父类容器被子类对象赋值,调用重写方法时还是调用的子类方法)
22.逆变和协变
用于接口和委托,为了类型安全使用。
协变是指在泛型类型的使用中,允许将返回类型为父类替换为该参数的子类,关键字out
逆变是指在泛型类型的使用中,允许将方法参数替换为该参数的父类,关键字in