【读书笔记 (一) C#要点技术】《Unity3D高级编程之进阶主程》- 陆泽西

本文是关于个人对该文章阅读后总结的读书笔记以及要点总结。
作者及书记地址:http://www.luzexi.com/tag/Unity3D

第一章:C#要点技术:

了解Add是最直接了解底层数据结构如何运作的途径

LIST

● 通过数组实现。默认容量为4,容量不够时按倍数扩充。
● 可伸缩数组,不用手动分配大小,可当链表使用。
● new与add等操作都会有性能问题,因为大多采用线性复杂度算法
● 线程不安全,兼容性强、好用、但效率较差。

Dictionary

● 用Hash函数建立映射关系,将每个key进行哈希运算,找到自己的位置。
● 对于不同的关键字可能得到同一哈希地址,即哈希冲突
● 处理哈希冲突的方式:拉链法 - 将所有关键字为同义词的结点链接在同一个单链表中。
● Hashtable在多线程读写中是线程安全的,而 Dictionary 不是。
● 同List一样, new 时尽量确定大致数量会更加高效。
● 根据哈希创建过程,用数值方式做Key比用类实例方式作为Key值更加高效率。
由数组构成,并且由哈希函数完成地址构建,由拉链法冲突解决方式来解决冲突。

Float

● 任意一个二进制浮点数 F 均可表示为:F = (-1 ^ s) * (1.M) * (2 ^ e),即符号部分,尾数部分,阶码部分。
● 计算机先转换为二进制数:整数部分采用 “除2取余法”,小数部分采用“乘2取整法”,并把整数和小数部分用点号隔开。
精度问题:随着数值越来越大,M的尾数的存储位数没有变化,能表达的位数有限。
● 用int或者long代替浮点数可以保证一致性。

● decimal类型由定点数实现,可以缩小精度问题,但占用空间大,不能灵活类型转换。
● 可尝试自己实现定点数,整数和小数部分分开存储。

delegate

● 可以被视为一个更高级的函数指针,不仅能指向另一个函数还能传递参数。
● 不是一个C#的基本类型,创建委托等于是创建了一个delegate类的实例。
= 操作会把函数地址给存起来,函数地址变量是个可变数组(链表)。
● 重写了+=、-=操作符,相当于把函数地址推入链表尾部或移出链表。被调用时依次执行链表上的函数。

event

● 在delegate的基础上又做了一次封装,目的是限制用户直接操作委托实例中变量的权限。
不再提供“=”操作符,目的是避免委托暴漏,而不小心被“=”赋值清空了委托链表。
● 更好的维护了delegate的秩序,保证了谁注册谁销毁的原则。

装箱和拆箱

● 装:值类型转引用类型;拆:引用类型转值类型。
● 值类型:直接存储数据;引用类型:持有的数据的引用(内存地址)。
Struct是值类型,当结构体实例a赋值给b的时候,是数据拷贝,修改b不会影响a。
● 栈:先进先出,连续的内存,定位速度快,但需要确定的生命周期。堆:随机分配空间,效率较低。
● Struct可通过重载函数、泛型、继承统一接口,来避免装箱、拆箱。谨慎使用Struct。

排序算法

● 一个项目中可能90%都是排序和搜索算法,尽可能提高其效率。
快排平均性能较好,不消耗额外内存,是最常用的排序算法。
○ 从序列中选一个元素作为基准元素。
○ 把所有小于基准元素的移到左边,大于移到右边。
○ 把左右两区块递归重复上述步骤。
○ 优化方案:
■ 随机中轴数
■ 三数取中
■ 小区间插入排序
■ 缩小分割范围
堆序最大最小堆优先级队列非常有用,在不断增加和删除根节点元素的情况下获取最大最小值。
○ 完全二叉树结构,所以可以用一维数组表示。
○ 关键算法:插入新元素和删除最大最小元素。
○ 新元素放入二叉树叶子节点上,比较它与父节点大小,大则结束,小则交换,如此往复。
○ 删除元素则相反。

搜索算法

● 广度优先与深度优先是最常见的搜索算法,好的搜索算法需要数据结构支持。
二分查找法,最常用,简单,效率较好。要求数组必须是有序数组。
○ 数组分为前后区域与中间位元素。
○ 将查找值与中间位进行比较,小于则在前半部分查找,大于则在后半部分查找。
○ 继续在范围中重复上述步骤,递归查找。
● 二叉树:搜索速度由原始数据的排列方式决定、稳定性不高。
● 平衡二叉树:左右深度差绝对值补能超过1。
● 红黑树:实现平衡二叉树的算法,从而获得较高查找效率。节点附加颜色值来确保平衡。项目运用少。
● B树:为磁盘存储设计的平衡二叉树,游戏项目较少使用。
● 四叉树、八叉树:注重的是空间上的划分。碰撞检测、地图查找、渲染裁切等。

程序逻辑优化技巧

● 优秀的程序员关注代码的性能,普通的程序员关注是否完成功能。?
● 使用List和Dictionary时提高效率:
○ List的方法大多使用遍历的形式进行,注意使用频率。
○ Dictionary设置一个合理的初始大小,避免自己扩容,否则会使哈希冲突变得频繁。
■ new Dictionary<T, T>(capacity)
● 巧用Struct:
○ 在函数内部定义Struct作为局部变量,快速回收内存、无内存碎片、无垃圾回收、连续内存读取友好。
○ 连续内存可以提高CPU缓存的命中率。
● 垃圾回收:
○ 遍历所有已分配的内存块,没有引用就回收,占用大量CPU算力。
○ 图方便图好用的往往要付出性能损耗的代价,而性能高的代码通常都有点反人性。
● 尽可能使用对象池:
○ new Class和new List常常被滥用而忘记回收。
○ UI库中的对象池,对象池并不复杂,麻烦的是使用:

