引言

烦躁死了,我最近一直在疯狂踩坑c#的引用类型值类型,虽然在c++中也学过那个String类的深度拷贝和浅拷贝,涉及过这部分的知识点。但是这里我还是想针对c#的语言特性做个总结。

为什么,因为老子肉眼定位不到自己的bug,但是知道肯定是因为这个特性没用好导致的。

概念介绍

引用类型:基类为Objcet

值类型:均隐式派生自
System.ValueTyp
值类型
byte,short,int,long,float,double,decimal,char,bool 和 struct 统称为值类型。
引用类型
string 和 class统称为引用类型。

  • 值类型变量声明后,不管是否已经赋值,编译器为其分配内存。
  • 引用类型当声明一个类时,只在栈中分配一小片内存用于容纳一个地址,而此时并没有为其分配堆上的内存空间。当使用 new 创建一个类的实例时,分配堆上的空间,并把堆上空间的地址保存到栈上分配的小片空间中。
  • 值类型的实例通常是在线程栈上分配的(静态分配),但是在某些情形下可以存储在堆中。 引用类型的对象总是在进程堆中分配(动态分配)。
  • unity 引用查询 unity值类型和引用类型_c#

unity 引用查询 unity值类型和引用类型_传引用_02

值类型在栈内分配空间大小因变量类型而异;
引用类型在栈内的空间大小相同;

概念深化

C#有以下一些引用类型:

  • 数组(派生于System.Array)

用户用定义的以下类型:

  • 类:class(派生于System.Object);
  • 接口:interface(接口不是一个“东西”,所以不存在派生于何处的问题。Anders在《C# Programming
    Language》中说,接口只是表示一种约定[contract]);
  • 委托:delegate(派生于System.Delegate)
  • object(System.Object的别名);
  • 字符串:string(System.String的别名)

可以看出:

  • 引用类型与值类型相同的是,结构体也可以实现接口;
  • 引用类型可以派生出新的类型,而值类型不能;
  • 引用类型可以包含null值,值类型不能(可空类型功能允许将 null 赋给值类型);
  • 引用类型变量的赋值只复制对对象的引用,而不复制对象本身。而将一个值类型变量赋给另一个值类型变量时,将复制包含的值。

对于最后一条,经常混淆的是string。我曾经在一本书的一个早期版本上看到String变量比string变量效率高;我还经常听说String是引用类型,string是值类型,等等。例如:

string s1 = "Hello, ";

string s2 = “world!”;

string s3 = s1 + s2;//s3 is “Hello, world!”

这确实看起来像一个值类型的赋值。再如:

string s1 = “a”;

string s2 = s1;

s1 = “b”;//s2 is still “a”

改变s1的值对s2没有影响。这更使string看起来像值类型。实际上,这是运算符重载的结果,当s1被改变时,.NET在托管堆上为s1重新分配了内存。这样的目的,是为了将做为引用类型的string实现为通常语义下的字符串。

使用情况分析

经常听说,并且经常在书上看到:值类型部署在栈上,引用类型部署在托管堆上。实际上并没有这么简单。

MSDN上说:托管堆上部署了所有引用类型。这很容易理解。当创建一个应用类型变量时:

object reference = new object();

关键字new将在托管堆上分配内存空间,并返回一个该内存空间的地址。左边的reference位于栈上,是一个引用,存储着一个内存地址;而这个地址指向的内存(位于托管堆)里存储着其内容(一个System.Object的实例)。下面为了方便,简称引用类型部署在托管推上。

数组

考虑数组:

int[] reference = new int[100];

根据定义,数组都是引用类型,所以int数组当然是引用类型(即reference.GetType().IsValueType为false)。

而int数组的元素都是int,根据定义,int是值类型(即reference[i].GetType().IsValueType为true)。那么引用类型数组中的值类型元素究竟位于栈还是堆?

如果用WinDbg去看reference[i]在内存中的具体位置,就会发现它们并不在栈上,而是在托管堆上。

实际上,对于数组:

TestType[] testTypes = new TestType[100];

如果TestType是值类型,则会一次在托管堆上为100个值类型的元素分配存储空间,并自动初始化这100个元素,将这100个元素存储到这块内存里。

如果TestType是引用类型,则会先在托管堆为testTypes分配一次空间,并且这时不会自动初始化任何元素(即testTypes[i]均为null)。等到以后有代码初始化某个元素的时候,这个引用类型元素的存储空间才会被分配在托管堆上。

类型嵌套

unity 引用查询 unity值类型和引用类型_unity 引用查询_03

结合unity的理解

Vector3本质是结构体(struct),所以是值类型。

组件(component)本质是类(class),所以是引用类型。
UnityEngine.Object类

使用情况分析深化

重点来了同志们,啊,我的眼睛,我的脑袋…我被恶心死了

