C#基础知识

面向对象的三大特点
继承
封装
多态性
简述值类型和引用类型
介绍:
值类型:包含了所有简单类型(整数、浮点、bool、char)、struct、enum。
继承自System.ValueType
引用类型包含了string,object,class,interface,delegate,array
继承自System.Object
区别:
值类型存储在内存栈中,引用类型数据存储在内存堆中,而内存单元中存放的是堆中存放的地址。
值类型存取快,引用类型存取慢。
值类型表示实际数据,引用类型表示指向存储在内存堆中的数据的指针和引用。
栈的内存是自动释放的,堆内存是.NET 中会由 GC 来自动释放。
值类型继承自 System.ValueType,引用类型继承自 System.Object。
值类型在栈中存储的是直接的值,引用类型数据本身实在堆中,栈中存放的是一个引用的地址。
ArrayList和 List的主要区别
● ArrayList 不带泛型 数据类型丢失
● List 带泛型 数据类型不丢失
● ArrayList 需要装箱拆箱 List不需要

GC 相关知识点
GC的概念
C#内部有两个内存管理池:堆内存和栈内存。栈内存(stack)主要用来存储较小的和短暂的数据,堆内存(heap)主要用来存储较大的和存储时间较长的数据。C#中的变量只会在堆栈或者堆内存上进行内存分配,变量要么存储在栈内存上,要么处于堆内存上。
只要变量处于激活状态,则其占用的内存会被标记为使用状态,则该部分的内存处于被分配的状态。
一旦变量不再激活,则其所占用的内存不再需要,该部分内存可以被回收到内存池中被再次使用,这样的操作就是内存回收。处于栈上的内存回收及其快速,处于堆上的内存并不是及时回收的,此时其对应的内存依然会被标记为使用状态。不再使用的内存只会在GC的时候才会被回收。
垃圾回收主要是指堆上的内存分配和回收,C#中会定时对堆内存进行GC操作。
GC会带来的问题
游戏性能:GC操作是一个极其耗费事件的操作,堆内存上的变量或者引用越多则导致遍历检查时的操作变得十分缓慢,使得游戏运行缓慢,例如当CPU处于游戏性能的关键时刻,任何一个操作就会导致游戏帧率下降,造成极大的影响。
游戏内存:(unityGC采用的是非分代非压缩的标记清除算法)GC操作会产生“内存碎片化”。当一个单元内存从堆中分配出来,其大小取决于存储变量的大小。当内存被回收到堆上时,有可能被堆内存分割成碎片化的单元。(就是说总容量大小时固定的,但是单元内存较小。例如房子很大,房间很小,找不到合适的房间)即下次分配时找不到合适的储存单元,就会触发GC操作,或者堆内存扩容操作,导致GC频发发生和游戏内存越来越大。
GC触发时机
在堆内存上进行内存分配操作,而内存不够的时候都会触发垃圾回收来利用闲置的内存;
GC会自动的触发,不同平台运行频率不—样;
GC可以被强制执行。
如何避免GC?
减少临时变量的使用,多使用公共对象,多利用缓存机制。(将容器定义到函数外,用到容器的时候进行修改即可)
减少new对象的次数。
对于大量字符串拼接时,将StringBuilder代替String。(string不可修改性,修改即创建一个新的string对象,旧的直接抛弃等待GC,但少量字符串拼接用string,性能优于stringbuilder)
使用扩容的容器时,例如:List,StringBuilder等,定义时尽量根据存储变量的内存大小定义储存空间,减少扩容的操作。(扩容后,旧的容器直接抛弃等待GC)
代码逻辑优化:例如计时器当大于1s后才进行文本修改,而不是每帧都修改,或者禁止在关键时候GC,影响游戏性能,可以在加载页面或者进度条的时候GC。
利用对象池:对象池是一种Unity经常用到的内存管理服务,针对经常消失生成的对象,例如子弹,怪物等,作用在于减少创建每个对象的系统开销。在我们想要对象消除时,不直接Destory,而是隐藏起来SetActive(false),放入池子中,当需要再次显示一个新的对象时,先去池子中看有没有隐藏对象,有就取出来(显示) SetActive(true),没有的话,再实例化。
减少装箱拆箱( 装箱是将值类型转换为 object 类型或由此值类型实现的任何接口类型的过程)的操作
协程: yeild return 0 会产生装箱拆箱,可以替换为 yeild return null。

