c# 知识记录

1、C#代理和C++指针的异同点
委托就是一种引用方法的类型。委托与分配的方法具有相同的行为,委托可以看作是函数的抽象,委托的实例是一个具体的函数。
----委托VS函数指针
委托和函数指针都描述了函数或者是方法,并且通过统一的接口调用不同的实现。两者之间的区别
(1)委托是函数的抽象,委托对象是真正的对象。函数指针只是指向函数入口的地址,不具有对象性质。
(2)委托对象可以指向不同类的方法,只要符合委托要求。而C++函数不能指向不同类的方法。
(3)在应用时,C++的指针没有C#的委托灵活。

委托与事件:
2、事件封装了委托类型的变量,使得在类的内部,不管你声明它是public还是protected,它总是private的。
在类的外部,注册“+=”和注销“-=”的访问限定符与你在声明事件时使用的访问符相同。
事件的声明与委托变量delegate的声明唯一的区别是多了一个event关键字。所以声明一个事件不过类似于声明一个进行了封装的委托类型的变量而已。
总结主要区别:

1.事件在类外绑定事件只能用“+=”(“-=”解绑),不能使用‘=’绑定;如果在类内部绑定事件可以用‘=’;

2.委托在类内类外都可以用"+="、"-="、"=";

3.事件的调用(执行)只能在类内调用;委托(公有)在类内类外都可以调用。事件调用比较安全。

2、struct和类的异同点。
类与结构的区别:
(1)C#允许在类里对字段进行初始化,不允许在结构里也这样做。结构里的字段将被自动设置为0或NULL。
(2)C#不允许在结构里声明一个无参构造器,C#做出这一限制的目的是为了加快基于结构的数组的创建速度。
(3)继承能力是类和结构的重要区别之一。结构不支持继承机制。所有的结构都是从System.ValueType类间接地派生出来的,而System.ValueType类又是从System.Object类派生出来的。但C#不允许程序员声明一个继承自某个结构的类,也不允许程序员声明一个继承自另一个结构的结构。一旦声明了一个结构,从System.ValueType类一直延续到这个结构的“家谱线”就走到尽头了。
(4)结构是值类型,类是引用类型。

4、数组,arrylist和list和异同点,
数组
优点:比如说数组在内存中是连续存储的,所以它的索引速度是非常的快,而且赋值与修改元素也很简单
缺点:在数组的两个数据间插入数据也是很麻烦的。还有我们在声明数组的时候,必须同时指明数组的长度,数组的长度过长,会造成内存浪费,数组和长度过短,会造成数据溢出的错误。

ArrayList是.Net Framework提供的用于数据存储和检索的专用类,它是命名空间System.Collections下的一部分。它的大小是按照其中存储的数据来动态扩充与收缩的。所以,我们在声明ArrayList对象时并不需要指定它的长度。

ArrayList继承了IList接口,所以它可以很方便的进行数据的添加,插入和移除.

List
数据类型要求相同,类型安全,不需要频繁装箱拆箱。可视为是ArrayList的改进,底层实现与ArrayList一样,都使用Array

字典存储的内部逻辑

Dictionary<Tkey,Tvalue>是Hastbale的泛型实现。
Dictionary和HashTable内部实现差不多,但前者无需装箱拆箱操作,效率略高一点。查找删除时间复杂度o(1) 排序时间复杂度o(logN)

Hashtable 和 Dictionary <K, V> 类型

1):单线程程序中推荐使用 Dictionary, 有泛型优势, 且读取速度较快, 容量利用更充分.
2):多线程程序中推荐使用 Hashtable, 默认的 Hashtable 允许单线程写入, 多线程读取, 对 Hashtable 进一步调用 Synchronized()方法可以获得完全线程安全的类型. 而Dictionary 非线程安全, 必须人为使用 lock 语句进行保护, 效率大减.
3):Dictionary 有按插入顺序排列数据的特性 (注: 但当调用 Remove() 删除过节点后顺序被打乱), 因此在需要体现顺序的情境中使用 Dictionary 能获得一定方便.

HashTable中的key/value均为object类型,由包含集合元素的存储桶组成。存储桶是 HashTable中各元素的虚拟子组,与大多数集合中进行的搜索和检索相比,存储桶可令搜索和检索更为便捷。每一存储桶都与一个哈希代码关联,该哈希代码是使用哈希函数生成的并基于该元素的键。HashTable的优点就在于其索引的方式,速度非常快。如果以任意类型键值访问其中元素会快于其他集合,特别是当数据量特别大的时候,效率差别尤其大。