无论是浅拷贝与深拷贝,C#都将源对象中的所有字段复制到新的对象中。不过,对于值类型字段,引用类型字段以及字符串类型字段的处理,两种拷贝方式存在一定的区别(见下表)。

unity 引用查询 unity值类型和引用类型_unity 引用查询_04


一般对C#中传值调用和传引用调用的理解:

  • 如果传递的参数是基元类型(int,float等)或结构体(struct),那么就是传值调用。
  • 如果传递的参数是类(class)那么就是传引用调用。
  • 如果传递的参数前有ref或者out关键字,那么就是传引用调用。

验证示例的代码如下:

using System;
public class ArgsByRefOrValue
{
    public static void Main(string[] args)
    {
        // 实验1. 传值调用--基元类型
        int i = 10;
        Console.WriteLine("before call ChangeByInt: i = " + i.ToString());
        ChangeByInt(i);
        Console.WriteLine("after call ChangeByInt: i = " + i.ToString());
        Console.WriteLine("==============================================");
        // 实验2. 传值调用--结构体
        Person_val p_val = new Person_val();
        p_val.name = "old val name";
        Console.WriteLine("before call ChangeByStruct: p_val.name = " + p_val.name);
        ChangeByStruct(p_val);
        Console.WriteLine("after call ChangeByStruct: p_val.name = " + p_val.name);
        Console.WriteLine("==============================================");
        // 实验3. 传引用调用--类
        Person_ref p_ref = new Person_ref();
        p_ref.name = "old ref name";
        Console.WriteLine("before call ChangeByClass: p_ref.name = " + p_ref.name);
        ChangeByClass(p_ref);
        Console.WriteLine("after call ChangeByClass: p_ref.name = " + p_ref.name);
        Console.WriteLine("==============================================");
        // 实验4. 传引用调用--利用ref
        Person_ref p = new Person_ref();
        p.name = "old ref name";
        Console.WriteLine("before call ChangeByClassRef: p.name = " + p.name);
        ChangeByClassRef(ref p);
        Console.WriteLine("after call ChangeByClassRef: p.name = " + p.name);
        Console.ReadKey(true);
    }
    static void ChangeByInt(int i)
    {
        i = i + 10;
        Console.WriteLine("when calling ChangeByInt: i = " + i.ToString());
    }
    static void ChangeByStruct(Person_val p_val)
    {
        p_val.name = "new val name";
        Console.WriteLine("when calling ChangeByStruct: p_val.name = " + p_val.name);
    }
    static void ChangeByClass(Person_ref p_ref)
    {
        p_ref.name = "new ref name";
        Console.WriteLine("when calling ChangeByClass: p_ref.name = " + p_ref.name);
    }
    static void ChangeByClassRef(ref Person_ref p)
    {
        p.name = "new ref name";
        Console.WriteLine("when calling ChangeByClassRef: p.name = " + p.name);
    }
}
public struct Person_val
{
    public string name;
}
public class Person_ref
{
    public string name;
}

unity 引用查询 unity值类型和引用类型_unity 引用查询_05


看起来似乎上面代码中实验3和实验4是一样的,即对于类(class)来说,不管加不加ref或out,都是传引用调用。

其实,这只是表面的现象,只要稍微改一下代码,结果就不一样了。

修改上面代码,再增加两个实验

