前言:记录了总6w字的面经知识点,文章中的知识点若想深入了解,可以点击链接学习。由于文本太多,按类型分开。这一篇是C# 常问问题总结,有帮助的可以收藏。
值类型:int,bool,float,char,struct,enum。
引用类型:string,object,delegate,interface,class,array。
1.引用类型在实例化时,先在栈内开辟空间,用于存储堆中对象的地址,然后在堆内开辟空间,存储引用对象。
2.而值类型直接在栈中开辟空间存储对象。值类型也有引用地址,但都在栈内的同一空间。
3.在参数对象进入方法体内,实则是在栈中开辟了新的临时空间。(也就是参数对象的副本)栈内值类型的修改,由于栈中地址不同,所以值类型不会影响到主体。而引用类型的存储数据是一个堆内的地址,所以对于引用类型的修改是直接修改堆内的对象。
4.值类型对象中的引用类型在堆中(struct中定义的string等)
引用类型对象中的值类型也在堆中(class中的int等)
详细请看:
C# “值类型“和“引用类型“在内存的分配_生产队的驴.的博客-CSDN博客_值类型和引用类型如何分配内存
string的修改,实则是new 一个新的string,在堆内新开辟空间。而此时栈内的副本也会指向堆内新对象。因此string改变。
是新建的对象,和本体没有联系。
当频繁堆一个字符串进行修改时,利用StringBuilder代替String
StringBuilder 是支持扩容的(char类型)数组,在每次空间不足时,会开辟一倍的空间(4 -> 8 -> 16...)。 在扩容的期间,会丢弃原数组内的内容,将内容拷贝到新数组。
StringBuffer是线程安全,一般用于多线程
StringBuilder是非线程安全,所以性能略好,一般用于单线程
答:不一定,stringbuilder有自身的GC消耗
极少拼接(或者短字符串)的情况下 String甚至优于StringBuilder,因为String是公用API,通用性好,用途广泛,读取性能高,占用内存较小,Stringbuilder初始化花费时间更大。
字符串池有什么用,原理是什么?
字符串池是CLR一种针对于反复修改字符串对象的优化措施,作用能够一定程度减少内存消耗。原理是内部开辟容器通过键值对的形式注册字符串对象,键是字符串对象的内容,值是字符串在托管堆上的引用。这样当新创建的时候,会去检查,如果不存在就在这个容器中开辟空间存放字符串。
C#:分代算法,有内存整理,避免碎片化。有压缩。
0代,未被标记回收的新分配对象
1代,上次垃圾回收中没有被回收的对象
2代,在一次以上的垃圾回收之后任然没有被回收的对象
1.GC会检查堆内存上的每个存储变量;
2.对每个变量会检测其引用是否处于激活状态;
3.如果变量的引用不再处于激活状态,则会被标记为可回收;
4.被标记的变量会被移除,其所占有的内存会被回收到堆内存上。
1.当新建立引用类型对象时,检查0代储存空间是否有充足的空间使得新的引用类型对象存储。若没有,将0代对象进行遍历检查,是否有被调用(激活),没有被调用的对象被标记“可回收”。
2.遍历完成后,将所有被“可回收”的对象进行垃圾回收,释放的空间返回给0代储存区,其他的对象的对象 迁移 到1代储存区,标记为“1代对象”,此时该对象是分散分布的,要进行 压缩 操作,使得1代对象顺序紧密排列。新对象存储于0代储存空间,标记为0代对象。
3.当1代空间满了时,将1代对象按照上述操作遍历,迁移,压缩到2代储存区,标记为2代对象,同时0代迁移压缩到1代。
GC在unity内存管理中,会带来以下问题:
1.游戏性能:GC操作是一个极其耗费事件的操作,堆内存上的变量或者引用越多则导致遍历检查时的操作变得十分缓慢,使得游戏运行缓慢,例如当CUP处于游戏性能的关键时刻,任何一个操作就会导致游戏帧率下降,造成极大的影响。
2.游戏内存:(unityGC采用的是非分代非压缩的标记清除算法)GC操作会产生“内存碎片化”。当一个单元内存从堆中分配出来,其大小取决于存储变量的大小。当内存被回收到堆上时,有可能被堆内存分割成碎片化的单元。(就是说总容量大小时固定的,但是单元内存较小。例如房子很大,房间很小,找不到合适的房间)即下次分配时找不到合适的储存单元,就会触发GC操作,或者堆内存扩容操作,导致GC频发发生和游戏内存越来越大。
1.在堆内存上进行内存分配操作,而内存不够的时候都会触发垃圾回收来利用闲置的内存;
2.GC会自动的触发,不同平台运行频率不—样;
3.GC可以被强制执行。
1.减少临时变量的使用,多使用公共对象,多利用缓存机制。(将容器定义到函数外,用到容器的时候进行修改即可)
2.减少new对象的次数。
3.对于大量字符串拼接时,将StringBuilder代替String。(string不可修改性,修改即创建一个新的string对象,旧的直接抛弃等待GC,但少量字符串拼接用string,性能优于stringbuilder)
4.使用扩容的容器时,例如:List,StringBuilder等,定义时尽量根据存储变量的内存大小定义储存空间,减少扩容的操作。(扩容后,旧的容器直接抛弃等待GC)
5.代码逻辑优化:例如计时器当大于1s后才进行文本修改,而不是每帧都修改,或者禁止在关键时候GC,影响游戏性能,可以在加载页面或者进度条的时候GC。
6.利用对象池:对象池是一种Unity经常用到的内存管理服务,针对经常消失生成的对象,例如子弹,怪物等,作用在于减少创建每个对象的系统开销。在我们想要对象消除时,不直接Destory,而是隐藏起来SetActive(false),放入池子中,当需要再次显示一个新的对象时,先去池子中看有没有隐藏对象,有就取出来(显示) SetActive(true),没有的话,再实例化。
7.减少装箱拆箱的操作:
7.1装箱拆箱介绍:
装箱是将值类型转换为 object 类型或由此值类型实现的任何接口类型的过程。
装箱的底层操作:
去堆内存new一个Object类对象
把值类型的数据存入到堆中的Object对象中
将堆上创建的对象的地址返回给引用类型变量。
拆箱是从 object 类型到值类型或从接口类型到实现该接口的值类型的显式转换。
拆箱底层操作:
获取已装箱的对象的地址检查对象实例,以确保它是给定值类型的装箱值。
将该值从实例复制到值类型变量中。
装箱是将一个 值类型 变量被用于 引用类型 变量的内部转换过程;拆箱 是将创想后的引用类型转回值类型的操作。(无装修即无拆箱)。
7.2产生GC的原因:在Unity的装箱操作中,对于值类型会在堆内存上分配一个System.Object类型的引用来封装该值类型变量,其对应的缓存就会产生内存垃圾。装箱操作是非常普遍的一种产生内存垃圾的行为,即使代码中没有直接的对变量进行装箱操作,在插件或者其他的函数中也有可能会产生。最好的解决办法是尽可能的避免或者移除造成装箱操作的代码。
7.3泛型介绍:处理多个代码对不同的数据类型执行相同指令的操作。也可以理解为:多个类型共享一组代码。泛型类不是实际的类,而是类的模板。泛型不会进行装箱拆箱,所以性能很 高,且规定了变量类型的限制,编译器可以在一定程度上验证类 型的假设,提高了程序类型的安全性,因此在使用容器时多使用 带有泛型的容器例如(ArrayList与List
8.协程: yeild return 0 会产生装箱拆箱,可以替换为 yeild return null。
9.减少不必要的Log;
提高代码重用度,增强软件可维护性的重要手段,符合开闭原则(软件中的对象扩展是开放的,修改是关闭的)。继承就是把子类的公共属性集合起来(变量,方法等)共同管理,这些公共属性设置为父类,C#的继承是单继承,但继承有传递性:A继承B,B继承C,A可以调用C#中的方法。
封装是将数据和行为相结合,通过行为约束代码修改数据的程度,增强数据的安全性,属性是C#封装实现的最好体现。将一些复杂的逻辑包装起来,程序员不管内部是如何实现的,只负责使用里面的数据或者逻辑,目的是保护或者防止代码被无意修改。
多态性是指同名的方法在不同环境下,自适应的反应出不同得表现,是方法动态展示的重要手段。例如叫声,在鸟这个类中是“鸣啼”在狗这个类中是“犬吠”。
protected internal: protected + internal
关键字sealed,类声明时可防止其他类继承此类,在方法中声明则可防止派生类重写此方法。与override一起使用。
详细请看:
文章https://blog.csdn.net/qq_40323256/article/details/86771078?ops_request_misc=&request_id=&biz_id=102&utm_term=sealed%E7%9A%84%E4%BD%BF%E7%94%A8&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-0-86771078.142%5Ev42%5Enew_blog_pos_by_title,185%5Ev2%5Econtrol&spm=1018.2226.3001.4187
结构体:
类:
1.类是引用类型,存储在堆中,堆的容量大,适合重量级的对象,栈的空间不大,大量的对应当存在于堆中。
2.如果对象需要继承和多态特征,用类(玩家、怪物)。
什么时候用结构呢?
结构使用简单,并且很有用,但是要牢记:结构在堆栈中创建,是值类型,而类是引用类型。每当需要一种经常使用的类型,而且大多数情况下该类型只是一些数据时,使用结构能比使用类获得更佳性能。
使用抽象类是为了代码的复用,而使用接口的动机是为了实现多态性。
抽象类适合用来定义某个领域的固有属性,也就是本质,接口适合用来定义某个领域的扩展功能。
抽象类
1.当2个或多个类中有重复部分的时候,我们可以抽象出来一个基类,如果希望这个基类不能被实例化,就可以把这个基类设计成抽象类。
2.当需要为一些类提供公共的实现代码时,应优先考虑抽象类。因为抽象类中的非抽象方法可以被子类继承下来,使实现功能的代码更简单。
接口
当注重代码的扩展性跟可维护性时,应当优先采用接口。
①接口与实现它的类之间可以不存在任何层次关系,接口可以实现毫不相关类的相同行为,比抽象类的使用更加方便灵活;
②接口只关心对象之间的交互的方法,而不关心对象所对应的具体类。接口是程序之间的一个协议,比抽象类的使用更安全、清晰。一般使用接口的情况更多。
每个虚函数都会有一个与之对应的虚函数表,该虚函数表的实质是一个指针数组,存放的是每一个对象的虚函数入口地址。对于一个派生类来说,他会继承基类的虚函数表同时增加自己的虚函数入口地址,如果派生类重写了基类的虚函数的话,那么继承过来的虚函数入口地址将被派生类的重写虚函数入口地址替代。那么在程序运行时会发生动态绑定,将父类指针绑定到实例化的对象实现多态。
解决值类型和引用类型在函数内部改值或者重新申明能够影响外部传入的变量让其也被修改。
就是在申明参数的时候前面加上ref和out的关键字即可,传入参数时同上。
ref传入的变量必须初始化但是在内部可改可不改。
out传入的变量不用初始化但是在内部必须修改该值(必须赋值)。
托管代码: 在公共语言运行时(CLR)控制下运行的代码。
非托管代码: 不在公共语言运行时(CLR)控制下运行的代码。
不安全(Unsafe)代码: 不安全代码可以被认为是介于托管代码和非托管代码之间的。不安全代码仍然在公共语言运行时(CLR)控制下运行,但它将允许您直接通过指针访问内存。
委托是约束集合中的一个类,而不是一个方法,相当于一组方法列表的引用,可以便捷的使用委托对这个方法集合进行操作。委托是对函数指针的封装。
接口介绍
接口是约束类应该具备功能的集合,约束了类应该具备哪些功能,使类从复杂的逻辑中解脱出来,方便类的管理和拓展,同时解决类的单继承问题。
使用情况
接口:无法继承的场所
完全抽象的场所
多人协作的场所
委托:多由于事件的处理
事件可以看做成委托中的一个变量。
事件是基于委托的存在,事件是委托的安全包裹 让委托的使用更具有安全性。
详细情况:从使用层面上了解委托和事件的区别 - 陈哈哈 - 博客园
类型 |
字节数 |
bool -> System.Boolean |
布尔型,其值为 true 或者 false |
byte -> System.Byte |
字节型,占 1 字节,表示 8 位正整数,范围 0 ~ 255 |
sbyte -> System.SByte |
带符号字节型,占 1 字节,表示 8 位整数,范围 -128 ~ 127 |
char -> System.Char |
字符型,占有两个字节,表示 1 个 Unicode 字符 |
short -> System.Int16 |
短整型,占 2 字节,表示 16 位整数,范围 -32,768 ~ 32,767 |
ushort -> System.UInt16 |
无符号短整型,占 2 字节,表示 16 位正整数,范围 0 ~ 65,535 |
uint -> System.UInt32 |
无符号整型,占 4 字节,表示 32 位正整数,范围 0 ~ 4,294,967,295 |
int -> System.Int32 |
整型,占 4 字节,表示 32 位整数,范围 -2,147,483,648 到 2,147,483,647 |
float -> System.Single |
单精度浮点型,占 4 个字节 |
ulong -> System.UInt64 |
无符号长整型,占 8 字节,表示 64 位正整数,范围 0 ~ 大约 10 的 20 次方 |
long -> System.Int64 |
长整型,占 8 字节,表示 64 位整数,范围大约 -(10 的 19) 次方 到 10 的 19 次方 |
double -> System.Double |
双精度浮点型,占8 个字节 |
特殊:bool:true/false
1:byte、char
2:char、short
4:int,float
8:long、double
rPoint1 = new RefPoint(1);
和谐、自然的变化
里式替换原则中,父类容器可以装载子类对象,子类可以转换成父类。比如string转object,感受是和谐的。
逆常规、不正常的变化
里式替换原则中,父类容器可以装载子类对象,但是子类对象不能装载父类。所以父类转换为子类,比如object转string,感受是不和谐的。
协变和逆变是用来修饰泛型的,用于泛型中修饰字母,只有泛型接口和泛型委托能使用.
作用:
//1.返回值与参数
//用out修饰的泛型,只能作为返回值
delegate T Testout();
//用in修饰的泛型,只能作为参数
delegate T TestIn(T t);
可以在加载程序运行时,动态获取和加载程序集,并且可以获取到程序集的信息反射即在运行期动态获取类、对象、方法、对象数据等的—种重要手段。
反射面向对象体现
之前了解的面向对象是基于类实现,而反射中就是基于程序集实现,只不过把类再用程序集包裹了一下,封装是把一些属性方法封装到一个类中,限制其数据修改的程度,那多加一层皮(程序集 ) 就是一个道理了,继承多态就是和类一样,把类换成程序集去理解。
允许在运行时发现并使用编译时还不了解的类型以及成员。
1.根据目标类型的字符串搜索扫描程序集的元数据的过程耗时。
2.反射调用方法或属性比较耗时。(首先必须将实参打包成数组,在内部,反射必须将这些实参解包到线程栈上。可以使用多态避免反射操作)
通过反射去获取对象的一个实例
反射可以直接访问类的构造,直接通过getConstructor,去访问这个构造函数,然后通过不同的参数列表,就可以具体的定位到哪一个构造的重载,通过这个方法,去得到类的实例,把对象就拿到了。
当删除遍历节点后面的节点时,会导致List.Count进行变化,删除元素后,当根据i++,遍历到删除的节点会发生异常。
处理
可以从后往前元素元素,即删除在访问的前面
原理:两个引用指向内存中同一份数据。
实际游戏场景中,首先的问题就是A掉血 B也会掉血,并且最大的问题是A死了一般情况是要删除,那么B引用会报空,可能出现未知的错误。