HashTable的应用场合有:做对象缓存,树递归算法的替代,和各种需提升效率的场合。

对于C# 中的Dictionary,具体实现如下
(1)最小数据单元是一个叫Entry的结构体。用entry[]存放元素
(2)buckets[]用于存放hash桶。
(3)数据Add进字典的时候,会根据Key生成一个hashcode,根据这个hashcode去决定该数据所在的桶。
每个桶中会存放一个整型值,这个值指向桶中存放的第一个元素(真正数据都在entry数组中)。entry中的每一个值都有next指针,从而指向下一个值(拉链法)。
(4)Remove的时候,把把要删除元素的前一个节点的next指针置为-1即可
(5)C#中碰撞次数的阈值是100,碰撞过多或者数组已满的时候就说明该扩容了。扩容过程是:
①如果是数组满了:直接申请两倍于现在大小的buckets[]、entries[],然后将现有元素拷贝到新的数组中
②如果是碰撞过多:在.net framework中会直接扩容,并重新计算哈希值,在.net core中采取和JDK相似的策略(使用红黑树来存储元素)

Array顺序存储的线性表、定长不支持(这里的插入与删除指会更改表长的行为)O(N)
LinkedList链式存储的线性表、不定长 O(1) O(N)
List顺序存储的线性表、不定长、动态扩容 O(N),结尾则是O(1) O(N)
Stack栈、不定长、动态扩容 O(1) 只能访问栈顶
Queue队列、不定长、动态扩容 O(1) 只能访问队列头部
Dictionary<K,T>保存键值对、使用开散列法、不定长、动态扩容、长度总为质数 O(1) O(1)
SortedList<K,T>保存键值对、内部使用数组实现、保持有序、不定长 O(logN) O(logN)
SortedDictionary<K,T>保存键值对、内部使用红黑树实现、保持有序、不定长 O(logN) O(logN)
HashSet使用开散列法、不定长、动态扩容、长度总为质数、是不含值的字典,故复杂度和它完全相同 O(1) O(1)
SortedSet内部使用红黑树实现、保持有序、不定长、是不含值的SortedDictionary<K,T>,故复杂度和它完全相同 O(logN) O(logN)

树的深度和广度遍历
树的深度搜索与树的前序遍历同理,根节点->左孩子->右孩子;树的广度搜索与树的层次遍历同理,一层一层遍历内容。深度搜索采用stack的适配器,先进后出原则;而广度搜索采用的queue适配器,先进先出原则,二者正好满足搜索需求。

图的深度和广度遍历
深度遍历:从图中某个顶点v出发,访问此顶点,然后从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问到。其更适合:目标比较明确,以找到目标为主要目的的情况。
广度遍历:类似于树中的层序遍历,首先遍历完和某一顶点v相连的所有顶点,然后再遍历和这些顶点相连的顶点,以此类推。其更适合于:不断扩大遍历范围时找到相对最优解的情况。

链表和循环链表的使用.
双向链表
单链表的一个优点是结构简单,但是它也有一个缺点,即在单链表中只能通过一个结点的引用访问其后续结点,而无法直接访问其前驱结点,
要在单链表中找到某个结点的前驱结点,必须从链表的首结点出发依次向后寻找,但是需要Ο(n)时间。
为此我们可以扩展单链表的结点结构,使得通过一个结点的引用,不但能够访问其后续结点,也可以方便的访问其前驱结点。
扩展单链表结点结构的方法是,在单链表结点结构中新增加一个域,该域用于指向结点的直接前驱结点。
双向链表是通过上述定义的结点使用 pre 以及 next 域依次串联在一起而形成的
在双向链表中同样需要完成数据元素的查找、插入、删除等操作。在双向链表中进行查找与在单链表中类似,只不过在双向链表中查找操作可以从链表的首结点开始,也可以从尾结点开始,但是需要的时间和在单链表中一样,在使用双向链表实现链接表时,为使编程更加简洁,我们使用带两个哑元结点的双向链表来实现链接表。其中一个是头结点,另一个是尾结点,它们都不存放数据元素,头结点的pre 为空,而尾结点的 Next 为空。在具有头尾结点的双向链表中插入和删除结点,无论插入和删除的结点位置在何处,因为首尾结点的存在,插入、删除操作都可以被归结为某个中间结点的插入和删除;并且因为首尾结点的存在,整个链表永远不会为空,因此在插入和删除结点之后,也不用考虑链表由空变为非空或由非空变为空的情况下 head 和 tail 的指向问题;从而简化了程序。

