Unity中有关C#的几个要点

C#

浏览数:98

2019-8-30

AD:资源代下载服务

大致总结下C#中几个常见知识点的个人理解,以对基础已经有所了解为前提。

ref/out

  • 区别:ref参数需要在传入前就初始化,out参数在函数结束前需要至少赋值一次。
  • 用处:1.需要返回多个数值的时候 2.需要修改值类型参数 或者 不希望传入值类型参数时发生Copy。
  • 原理:ref参数解析出来,其实传入的是参数的存储地址,类似C++的&地址符。out也一样。从而实现像修改引用类型一样,修改值类型。
    深度思考ref和out及其使用情景
void RefOutTest()
{
    int a;
    // refTest(ref a);//error: a需要先初始化
    a = 1;
    refTest(ref a);

    outTest(out a);

    Debug.Log("out - " + a);// out - 2
}

void RefTest(ref int a)
{
}

void OutTest(out int a)
{
    a = 2;
}

string

  • string 是引用类型哦~!看着像是值类型,实际上是因为每次对string操作,它都会返回一个新的string,所以看起来就跟值类型一样,直接改变了值。
  • 比较惊讶的是!同样的字符串内容,在堆内存中只保留一份。感觉有点像是,一种字符串,便是一个静态类实例。
    net中String是引用类型还是值类型
string str1 = "abc";
string str2 = "abc";
Debug.Log(object.ReferenceEquals(str1, str2)); //true
Debug.Log(object.ReferenceEquals(str1, "abc")); //true!!

装拆箱

  • 装箱:从值类型到引用类型,从栈到堆。(int转为object)涉及到 1.在堆中开辟空间 2.把值类型的值复制到新开辟的空间中(可以理解成new了一个class,然后把复制到其中一个成员变量里)
  • 拆箱:从引用类型变回值类型,从堆到栈。(object转为int)涉及到 1.从引用获取到堆中的存储位置 2.复制到栈中
  • 因为涉及到申请内存空间,当已分配的内存空间不足时,就会触发GC,引起后续一系列性能消耗,所以要避免频繁的装箱拆箱。

迭代器

  • IEnumrable:IEnumrator GetEnumrator()。
  • IEnumrator: object Current,bool MoveNext(),Reset()。
  • Enumrator实际上是被解析成一个类,类通过简单的状态机(MoveNext)控制实现迭代器。而IEnumrable提供一个获取迭代器的方法,本质上是 new 一个 Enumrator类,并返回。
  • Enumrator状态分为 before,running,suspended,after 四个状态,在MoveNext中通过switch-case来实现状态机。
  • 迭代器是迭代器模式和状态模式的混合。
  • StartCoroutine的参数类型是IEnumrator,而不是IEnumrable。
  • foreach 会自动调用 IEnumrable.GetEnumrator 从而通过迭代器实现遍历。
    匹夫细说C#:庖丁解牛迭代器,那些藏在幕后的秘密
void Start()
{
    var enumerator = EnumerableTest().GetEnumerator();

    Debug.LogError(enumerator.Current);
    Debug.LogError(enumerator.MoveNext());
    Debug.LogError(enumerator.Current);

    //两个输出结果一致
    StartCoroutine(EnumerableTest().GetEnumerator());
    StartCoroutine(EnumeratorTest());
}

IEnumerable EnumerableTest()
{
    Debug.Log(1);
    yield return new WaitForSeconds(.5f);
    Debug.Log(2);
    yield return new WaitForSeconds(.5f);
    Debug.Log(3);
}

IEnumerator EnumeratorTest()
{
    var enumerator = EnumerableTest().GetEnumerator();
    while(enumerator.MoveNext())
    {
        yield return enumerator.Current;
    }
}