using System;
public class ArgsByRefOrValue
{
    public static void Main(string[] args)
    {
        // 实验1. 传值调用--基元类型
        int i = 10;
        Console.WriteLine("before call ChangeByInt: i = " + i.ToString());
        ChangeByInt(i);
        Console.WriteLine("after call ChangeByInt: i = " + i.ToString());
        Console.WriteLine("==============================================");
        // 实验2. 传值调用--结构体
        Person_val p_val = new Person_val();
        p_val.name = "old val name";
        Console.WriteLine("before call ChangeByStruct: p_val.name = " + p_val.name);
        ChangeByStruct(p_val);
        Console.WriteLine("after call ChangeByStruct: p_val.name = " + p_val.name);
        Console.WriteLine("==============================================");
        // 实验3. 传引用调用--类
        Person_ref p_ref = new Person_ref();
        p_ref.name = "old ref name";
        Console.WriteLine("before call ChangeByClass: p_ref.name = " + p_ref.name);
        ChangeByClass(p_ref);
        Console.WriteLine("after call ChangeByClass: p_ref.name = " + p_ref.name);
        Console.WriteLine("==============================================");
        // 实验4. 传引用调用--利用ref
        Person_ref p = new Person_ref();
        p.name = "old ref name";
        Console.WriteLine("before call ChangeByClassRef: p.name = " + p.name);
        ChangeByClassRef(ref p);
        Console.WriteLine("after call ChangeByClassRef: p.name = " + p.name);
        Console.WriteLine("==============================================");
        // 实验5. 传引用调用--类 在调用的函数重新new一个对象
        Person_ref p_ref_new = new Person_ref();
        p_ref_new.name = "old new ref name";
        Console.WriteLine("before call ChangeByClassNew: p_ref_new.name = " + p_ref_new.name);
        ChangeByClassNew(p_ref_new);    //注意区别这里
        Console.WriteLine("after call ChangeByClassNew: p_ref_new.name = " + p_ref_new.name);
        Console.WriteLine("==============================================");
        // 实验6. 传引用调用--利用ref 在调用的函数重新new一个对象
        Person_ref p_new = new Person_ref();
        p_new.name = "old new ref name";
        Console.WriteLine("before call ChangeByClassRefNew: p_new.name = " + p_new.name);
        ChangeByClassRefNew(ref p_new);
        Console.WriteLine("after call ChangeByClassRefNew: p_new.name = " + p_new.name);
        Console.ReadKey(true);
    }
    static void ChangeByInt(int i)
    {
        i = i + 10;
        Console.WriteLine("when calling ChangeByInt: i = " + i.ToString());
    }
    static void ChangeByStruct(Person_val p_val)
    {
        p_val.name = "new val name";
        Console.WriteLine("when calling ChangeByStruct: p_val.name = " + p_val.name);
    }
    static void ChangeByClass(Person_ref p_ref)
    {
        p_ref.name = "new ref name";
        Console.WriteLine("when calling ChangeByClass: p_ref.name = " + p_ref.name);
    }
    static void ChangeByClassRef(ref Person_ref p)
    {
        p.name = "new ref name";
        Console.WriteLine("when calling ChangeByClassRef: p.name = " + p.name);
    }
    static void ChangeByClassNew(Person_ref p_ref_new)
    {
        p_ref_new = new Person_ref();   //这样不改变 p_ref_newp_ref_new
        p_ref_new.name = "new ref name";
        Console.WriteLine("when calling ChangeByClassNew: p_ref_new.name = " + p_ref_new.name);
    }
    static void ChangeByClassRefNew(ref Person_ref p_new)
    {
        p_new = new Person_ref();
        p_new.name = "new ref name";
        Console.WriteLine("when calling ChangeByClassRefNew: p_new.name = " + p_new.name);
    }
}
public struct Person_val
{
    public string name;
}
public class Person_ref
{
    public string name;
}

unity 引用查询 unity值类型和引用类型_引用类型_06

停一下,看吐了没。我看吐了。我要截个图来分析。。(这就舒服多了…)

unity 引用查询 unity值类型和引用类型_传引用_07

内存分析

实验3

unity 引用查询 unity值类型和引用类型_引用类型_08

从图中我们可以看出实参new出来之后就在托管堆上分配了内存,并且在栈上保存了对象的指针。

调用函数ChangeByClass后,由于没有ref参数,所以将栈上的实参p_val拷贝了一份作为形参,注意这里p_val(实参)和p_val(形参)是指向托管堆上的同一地址。

所以说没有ref时,即使参数为引用类型(class)时,也可算是一种传值调用,这里的值就是托管堆中对象的地址(0x1000)。

调用函数ChangeByClass后,通过p_val(形参)修改了name属性的值,由于p_val(实参)和p_val(形参)是指向托管堆上的同一地址,所以函数外的p_val(实参)的name属性也被修改了。

unity 引用查询 unity值类型和引用类型_传引用_09

实验5

unity 引用查询 unity值类型和引用类型_引用类型_10


函数ChangeByClassNew中,对p_val(形参)重新分配了内存(new操作),使其指向了新的地址(0x1100),如下图:

unity 引用查询 unity值类型和引用类型_c#_11


所以p_val(形参)的name属性改了时候,p_val(实参)的name属性还是没变。实验6:

unity 引用查询 unity值类型和引用类型_传引用_12


参数中加了ref关键字之后,其实传递的不是托管堆中对象的地址(0x1000),而是栈上p_val(实参)的地址(0x0001)。

所以这里实参和形参都是栈上的同一个东西,没有什么区别了。我觉得这才是真正的传引用调用。

然后调用了函数ChangeByClassRefNew,函数中对p_val(形参)重新分配了内存(new操作),使其指向了新的地址(0x1100)。

unity 引用查询 unity值类型和引用类型_传引用_13


由于p_val(形参)就是p_val(实参),所以p_val(形参)的name属性改变后,函数ChangeByClassRefNew外的p_val(实参)的name属性也被改变了。

而原先分配的对象(地址0x1000)其实已经没有被引用了,随时会被GC回收。

其实就是和c++那套指针,堆栈一样一样。。。就是c#把* & 这些操作符都隐藏起来了,他奶奶的。一点都不直观。

总结

啊!恶心死了!我还是想写c++…

unity为啥不用c++做开发语言!!!!!!!!!!!!!!!