在一个循环链表中, 首节点和末节点被连接在一起。这种方式在单向和双向链表中皆可实现。
要转换一个循环链表,你开始于任意一个节点然后沿着列表的任一方向直到返回开始的节点。
循环链表可以被视为"无头无尾"。循环链表中第一个节点之前就是最后一个节点,反之亦然。
循环链表的无边界使得在这样的链表上设计算法会比普通链表更加容易。对于新加入的节点应该是在第一个节点之前还是最后一个节点之后可以根据实际要求灵活处理,区别不大。另外有一种模拟的循环链表,就是在访问到最后一个节点之后的时候,手工跳转到第一个节点。访问到第一个节点之前的时候也一样。这样也可以实现循环链表的功能,在直接用循环链表比较麻烦或者可能会出现问题的时候可以用。

栈 与 堆内存 的GC问题,
1、栈区(stack)
由编译器自动分配释放 ,存放函数的参数值,局部变量的值等,内存的分配是连续的,类似于平时我们所说的栈,如果还不清楚,那么就把它想成数组,它的内存分配是连续分配的,即,所分配的内存是在一块连续的内存区域内.当我们声明变量时,那么编译器会自动接着当前栈区的结尾来分配内存.
2、堆区(heap)
一般由程序员分配释放, 若程序员不释放,程序结束时可能由操作系统回收.类似于链表,在内存中的分布不是连续的,它们是不同区域的内存块通过指针链接起来的.一旦某一节点从链中断开,我们要人为的把所断开的节点从内存中释放.堆都是动态分配的,没有静态分配的堆
3、全局区(静态区)(static)
全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 程序结束后由系统释放
4、文字常量区
常量字符串就是放在这里的。 程序结束后由系统释放
5、程序代码区
存放函数体的二进制代码。

分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由 malloc 函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。

分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是 C/C++ 函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

堆和栈还有几点不同:
1、申请后系统的响应

栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。

堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。

另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
2、申请大小的限制

栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。

堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
3、申请效率的比较:

栈由系统自动分配,速度较快。但程序员是无法控制的。

堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。

另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一块内存,虽然用起来最不方便。但是速度快,也最灵活。
4、堆和栈中的存储内容

栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。

当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。

堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。
5、堆和栈上的内存操作越界
1>堆内存越界主要是操作的内存超过了calloc/malloc/new等在堆上分配内存函数所分配的大小,后果导致下次calloc/malloc/new的失败,malloc失败发生_int_malloc错误(引起abort)大多是这种情况引起的;
2>栈内存越界的情况大多出现在对数组的操作上,数组下标超过了数组定义的长度,后果导致覆盖其他变量。

设计模式,

AssetBundle打包的资源是存在依赖关系的,你打包了这个资源依赖于另一个资源他会自动的把依赖资源也打进这个包,如果不注意打包的策略,就会发生同一个资源被重复的打包,产生资源冗余,比如你要把不同的UI类型分开打包,他们都会把自己使用的图集都打到AB包中,这样直接进行打包,那这个贴图就会被打入AB包两次,很浪费资源,所以在打包的时候要注意打包策略
UI公用资源包打成一个包
模型动画可以根据角色来分开打包
特效部分可以打入一个包里
声音部分可以打到一个包里
打包前先使用AssetDatabase.CollectDependencies遍历所有资源收集他们间的依赖关系,在后面打包的时候按照每个资源被依赖的深度进行分级,先打包级别较低的,如shader,script这些资源被其他资源依赖但不会依赖别的资源,级别最低。如prefab依赖前面的所有资源,级别最高,放在最后打包。一般是按照资源的类型(prefab,mesh,animator,texture,script…)进行分级。即使这样按类型分好级后仍是不够的,因为同一级的资源也有可能产生相互依赖的关系。比如使用NGUI,一个面板prefab依赖于几个挂UIAtlas的prefab,这种同级的依赖需要用深度优先遍历对他们进行排序以确定依赖关系。这个依赖关系使用序列化文件记录下来,供后面加载包的时候先加载所有被依赖的包使用。每次更新的时候这个依赖关系的序列化文件也要同其他资源一起更新。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

虾米神探

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值