结构体和类
结构体和类的区别
结构体是值类型,类是引用类型。
结构体存在栈中,类存在堆中。
结构体变量和类对象进行类型传递时,结构体变量进行的就是值传递,而类对象进行的是引用传递,或者说传递的是指针,这样在函数中改变参数值,结构体对象的值是不变的,而类对象的值是变化了。
在C#中结构体类型定义时,成员是不能初始化的,这样就导致了,定义结构体变量时,变量的所有成员都要自己赋值初始化。但对于类,在定义类时,就可以初始化其中的成员变量,所以在定义对象时,对象本身就已经有了初始值,你可以自己在重新给个别变量赋值。(注意在C++中,类的定义中是不能初始化的,初始化要放在构造函数中)
结构体不能申明无参的构造函数,而类可以。
声明了结构类型后,可以使用new运算符创建构造对象,也可以不使用new关键字。如果不使用new,那么在初始化所有字段之前,字段将保持未赋值状态且对象不可用。
结构体申明有参构造函数后,无参构造不会被顶掉。
结构体不能申明析构函数,而类可以。
结构体不能被继承,而类可以。
结构体需要在构造函数中初始化所有成员变量,而类随意。
结构体不能被静态static修饰(不存在静态结构体),而类可以。

C#中四种访问修饰符

  1. 属性修饰符
    ● Serializable:按值将对象封送到远程服务器。
    ● STATread:是单线程套间的意思,是⼀种线程模型。
    ● MATAThread:是多线程套间的意思,也是⼀种线程模型。
  2. 存取修饰符
    ● public:对任何类和成员都公开,无限制访问
    ● private:只有包含该成员的类可以存取。
    ● internal:只能在包含该类的程序集中访问该类
    ● protected:只有包含该成员的类以及派⽣类可以存取。
  3. 类修饰符
    ● abstract:抽象类。指示⼀个类只能作为其它类的基类。
    ● sealed:密封类。指示⼀个类不能被继承。理所当然,密封类不能同时⼜是抽象类,因为抽象总是希望被继承的。
  4. 成员修饰符
    ● abstract:指示该⽅法或属性没有实现。
    ● sealed:密封⽅法。可以防⽌在派⽣类中对该⽅法的override重载。不是类的每个成员⽅法都可以作为密封⽅法密封⽅法,必须对基类的虚⽅法进⾏重载,提供具体的实现⽅法。所以,在⽅法的声明中,sealed修饰符总是和override修饰符同时使⽤。
    ● delegate:委托。⽤来定义⼀个函数指针。C#中的事件驱动是基于delegate + event的。
    ● const:指定该成员的值只读不允许修改。
    ● event:声明⼀个事件。
    ● extern:指示⽅法在外部实现。
    ● override:重写。对由基类继承成员的新实现。
    ● readonly:指示⼀个域只能在声明时以及相同类的内
    ● 部被赋值。
    ● static:指示⼀个成员属于类型本身,⽽不是属于特定的对象。即在定义后可不经实例化,就可使⽤。
    ● virtual:指示⼀个⽅法或存取器的实现可以在继承类中被覆盖。
    ● new:在派⽣类中隐藏指定的基类成员,从⽽实现重写的功能。 若要隐藏继承类的成员,请使⽤相同名称在派⽣类中声明该成员,并⽤ new 修饰符修饰它。
    堆和栈的区别
    GC方面:栈保持着先进后出的原则,是一片连续的内存域,有系统自动分配和维护,产生的垃圾系统自动释放。而堆是无序的,他是一片不连续的内存域,用户自己来控制和释放,如果用户自己不释放的话,当内存达到一定的特定值时,通过垃圾回收器(GC)来回收。
    存储方面:栈通常保存着我们代码执行的步骤,如方法变量等等。而堆上存放的则多是对象,数据等。我们可以把栈想象成一个接着一个叠放在一起的盒子(越高内存地址越低)。当我们使用的时候,每次从最顶部取走一个盒子,当一个方法(或类型)被调用完成的时候,就从栈顶取走接着下一个。堆则不然,像是一个仓库,储存着我们使用的各种对象等信息,跟栈不同的是他们被调用完毕不会立即被清理掉。
    缓存方面:栈使用的是一级缓存,他们通常都是被调用时处于存储空间中,调用完毕立即释放;堆是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。
    存储方面:栈(Stack)是一种先进后出的数据结构,在内存中,变量会被分配在栈上来进行操作。堆(heap)是用于为引用类型的实例(对象),分配空间的内存区域,在堆上创建一个对象,会将对象的地址传给栈上的变量(反过来叫变量指向此对象,或者变量引用此对象)-----也就是栈上的变量指向了堆上地址为XXX的实例(对象)。
    String StringBuilder
    ● String主要用于公共 API,通用性好、用途广泛、读取性能高、占用内存小。
    ● StringBuilder主要用于拼接 String,修改性能好。
    ● 不过现在的编译器已经把String的 + 操作优化成 StringBuilder 了, 所以一般用String 就可以了
    ● String是不可变的,所以天然线程同步。
    ● StringBuilder可变,非线程同步。
    虚函数实现原理
    每个虚函数都会有一个与之对应的虚函数表,该虚函数表的实质是一个指针数组,存放的是每一个对象的虚函数入口地址。对于一个派生类来说,他会继承基类的虚函数表同时增加自己的虚函数入口地址,如果派生类重写了基类的虚函数的话,那么继承过来的虚函数入口地址将被派生类的重写虚函数入口地址替代。
    那么在程序运行时会发生动态绑定,将父类指针绑定到实例化的对象实现多态。

