1、【重点面试题】面向对象的三大特性 封装 :隐藏对象的属性,并实现细节(方法),对外提供接口, public全局,protected子类,internal同集,隐藏private 同类,public属性器,private字段,对赋值进行限定。 sealed修饰符的子类是不能被继承的。
设计上:分而治之,封装变化、高内聚低耦合
数据上:把一些基本数据复合成一个自定义类型的数据
方法上:隐藏实现细节,向外提供接口
复制
继承:重用现有代码 多态:静态多态重载,动态多态重写。 父类行为由子类具体实现,包含virtual虚方法,abstract抽象方法,interface接口 可以涉及题目虚方法、抽象方法、接口的区别
重载和重写区别 静态重载:返回值无关,与参数个数,类型相关。编译阶段。便于扩展和维护 动态重写override(运行时,改写了方法表的新地址)。 抽象方法的父类必须是抽象类,子类是抽象类可以不重写,抽象类不可以被实例化。
举例子非常重要 基类抽象人物工厂(接口),子类具体玩家工厂,子类怪物工厂,子类npc工厂,抽象产品(接口)具体魔法师,弓箭手产品~ 资源管理工厂,UI资源管理工厂,音频资源管理工厂,资源管理,UI资源管理,音频资源
2、【重点面试题】值类型和引用类型区别 值类型:包含了所有简单类型(整数、浮点、bool、char)、struct、enum。 继承自System.ValueTyoe
引用类型包含了string,object,class,interface,delegate,array 继承自System.Object
内存区域上的区别 值类型:数据存储在栈上,超出作用域就自动清理 引用类型:数据存储在托管堆上,引用地址在线程栈上,地址指向数据存放的堆上 托管堆会由GC来自动释放 ,线程栈数据在作用域结束后会被清理。
拷贝策略:值类型是拷贝数据,引用类型是拷贝引用地址 如果值类型为传值参数,传值参数会在栈上新开辟一个副本,原先的值类型数据不会改变 如果引用类型是传值参数,传值参数会创建一个新的引用地址,两个引用地址会指向同一个对象实例的数据,实例数据会随着改变进行改变。(这种行为被称为副作用,一般实际项目不会这么操作,要么return返回参数,要么使用ref或者out修饰符) 【扩展Ref引用参数,Out输出参数可以利用这一副作用机制】
值类型和引用类型互相转换:拆箱和装箱 装箱:值类型====》引用类型object 1.分配内存堆 2.值类型数据拷贝到新的内存堆中 3.栈中分配一个新的引用地址指向内存堆 拆箱:引用类型object====》值类型 1.检查确保对象是给定值类型的一个装箱值 2.将该值数据复制到栈中的值类型
string是特殊的引用类型,如果传入参数是string,在方法里修改,原string数值不变。 原因是string的不变性,系统内部做了特殊处理。 链接: B站刘铁猛C#入门精要.
【重点面试题】3、装箱和拆箱的区别 值类型和引用类型的最终基类是Object 装箱:值类型转换成引用类型的过程,生成新的引用 拆箱;引用类型转换成值类型的过程
装箱操作:托管堆分配内存,值类型拷贝数据,object地址指向托管堆对象 拆箱操作:根据object引用地址找到托管堆上的数据,栈上数据拷贝 避免装箱操作,生成新的应用,解决办法就是第一是重载,第二是泛型 链接: 参考资料.
4、public、private、protected、internal、sealed的区别 public全局、private类内部、protected派生类、internal本程序集 sealed声明类就不能继承,声明方法就是不能被重写
【重点面试题】6、什么是接口,描述一下接口的成员具体实现(手撸代码和注意修饰符) 接口interface,不能定义字段,可以定义【非静态的】属性、索引器、事件、方法 默认public,但不能写任何访问修饰符 接口是引用类型,可以通过as运算符强转,获取某对象的接口的引用 接口可以继承N个接口,继承类要实现所有接口的方法
声明接口IA > 继承接口的类B > 类B实现接口所有方法 接口要小而精,定义一组方法,继承接口的派生类要实现接口的所有方法。 接口和抽象类是不能被实例化的对象(引用类型)。
public delegate void DelegateTest();
public interface ITest //只能包含非静态成员函数,隐式public,但不允许访问修饰符
{
void Method(string a); //方法
string Property //属性
{
get; set;
}
event DelegateTest EventTest; //事件,需要先定义一个委托
int this[int index] //索引器
{
get; set;
}
}
复制
举个例子:游戏门:抽象类,不能实例,很多行为,定义接口,破坏可以击碎 系统接口鼠标行为,停留进入离开 很多设计模式,是对接口的应用,面向接口编程,实现层面更加有层次。 【参考C#图解第十五章接口】
【重点面试题】7、foreach迭代器遍历和for循环遍历的区别 如果集合需要foreach遍历,是否可行,存在一定问题 foreach中的迭代变量item是的只读,不能对其进行修改,比如list.Remove(item)操作 foreach只读的时候记录下来,在对记录做操作,或者直接用for循环遍历 foreach对int[]数组循环已经不产生GC,避免对ArrayList进行遍历
for语句中初始化变量i的作用域,循环体内部可见。 通过索引进行遍历,可以根据索引对所遍历集合进行修改 unity中for循环使用lambda表达式注意闭包问题
Foreach遍历原理 任何集合类(Array)对象都有一个GetEnumerator()方法,该方法可以返回一个实现了 IEnumerator接口的对象。 这个返回的IEnumerator对象既不是集合类对象,也不是集合的元素类对象,它是一个独立的类对象。 通过这个实现了 IEnumerator接口对象A,可以遍历访问集合类对象中的每一个元素对象 对象A访问MoveNext方法,方法为真,就可以访问Current方法,读取到集合的元素。
List<string> list = new List<string>() {
"25", "哈3", "26", "花朵" };
IEnumerator listEnumerator = list.GetEnumerator();
while (listEnumerator.MoveNext())
{
Console.WriteLine(listEnumerator.Current);
}
复制
枚举器的实现(枚举器可用于读取集合中的数据,但不能用于修改集合)
链接: 参考资料.
【重点面试题】8、string和stringbuilder和stringBuffer区别 String不变性,字符序列不可变,对原管理中实例对象赋值,会重新开一个新的实例对象赋值,新开的实例对象会等待被GC。 string拼接要重新开辟空间,因为string原值不会改变,导致GC频繁,性能消耗大
StringBuffer是字符串可变对象,可通过自带的StringBuffer.方法来改变并生成想要的字符串。对原实例对象做拼接的实例,不会生成新的实例对象。 拼接使用StringBuilder和StringBuffer,只开辟一个内存空间,这是性能优化的点。
StringBuilder是字符串可变对象,基本和StringBuilder相同。 唯一的区别是StringBuffer是线程安全,相关方法前带synchronized关键字,一般用于多线程 StringBuilder是非线程安全,所以性能略好,一般用于单线程 三者性能比较 StringBuilder>StringBuffer>String
相关方法 StringBuilder.Append 将信息追加到当前 StringBuilder 的结尾。 StringBuilder.AppendFormat 用带格式文本替换字符串中传递的格式说明符。 StringBuilder.Insert 将字符串或对象插入到当前 StringBuilder 对象的指定索引处。 StringBuilder.Remove 从当前 StringBuilder 对象中移除指定数量的字符。 StringBuilder.Replace 替换指定索引处的指定字符。
9、使用List的区别 list=new list()会导致每增加一个内容就增加新内存,导致原内存浪费,GC频繁 需要添加一个固定参数,只开辟一个内存,list = new list(50) 性能优化的点
10、字符串比较 先用string 变量存储 obj.name ,这用只有一个内存空间保存 如果不存储 obj.name每一次比较都会产生新的内存空间、 比较obj.tag==”Tag“不使用,而是使用避免GC的obj.CompareTag(“tag”) 射线检测SphereColliderNoAlloc可以避免GC,比直接使用SphereCollider性能要好
【重点面试题】11、请简述GC垃圾管理器,和GC产生的原因,并描述如何避免 GC垃圾回收机制,避免堆内存溢出,定期回收那些没有有效引用的对象内存 GC优化,就是优化堆内存,减少堆内存,即时回收堆内存 GC归属于CLR
如何避免 1.减少new的次数 2.字符串拼接使用stringbuilder,字符串比较先定义一个变量存储,防止产生无效内存 3.list,new时候,规定内存大小 4.如果要射线检测,应该使用避免GC的方法XXXXNoAlloc函数 5.foreach迭代器容易导致GC(目前Unity5.5已修复),使用For循环 6.使用静态变量,GC不会回收存在的对象,但静态变量的引用对象可能被回收 7.使用枚举替代字符串变量 8.调用gameobject.tag==”XXX”就会产生内存垃圾;那么采用GameObject.CompareTag()可以避免内存垃圾的产生: 9.不要在频繁调用的函数中反复进行堆内存分配,比如OnTriggerXXX,Update等函数 10.在Update函数中,运行有规律的但不需要每一帧执行的代码,可以使用计时器,比如1秒执行一次某些代码!!! 链接: 参考文章.
12、请描述interface和抽象类之间的不同 接口是一种行为,抽象类是一种不能实例化的对象。 接口interface可以定义方法、属性、索引器、事件 抽象类abstract可以定义字段、静态字段和方法、抽象方法、属性、构造函数 接口可以继承多个接口,抽象类只能继承一个类 接口直接实现所有成员,抽象类重写override抽象方法 接口和抽象都不能被实例化,派生类必须实现基类或接口的方法 抽象类可以派生自另一个抽象类,接口可以多重实现,抽象类只能单一继承 举个例子:抽象类门,多接口继承【可破坏、金属】的行为方法,派生类实例化这个门,接口的实现类实现具体行为,派生类创建这样具体的可破坏的铁门 链接: 参考资料.
【重点面试题】13、反射的实现原理? 定义:运行时,动态获取类型信息,动态创建对象,动态访问成员的过程。 另一种定义:审查元数据并收集元数据的信息。 元数据:编译后的最基本数据单元,就是一堆表,反射就是解析这些元数据。 反射是在运行期间获取到类、对象、方法、数据的一种手段 主要使用类库System.Reflection 反射要点:如何获取类型,根据类型来动态创建对象,反射获取方法以及动态调用方法,动态创建委托 一、动态获取类型信息 1.System.Reflection.Assembly.Load(“XXXX.dll”) 动态加载程序集 2.System.Type.GetType(“XXXX类名”); //动态获取某程序集中某类信息 3.obj.GetType(); //已知对象获取类信息 ——或者——typeof(类型) //已知类类型 二、动态创建对象实例(上一步操作后获得类对象) System.Activator.CreateInstance(Type type); 三、动态访问成员调用方法(上一步操作后已获取实例对象) System.Reflection.MethodInfo method = type.GetMethod(“方法名”);//获得方法 System.Reflection.MethodInfo.Invoke(object , new object[]{参数}) //调用的类实例和实例参数
核心类 System.Reflection.Assembly 描述程序集 System.Type 描述类 System.Reflection.FieldInfo 描述了类的字段 System.Reflection.ConstructorInfo 描述构造函数 System.Reflection.MethodInfo 描述类的方法 System.Reflection.PropertyInfo 描述类的属性
反射耗性能,lua是动态语言,一种小巧的脚本语言,会使用反射机制。
知识扩展 手机端不支持编译,需要热更方案,通过lua的反射机制将旧的DLL文件替换成新的DLL文件。 Xlua是lua框架,由TX鹅肠进行维护,方便了C#与lua相互调用,C#端实现lua虚拟机 链接:参考资料太多,主要搜索,C#反射机制,lua,xlua性能等等。
14、.Net 与 Mono 的关系? .Net是一个语言平台 Mono为.Net提供集成开发环境,集成并实现了 .NET的编译器、CLR 和基础类库, 使得.Net既可以运行在windows也可以运行于 linux,Unix,Mac OS 等。
15、在类的构造函数前加上 static 会报什么错?为什么? 静态构造函数不允许添加访问修饰符,且必须无参数 原因:无论创建多少类型的对象,静态构造函数只执行一次 类实例化或者首静态成员调用之前,运行库会先调用静态构造函数 静态构造函数优先级高于任何其它构造函数 也无法使用this和base来调用静态构造函数 一个类只能有一个静态函数,如果有静态变量,系统也会自动生成静态函数
16、C# String 类型比 stringBuilder 类型的优势是什么? string功能性更强,通用性更好,用途更广泛 string不可变性,线程栈同步 编译器已将把string,并通过操作优化成stringbuilder,在性能上不差,一般可以用string代替stringbuilder
17、C# 函数 Func(string a, string b)用 Lambda 表达式怎么写? Lambda表达式(任意参数)=> { 表达式} ; => 读作goesto (a,b)=> { } ;
【重点面试题】18、C#中有哪些常用的容器类,各有什么特点,性能区别? Stack栈:先进后出,入栈和出栈,底层泛型数组实现,入栈动态扩容2倍 Queue队列:先进先出,入队和出队,底层泛型数组实现,表头表尾指针,判空还是满通过size比较 Queue和Stack主要是用来存储临时信息的
Array数组:需要声明长度,不安全 ArrayList数组列表:动态增加数组,不安全,实现了IList接口(表示可按照索引进行访问的非泛型集合对象),Object数组实现 List列表:底层实现是泛型数组,特性,动态扩容,泛型安全 将泛型数据(对值类型来说就是数据本身,对引用类型来说就是引用)存储在一个泛型数组中,添加元素时若超过当前泛型数组容量,则以2倍扩容,进而实现List大小动态可变。(注:大小指容量,不是Count) LinkList链表 1、数组和List、ArrayList集合都有一个重大的缺陷,就是从数组的中间位置删除或插入一个元素需要付出很大的代价,其原因是数组中处于被删除元素之后的所有元素都要向数组的前端移动。 2、LinkedList(底层是由链表实现的)基于链表的数据结构,很好的解决了数组删除插入效率低的问题,且不用动态的扩充数组的长度。 3、LinkedList的优点:插入、删除元素效率比较高;缺点:访问效率比较低。 C#则List和LinkedList的区别 List是数组列表,LinkedList是双向链表,List读取速度快,时间复杂度是O(1),增删比较麻烦,时间复杂度是O(n). LinkedList读取时间复杂度是O(n),增删时间复杂度是O(1)
HashTable哈希表(散列表) 概念:不定长的二进制数据通过哈希函数映射到一个较短的二进制数据集,即Key通过HashFunction函数获得HashCode 装填因子:α=n/m=0.72 ,存储的数据N和空间大小M 然后通过哈希桶算法,HashCode分段,每一段都是一个桶结构,一般是HashCode直接取余。 桶结构会加剧冲突,解决冲突使用拉链法,将产生冲突的元素建立一个单链表,并将头指针地址存储至Hash表对应桶的位置。这样定位到Hash表桶的位置后可通过遍历单链表的形式来查找元素。 1、Key—Value形式存取,无序,类型Object,需要类型转换。 2、Hashtable查询速度快,而添加速度相对慢 3、Hashtable中的数据实际存储在内部的一个数据桶里(bucket结构体数组),容量固定,根据数组索引获取值。 Directionary<TKey,TVaule>字典,有序,泛型存储不需要进行类型装换(不需要装箱拆箱),碰撞阈值扩容~ HashSet:一组不包含重复的元素集合【LeetCode算法217存在重复元素】
性能排序: 插入性能: LinkedList > Dictionary > HashTable > List 遍历性能:List > LinkedList > Dictionary > HashTable 删除性能: Dictionary > LinkedList > HashTable > List 小结: 在修改较频繁,且查找和删除也较多时,首选LinkedList, 在主要以删除为主,插入为辅,且查找较少时,首选Dictionary, 在查找频繁,而又无需修改的情况下,则首选List。
//哈希表结构体
private struct bucket {
public Object key;//键
public Object val;//值
public int hash_col;//哈希码
}
//字典结构体
private struct Entry {
public int hashCode; // 除符号位以外的31位hashCode值, 如果该Entry没有被使用,那么为-1
public int next; // 下一个元素的下标索引,如果没有下一个就为-1
public TKey key; // 存放元素的键
public TValue value; // 存放元素的值
}
private int[] buckets; // Hash桶
private Entry[] entries; // Entry数组,存放元素
private int count; // 当前entries的index位置
private int version; // 当前版本,防止迭代过程中集合被更改
private int freeList; // 被删除Entry在entries中的下标index,这个位置是空闲的
private int freeCount; // 有多少个被删除的Entry,有多少个空闲的位置
private IEqualityComparer<TKey> comparer; // 比较器
private KeyCollection keys; // 存放Key的集合
private ValueCollection values; // 存放Value的集合
复制
链接: Stack参考链接. 链接: Queue参考链接. 链接: ArrayList参考链接. 链接: List参考链接. 链接: HashTable参考链接. 链接: Dictionary参考链接.
19、C#中常规容器和泛型容器有什么区别,哪种效率高? 常规容器有拆箱和装箱操作,速度慢,消耗性能 泛型容器效率更高
20、有哪些常见的数值类? 简单数值类型:整数型、字符型、布尔型、实数型 复合类型:结构类型、枚举类型
21、C#中委托和接口有什么区别?各用在什么场合? 委托delegate:unity事件与委托密切相关,回调机制,减少对象之间数据交互 接口interface:多人协作,完全抽象,类单继承 委托是约束方法的集合 接口是约束类具备的功能集合,解决类单继承问题
22、C#中unsafe关键字是用来做什么的?什么场合下使用? unsafe 非托管代码,配合fixed一起使用 ,用在需要指针操作的场合 项目背包系统的任务装备栏使用到
【重点面试题】23、C#中ref和out关键字有什么区别?知道Ref的深层原理是什么? ref修饰引用参数。参数必须赋值,带回返回值,又进又出 out修饰输出参数。参数可以不赋值,带回返回值之前必须明确赋值, 引用参数和输出参数不会创建新的存储位置
如果ref参数是值类型,原先的值类型数据,会随着方法里的数据改变而改变, 如果ref参数值引用类型,方法里重新赋值后,原对象堆中数据会改变,如果对引用类型再次创建新对象并赋值给ref参数,引用地址会重新指向新对象堆数据。方法结束后形参和新对象都会消失。实参还是指向原始对象,值不够数据改变了 【参考C#图解教程:引用类型作为值参数和引用参数】
24、For,foreach,Enumerator.MoveNext的使用,与内存消耗情况 for通过索引或下标一次进行遍历 foreach和Enumerator.MoveNext通过迭代进行遍历 内存消耗本质没有多少区别 迭代器有一个状态机 before running:yield return 或 yield break 或迭代结束 after
25、函数中多次使用string的+=处理,会产生大量内存垃圾(垃圾碎片),有什么好的方法可以解决。 使用stringbuilder的append
26、当需要频繁创建使用某个对象时,有什么好的程序设计方案来节省内存? Unity对象池 设计单例模式全局实例化一次
27、Foreach循环迭代时,若把其中的某个元素删除,程序报错,怎么找到那个元素?以及具体怎么处理这种情况?(注:Try…Catch捕捉异常,发送信息不可行) foreach迭代器不能进行操作 在循环中记录索引值或者key值,在迭代结束后,查找到这个元素,在进行删除操作
28、GameObject a=new GameObject() GameObject b=a 实例化出来了A,将A赋给B,现在将B删除,问A还存在吗? 存在 a引用地址在线程栈中,数据内容在托管堆中 b引用地址在线程栈中,数据内容指向A的托管堆中的内容 B删除,只是删除b的引用地址
【重点面试题】29、C#引用和C++指针的区别 C#不支持指针,但可以使用Unsafe,不安全模式,CLR不检测 C#可以定义指针的类型、整数型、实数型、struct结构体 C#指针操作符、C#指针定义 使用fixed,可以操作类中的值类型 相同点:都是地址 指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名。 不同点: 指针是个实体,引用是个别名。 sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小; 引用是类型安全的,而指针在不安全模式下
【重点面试题】30、using的作用 资源:实现了IDisposable接口的类或结构。 using语句确保这些资源能够被适当的释放(Resource.Dispose) using原理 using(分配资源){ 使用资源 } ——> 释放资源 (隐式) 使用资源(可能会导致异常)会被放进Try块里,释放资源(有无异常)都会放进在finally块
using(分配资源)
{
try{
使用资源 }
finally{
Resource.Dispose}
}
复制
using指令,using+命名空间(或命名空间一个类型) 在源文件的顶端声明 也可以不使用using,直接命名空间.类.成员方法
【重点面试题】31、字典Dictionary的内部实现原理 泛型集合命名空间using System.Collections.Generic; 任何键都必须是唯一
该类最大的优点就是它查找元素的时间复杂度接近O(1),实际项目中常被用来做一些数据的本地缓存,提升整体效率。 实现原理 1.哈希算法:将不定长度的二进制数据集给映射到一个较短的二进制长度数据集一个Key通过HashFunc得到HashCode 2.Hash桶算法:对HashCode进行分段显示,常用方法是对HashCode直接取余 3.解决碰撞冲突算法(拉链法):分段会导致key对应的桶会相同,拉链法的思想就像对冲突的元素,建立一个单链表,头指针存储到对应的哈希桶位置。反之就是通过确定hash桶位置后,遍历单链表,获取对应的value
Key值 HashFunc Buckets桶 Entries入口(最小数据结构)
Dictionary字典中最小的数据结构体Entry,调用Add(Key,Value)方法添加的元素都会被封装在这样的一个结构体中。
private struct Entry {
public int hashCode; // 除符号位以外的31位hashCode值, 如果该Entry没有被使用,那么为-1
public int next; // 下一个元素的下标索引,如果没有下一个就为-1
public TKey key; // 存放元素的键
public TValue value; // 存放元素的值
}
复制
Collection版本控制,字典重要变量version,这个变量,在每一次新增、修改和删除操作时,都会使version++ 之后每一次迭代过程都会检查版本号是否一致,如果不一致将抛出异常。 这样就避免了在迭代过程中修改了集合,造成很多诡异的问题。
链接: Dictionary实现原理资料.
【重点面试题】32、泛型是什么 多个代码对 【不同数据类型】 执行 【相同指令】的情况 泛型:多个类型共享一组代码 泛型允许类型参数化,泛型类型是类型的模板 5种泛型:类、结构、接口、委托、方法 类型占位符 T 来表示泛型
泛型类不是实际的类,而是类的模板 从泛型类型创建实例 声明泛型类型》通过提供【真实类型】创建构造函数类型》从构造类型创建实例 类<T1,T2> 泛型类型参数
性能:泛型不会强行对值类型进行装箱和拆箱,或对引用类型进行向下强制类型转换,所以性能得到提高 安全:通过知道使用泛型定义的变量的类型限制,编译器可以在一定程度上验证类型假设,所以泛型提高了程序的类型安全。
【重点面试题】33、结构体和类有什么区别