C#中的引用类型以及特殊引用类型详解

 更新时间:2022年07月31日 09:43:17   作者:Darren Ji  
本文详细讲解了C#中的引用类型以及特殊引用类型,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

基本

哪些属于引用类型

类(object,string),接口、数组、委托

引用类型分配在哪里

  • 引用类型变量位于线程栈。
  • 引用类型实例分配在托管堆上。
  • 当引用类型实例的大小小于85000bytes,被分配在GC堆上,当大于或等于85000bytes,被分配在LOH(Large Object Heap)上。

变量(Variable),对象(Object),实例(Instance)

变量:
变量分配在线程栈上。
变量可以是值类型,也可以是引用类型。
当变量是引用类型时,包含了对对象的引用(内存地址),也叫做"对象引用"。

对象:
对类、接口、委托和数组等的一个抽象描述。

实例:
在堆上创建的对象,称为对象实例。

引用类型没有new意味着什么?

Object a = null;
Console.WriteLine(a.ToString());
运行报错"未将对象引用设置到对象实例"。
意思是,在线程栈上创建的变量a没有指向到堆上的对象实例。

托管堆上的垃圾回收

GC会遍历所有托管堆上的对象,按照一定的递归遍历算法,对那些没有被引用的不可访问对象实施回收。

new的背后发生了什么

    class Program 
    { 
        static void Main(string[] args) 
        { 
            Person p; 
            p = new Person(20); 
        } 
    }

    public class Person 
    { 
        public int _age;

        public Person(int age) 
        { 
            _age = age; 
        }

        public Person() 
        { 
            
        } 
    }

另外,引用类型的值,比如这里的引用类型Person中的值_age也被分配在托管堆上。   

线程栈上的2个变量引用同一个对象实例的内存地址

线程栈上的2个变量引用同一个对象实例的内存地址,改变其中一个变量的值会影响到另外一个变量。

    class Program 
    { 
        static void Main(string[] args) 
        { 
            Person p1 = new Person(20); 
            Person p2 = p1; 
            Console.WriteLine("没有改变时p2的年龄是:" + p2._age + "岁"); 
            p1._age = 18; 
            Console.WriteLine("改变p1的值,p2的年龄也被改变了,现在是:" + p2._age + "岁,真好,年轻了!"); 
            Console.ReadKey(); 
        } 
    }

    public class Person 
    { 
        public int _age;

        public Person(int age) 
        { 
            _age = age; 
        }

        public Person() 
        { 
            
        } 
    }

string类型是特殊的引用类型

特殊性体现在

从应用角度体现了值类型语义,从内存角度实现为引用类型存储,位于托管堆。

什么是string

可以看作是char的集合char[]    

string创建与实例化

string str = "Hello";

以下错误
String str = new String("Hello");
因为System.String没有提供此构造函数

以下可以
Char[] cs = {'a', 'b','c'};
String strArr = new String(cs);
但很少使用这种方式。

字符串的恒定性Immutability

是指字符串一经创建,就不可改变。
字符串一旦创建,在托管堆上分配一块连续的内存空间。

恒定性的好处:
对String对象的任意操作,不会改变原字符串。
操作字符串不会出现线程同步的问题。
成就了字符串驻留。

恒定性的不足:
因为恒定性,对字符串的任何操作,比如字符串比较,字符串链接,字符串格式化等都会创建新的字符串,这样造成内存与性能的双重损耗。如下:

public static void Main() 
{ 
    string str = "This is a test about immuntablility of string type."; 
    Console.WriteLine(str.Inseert(0,"Hi").Substring(19).ToUpper()); 
    Console.WriteLine(str); 
}

由于Insert,Substring,ToUpper这些方法,都会创建出新的临时字符串,而这些新的字符串不被其他代码引用的时候,就会被垃圾回收,造成性能上的损失。

恒定性的前提,String为密封类:

public sealed class String:IComparable, ICloneable,IConvertible,IComparable<string>,IEnumerable<char>,IEnumerable,IEquatable<string>

字符串驻留String Interning

MSDN对于字符驻留的定义:公共语言运行库通过维护一个哈希表(Hash Table)来存放字符串,该表成为拘留池,也叫驻留池。

字符串驻留弥补了恒定性的不足:
对于相同的字符串,CLR不会不会为其分配内存空间,而是共享同一内存。
CLR内部维护了一个哈希表HashTable来管理其创建的大部分String对象。key是string本身,value是string对应的内存地址。

驻留的2个静态方法:
public static string Intern(string str);
当str位于作为key位于CLR的驻留池时,返回对str的引用,否则将str字符串添加到hash table中,作为key,并返回引用。

public static string IsInterned(string str);
当str位于作为key位于CLR的驻留池时,返回对str的引用,否则返回null引用,也不添加到hash table中。

字符串驻留是进程级的

可以跨应用程序域AppDomain而存在,驻留池在CLR加载时创建,分配在System Domain中,被进程所有AppDomain所共享,其生命周期不受GC控制。

例子1:

        static void Main(string[] args) 
        { 
            string strA = "ab"; 
            string strB = "ab"; 
            Console.WriteLine(ReferenceEquals(strA,strB)); 
            string strC = "a"; 
            string strD = strC + "b"; 
            Console.WriteLine(ReferenceEquals(strA, strD)); 
            strD = String.Intern(strD); 
            Console.WriteLine(ReferenceEquals(strA,strD)); 
            Console.ReadKey(); 
        }

返回:

true
false
true