指针和引用的区别
引用不能为空,即不存在对空对象的引用,指针可以为空,指向空对象。
引用必须初始化,指定对哪个对象的引用,指针不需要。
引用初始化后不能改变,指针可以改变所指对象的值。
引用访问对象是直接访问,指针访问对象是间接访问。
引用的大小是所引用对象的大小,指针的大小,是指针本身大小,通常是4字节。
引用没有const,指针有const
引用和指针的+自增运算符意义不同。
引用不需要分配内存空间,指针需要。
常用的容器类
● Stack栈:先进后出,入栈和出栈,底层泛型数组实现,入栈动态扩容2倍
● Queue队列:先进先出,入队和出队,底层泛型数组实现,表头表尾指针,判空还是满通过size比较Queue和Stack主要是用来存储临时信息的
● Array数组:需要声明长度,不安全
● ArrayList数组列表:动态增加数组,不安全,实现了IList接口(表示可按照索引进行访问的非泛型集合对象),Object数组实现
● List列表:底层实现是泛型数组,特性,动态扩容,泛型安全
○ 将泛型数据(对值类型来说就是数据本身,对引用类型来说就是引用)存储在一个泛型数组中,添加元素时若超过当前泛型数组容量,则以2倍扩容,进而实现List大小动态可变。(注:大小指容量,不是Count)
● LinkList链表
○ 1、数组和List、ArrayList集合都有一个重大的缺陷,就是从数组的中间位置删除或插入一个元素需要付出很大的代价,其原因是数组中处于被删除元素之后的所有元素都要向数组的前端移动。
○ 2、LinkedList(底层是由链表实现的)基于链表的数据结构,很好的解决了数组删除插入效率低的问题,且不用动态的扩充数组的长度。
○ 3、LinkedList的优点:插入、删除元素效率比较高;缺点:访问效率比较低。
● HashTable哈希表(散列表)
概念:不定长的二进制数据通过哈希函数映射到一个较短的二进制数据集,即Key通过HashFunction函数获得HashCode
装填因子:α=n/m=0.72 ,存储的数据N和空间大小M
然后通过哈希桶算法,HashCode分段,每一段都是一个桶结构,一般是HashCode直接取余。
桶结构会加剧冲突,解决冲突使用拉链法,将产生冲突的元素建立一个单链表,并将头指针地址存储至Hash表对应桶的位置。这样定位到Hash表桶的位置后可通过遍历单链表的形式来查找元素。
○ 1、Key—Value形式存取,无序,类型Object,需要类型转换。
○ 2、Hashtable查询速度快,而添加速度相对慢
○ 3、Hashtable中的数据实际存储在内部的一个数据桶里(bucket结构体数组),容量固定,根据数组索引获取值。
性能排序:
插入性能: LinkedList > Dictionary > HashTable > List
遍历性能:List > LinkedList > Dictionary > HashTable
删除性能: Dictionary > LinkedList > HashTable > List
ref和out关键字
ref 关键字 是作用是把一个变量的引用传入函数,和 C/C++ 中的指针几乎一样,就是传入了这个变量的栈指针。
out 关键字 的作用是当你需要返回多个变量的时候,可以把一个变量加上 out 关键字,并在函数内对它赋值,以实现返回多个变量。
JIT和AOT区别
Just-In-Time -实时编译
执行慢安装快占空间小一点
Ahead-Of-Time -预先编译
执行快安装慢占内存占外存大
反射的实现原理
可以在加载程序运行时,动态获取和加载程序集,并且可以获取到程序集的信息反射即在运行期动态获取类、对象、方法、对象数据等的一种重要手段。
主要使用的类库:System.Reflection
核心类:

  1. Assembly描述了程序集
  2. Type描述了类这种类型
  3. ConstructorInfo描述了构造函数
  4. MethodInfo描述了所有的方法
  5. FieldInfo描述了类的字段
  6. PropertyInfo描述类的属性
    通过以上核心类可在运行时动态获取程序集中的类,并执行类构造产生类对象,动态获取对象的字段或属性值,更可以动态执行类方法和实例方法等。
    //实现步骤:
    //导⼊using System.Reflection;
    Assembly.Load(“程序集”)加载程序集,返回类型是
    //⼀个Assembly
    foreach (Type type in assembly.GetTypes())
    {
    string t = type.Name;
    }
    //得到程序集中所有类的名称
    Type type = assembly.GetType(“程序集.类名”);