GC(Garbage Collector 垃圾收集器)

  • GC大致过程:首先需要挂起所有托管线程,然后扫描还活着的对象,释放空间,最后恢复线程。扫描过程可能会改变数据在堆中的存放位置,腾出新空间,将已使用和空间的空间重新排列分开,于是同时也要改变栈上堆上的指针指向,如果空间还不够,则要申请更多的空间。
  • GC会自动定时执行,也可手动执行。
  • 减少GC,就要减少不必要的堆内存分配了。比如使用对象池复用一些频繁创建的对象。
  • 影响GC速度的是堆中对象的数量,越多则越慢。
    C#知识点扫盲-GC

委托和事件

  • 委托本质上是一个类,继承了System.MulticastDelegate。

  • 三个重要的非公有字段: _target (该委托的实例引用,比如委托指向的方法为类a的某个方法则 target=a), _methodPtr(回调函数(方法)的句柄),_invocationList(一个委托数组,通过System.Delegate.Combine连接两个委托就是将后面的委托加到前面委托的 _invocationList中)。

  • 委托实际上是包装了 执行操作的对象 和 对象要执行的方法 的包装器(类)。

  • 事件本质上是对System.Delegate和委托的封装而已,根本上是通过调用System.Deleagte.Combine等方法对delegate操作的一个委托管理器。
    匹夫细说C#:庖丁解牛聊委托,那些编译器藏的和U3D给的

闭包

  • 闭包:一个方法除了能和传递给它的参数交互之外,还可以同上下文进行更大程度的互动。实际上就是匿名函数会把使用到的变量都保存到自己的类中。
  • 所以局部变量不一定就在栈上,也可能在堆上。(类中的值类型成员是和该类一起存放于堆中的)

容器(数据结构)

Array 数组

  • 固定长度
  • 连续存储

ArrayList 数组

  • 类型不安全(存储类型为object),会发生装箱拆箱,也因此可以存放各种不同类型的值。
  • 可变长,插入方便。

List<T> 数组

  • 类型安全的ArryList。

LinkedList<T> 链表

  • 不连续存储。
  • 增删插入都很方便,只需要修改next指针,但是查找就没数组来的方便了,数组直接用下标。

Queue<T> 队列

  • 本质是数组,环形的数组。
  • 通过一个 head 和 tail 变量来指定该数组的头和尾。
  • 长度不够时,也会和数组一样自动扩容。(因为本质就是用数组来存储数据的)

Stack<T> 栈

  • 本质是数组,垂直的数组。
  • 头和尾不用指定了,就是0和count-1了。
  • 长度不够,自动扩容。同上。

HashTable 散列表

  • 类型不安全。
  • 通过特定的哈希函数,对Key加工,得到一个压缩后的哈希值(数字组成,用来当做数组下标)。哈希值是数组下标!?
  • 冲突避免机制:选择合适的哈希函数
  • 冲突解决机制:如果不同的Key得到了一样的哈希值,这时要采取一定的策略(有规则的)保存他们。
  • HashTable采用的双重哈希(二度哈希)来解决冲突,通过更换哈希函数来解决,据说如果发生冲突,则会导致所有哈希被重新计算。

Dictionary<K,T> 字典

  • 类型安全的HashTable。
  • 采用链接技术(Chaining)来解决冲突,用额外的数据结构来处理冲突。
  • 一个字典,实际上由 int[] buckets 和 Entry[] entries 两个数组组成。
  • buckets保存指定哈希值对应的数据在哈希表中的下标。
  • entries保存数据链表,即Entry实际上是一个链表类型的数据结构。
  • 如果两个数据拥有同一个哈希值,那么他们会以链表的形式,连接在同一个Entry中。

使用

  • 数组:元素数量固定,需要使用下标
  • 链表:当元素需要能够在列表两端添加时,否则使用List<T>
  • 队列和栈:明确的场景 FIFO,LILO
  • 哈希表和字典:需要使用键值对(key-value)来快速添加和查找,并且元素没有特定顺序时。

作者:Nick_Can