internal class ObjectPool<T> where T : new()
{
    
    
    private readonly Stack<T> m_Stack = new Stack<T>();
    private readonly UnityAction<T> m_ActionOnGet;
    private readonly UnityAction<T> m_ActionOnRelease;

    public int countAll {
    
     get; private set; }
    public int countActive {
    
     get {
    
     return countAll - countInactive; } }
    public int countInactive {
    
     get {
    
     return m_Stack.Count; } }

    public ObjectPool(UnityAction<T> actionOnGet, UnityAction<T> actionOnRelease)
    {
    
    
        m_ActionOnGet = actionOnGet;
        m_ActionOnRelease = actionOnRelease;
    }

    public T Get()
    {
    
    
        T element;
        if (m_Stack.Count == 0)
        {
    
    
            element = new T();
            countAll++;
        }
        else
        {
    
    
            element = m_Stack.Pop();
        }
        if (m_ActionOnGet != null)
            m_ActionOnGet(element);
        return element;
    }

    public void Release(T element)
    {
    
    
        if (m_Stack.Count > 0 && ReferenceEquals(m_Stack.Peek(), element))
            Debug.LogError("Internal error. Trying to destroy object that is already released to pool.");
        if (m_ActionOnRelease != null)
            m_ActionOnRelease(element);
        m_Stack.Push(element);
    }
}

internal static class ListPool<T>
{
    
    
    // Object pool to avoid allocations.
    private static readonly ObjectPool<List<T>> s_ListPool = new ObjectPool<List<T>>(null, l => l.Clear());

    public static List<T> Get()
    {
    
    
        return s_ListPool.Get();
    }

    public static void Release(List<T> toRelease)
    {
    
    
        s_ListPool.Release(toRelease);
    }
}

使用案例:
public class A
{
    
    
	public int a;
	public float b;
}



public void Main()
{
    
    
	Dictionary<int,A> dic2 = new Dictionary<int, A>(16);
	for(int i = 0 ; i<1000 ; i++)
	{
    
    
		A a = ObjectPool<A>.Get(); //从对象池中获取对象
		a.a = i;
		a.b = 3.5f;

		A item = null;
		if(dic.TryGetValue(a.a, out item))
		{
    
    
			ObjectPool<A>.Release(item); //值会被覆盖,所以覆盖前收回对象
		}

		dic[a.a] = a;

		int removeKey = Random.RangeInt(0,10);
		if(dic.TryGetValue(removeKey, out item))
		{
    
    
			ObjectPool<A>.Release(item); //移除时收回对象
			dic.Remove(removeKey);
		}
	}

	Dictionary<int,List<A>> dic2 = new Dictionary<int, List<A>>(1000);
	for(int i = 0 ; i<1000 ; i++)
	{
    
    
		List<A> arrayA = ListPool<A>.Get(); // 从对象池中分配List内存空间

		dic2.Add(i,arrayA);

		List<A> item = null;
		int removeKey = Random.RangeInt(0,1000);
		if(dic.TryGetValue(removeKey, out item))
		{
    
    
			ListPool<A>.Release(item); // 移除时收回对象
			dic.Remove(removeKey);
		}
	}

}

● 字符串的性能问题:
○ c#中string是引用类型,每次动态创建string时都会在堆内存中分配内存。
○ 解决方案:
■ 自建缓存机制,建立一个字典容器将其缓存起来。

Dictionary<int,string> strCache;

string strName = null;
if(!strCache.TryGetValue(id, out strName))
{
    
    
	ResData resData = GetDataById(ID);
	string strName = "This is " + resData.Name;
	strCache.Add(id, strName);
}

return strName;
    ■ 使用不安全的native方法,类似指针。
string strA = "aaa";

string strB = "bbb" + "b";

fixed(char* strA_ptr = strA)
{
    
    
	fixed(char* strB_ptr = strB)
	{
    
    
		memcopy((byte*)strB_ptr, (byte*)strA_ptr, 3*sizeof(char));
	}
}

print(strB); // 此时strB内容为 “aaab”
  ○ 字符串的隐藏问题:Length方法需要通过遍历字符串来获取长度。比如,会造成两层循环:
string str = "Hello world.";
for(int i = 0 ; i<str.Length ; ++i)
{
    
    
	// do some thing.
}

程序运行原理

● 计算机使用机器码,所有由‘1’和‘0’组成的机器指令码都能一一对应到汇编指令。
● 一个程序在内存中运行时,通常由几个内存块组成:
指令内存块:存储已编写设计好的指令,需要执行的指令会从指令内存块中取,指令计数器也不断跳跃在这些指令中。
数据内存块:存放的都是我们设置好的数据以及分配过的内存。分为静态数据块和堆内存数据。
■ 动态内存申请都来自于堆内存。
■ 可以认为是一个很长的byte数组。
类方法被编译成指令序列,放在了指令内存块中,所有方法、函数都在这里集中存放。
● 一个可执行文件或文件库里,几乎都是指令机器码,以及附带的常量数据。
栈内存通常是函数方法执行的重要部分,每分配一块内存,回收时也必须按照先进后出的秩序回收,这个规则使得栈内存永远是连续的。
● 上述汇编里的数据段、代码段、栈段三个段,分别使用了段地址和偏移量来表示数据和指令内容。

猜你喜欢

转载自blog.csdn.net/sylardie/article/details/132586934