//获取当前类的类型
Activator.CreateInstance(type);

//创建此类型实例
MethodInfo mInfo = type.GetMethod(“⽅法名”);

//获取当前⽅法
mInfo.Invoke(null,⽅法参数);

字典Dictionary的内部实现原理
泛型集合命名空间using System.Collections.Generic;
任何键都必须是唯一
该类最大的优点就是它查找元素的时间复杂度接近O(1),实际项目中常被用来做一些数据的本地缓存,提升整体效率。
实现原理
● 哈希算法:将不定长度的二进制数据集给映射到一个较短的二进制长度数据集一个Key通过HashFunc得到HashCode
● Hash桶算法:对HashCode进行分段显示,常用方法是对HashCode直接取余
● 解决碰撞冲突算法(拉链法):分段会导致key对应的桶会相同,拉链法的思想就像对冲突的元素,建立一个单链表,头指针存储到对应的哈希桶位置。反之就是通过确定hash桶位置后,遍历单链表,获取对应的value
装箱拆箱
C#装箱是将值类型转换为引用类型;
拆箱是将引用类型转换为值类型;
非托管代码与不安全代码
● 托管代码: 在公共语言运行时(CLR)控制下运行的代码。
● 非托管代码: 不在公共语言运行时(CLR)控制下运行的代码。
● 不安全(Unsafe)代码: 不安全代码可以被认为是介于托管代码和非托管代码之间的。不安全代码仍然在公共语言运行时(CLR)控制下运行,但它将允许您直接通过指针访问内存。
Heap与Stack有何区别?

  1. heap是堆,stack是栈。
  2. stack的空间由操作系统自 动分配和释放,heap的空间是手动申请和释放的, heap常用new关键字来分配。
  3. stack空间有限,heap 的空间是很大的自由区。
    栈溢出一般是由什么原因导致
  4. 无限递归。函数递归调用时,系统要在栈中不断保存函数调用时的现场和产生的变量,如果递归调用太深,就会造成栈溢出,这时递归无法返回。再有,当函数调用层次过深时也可能导致栈无法容纳这些调用的返回地址而造成栈溢出。
  5. 无限循环。
  6. 大量局部变量分配。
  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值