分析:

  • strA与strB内容相同,在hash table中的key相同,对应了一样的引用地址,所以返回true。
  • strD的内容虽然与strA相同,但由于是动态生成的,不会把hash table中key为ab的引用地址赋值给strD,所以strA与strD引用地址不一样,返回false。
  • strD = String.Intern(strD);手动对strD实施驻留,并发现hash table中已经有了ab这个key,就把对应的引用地址赋值给了strD,这样,strA与strD引用地址相同,返回true。

例子2:

        static void Main(string[] args) 
        { 
            string s1 = "abc"; 
            string s2 = "ab"; 
            string s3 = s2 + "c"; 
            Console.WriteLine(string.IsInterned(s3) ?? "null"); 
            Console.WriteLine(ReferenceEquals(s1,s3)); 
            Console.ReadKey(); 
        }

返回:

abc
false   

分析:

  • string.IsInterned(s3)对s3进行手动驻留,发现hash table中abc这个key,于是,就返回abc的引用地址。但并没有把引用地址赋值给s3。
  • s1和s3的引用地址还是不一样,返回false。

例子3:

        static void Main(string[] args) 
        { 
            string strA = "abcdef"; 
            string strC = "abc"; 
            string strD = strC + "def"; 
            Console.WriteLine(ReferenceEquals(strA,strD)); 
            string StrE = "abc" + "def"; 
            Console.WriteLine(ReferenceEquals(strA,StrE)); 
            Console.ReadKey(); 
        }

返回:

False,       
true

分析:

  • 因为strD = strC + "def"中strD的内容虽然与strA想同,但因为是动态生成的,不会被添加到hash table中,所以引用地址不一样,返回false。
  • "abc"+"def在IL中呈现为abcdef,不是动态生成的,并且发现hash table中已经有了abcdef这个key,于是就把对应的引用地址赋值给了strE,这样strA和StrE就有了相同的引用地址,返回true。如图:

例子4:

        static void Main(string[] args) 
        { 
            string s = "abc"; 
            //IsInterned()获取字符串变量的引用 
            Console.WriteLine(string.IsInterned(s) ?? "null"); 
            Console.ReadKey(); 
        }

返回

"abc"。

分析:通过string s = "abc"使得hash table中有abc这个key,当进行string.IsInterned(s)手动驻留判断的时候,发现有abc这个key,就把对应的引用地址返回。

例子5:

    class Program 
    { 
        static void Main(string[] args) 
        { 
            string s = "ab"; 
            s += "c"; 
            //IsInterned()获取字符串变量的引用 
            Console.WriteLine(string.IsInterned(s) ?? "null"); 
            Console.ReadKey(); 
        } 
    }

返回

null

分析:通过string s = "ab";使得hash table中有了ab这个key,s += "c"是动态拼接,不会把abc放到hashtable中,所以当通过string.IsInterned(s)手动驻留判断的时候,发现没有abc这个key,就返回null。

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对脚本之家的支持。如果你想了解更多相关内容请查看下面相关链接

相关文章

  • 浅拷贝和深拷贝深入理解(shallow copy VS deep copy)

    浅拷贝和深拷贝深入理解(shallow copy VS deep copy)

    浅拷贝和深拷贝深入理解(shallow copy VS deep copy) 本文重点讨论引用类型变量的拷贝机制和实现
    2014-01-01
  • C#引用类型作为方法的参数分析

    C#引用类型作为方法的参数分析

    这篇文章主要介绍了C#引用类型作为方法的参数分析,以实例的形式较为详细的分析了参数的传值问题,需要的朋友可以参考下
    2014-11-11
  • C#中结构体定义并转换字节数组详解

    C#中结构体定义并转换字节数组详解

    在写C#TCP通信程序时,发送数据时,只能发送byte数组,处理起来比较麻烦不说,如果是和VC6.0等写的程序通信的话,很多的都是传送结构体,在VC6.0中可以很方便的把一个char[]数组转换为一个结构体,而在C#却不能直接把byte数组转换为结构体,要在C#中发送结构体,应该怎么做呢?
    2017-11-11
  • C#代码实现PDF文档操作类

    C#代码实现PDF文档操作类

    本篇文章给大家介绍使用pdf文档操作C#代码,本文代码非常简单,代码附有注释,需要注意的是:需要添加itextsharp.dll引用才可以正常通过编译,感兴趣的朋友可以参考下
    2015-10-10
  • C#异常处理详解

    C#异常处理详解

    这篇文章介绍了C#异常处理,有需要的朋友可以参考一下
    2013-10-10
  • C# 一个WCF简单实例

    C# 一个WCF简单实例

    以订票为例简单应用wcf程序,需要的朋友可以参考下
    2012-10-10
  • C# 使用EntityFramework CodeFirst 创建PostgreSQL数据库的详细过程

    C# 使用EntityFramework CodeFirst 创建PostgreSQL数据库的详细过程

    这篇文章主要介绍了C#使用EntityFramework CodeFirst创建PostgreSQL数据库的过程,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-07-07
  • C#加密app.config中连接字符串的方法

    C#加密app.config中连接字符串的方法

    这篇文章主要介绍了C#加密app.config中连接字符串的方法,涉及C#配置文件加密的相关实现技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-07-07
  • unity自定义弹出框功能

    unity自定义弹出框功能

    这篇文章主要为大家详细介绍了unity自定义弹出框功能,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-11-11
  • C#用NPOI导出导入Excel帮助类

    C#用NPOI导出导入Excel帮助类

    这篇文章主要为大家详细介绍了C# NPOI导出导入Excel帮助类,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-02-02

最新评论