C#值类型与引用类型

类型

C#中的类型指的是{类,结构,接口,枚举,委托}中的任意一个成员。类型(type)和(class)不同,后者是前者的的一个特殊情况,任何拥有某个类型的(value)被称为某类型的一个实例(instance)。

类型分类

类型可以分为值类型以及引用类型,没有第三种情况

  • 值类型:结构和枚举
  • 引用类型:类、接口、指针、字符串、委托、数组

引用类型Reference Type

内存布局

引用类型的内存分配永远是两部分

  1. 引用它的对象
  2. 堆上的一个对象

其中堆上的对象可以被如下的对象引用:

  • 栈上的一个变量(最常见的情况)
  • P/Invoke情形下的句柄表
  • Finalizer queue终结队列
  • 寄存器在这里插入图片描述
    引用类型在申请内存时,需要计算它本身所需要的内存以及它的父类成员需要的内存,一直算到System.Object(不过它没有成员,所以一般没有指定父类的引用类型计算内存就只需要计算它自己就够了,因为对于没有指定父类的引用类型来说,其父类为System.Object)。

内存布局(以32位机为例):
4. 同步块索引:内存的分配从同步块索引开始,它占据4个字节,栈上的引用指向同步块索引的后边的部分,所以同步块索引占据-4字节到0
5. 方法表指针(又叫类型对象指针):占据4个字节(指向方法表,位于类型对象中,而类型对象一般位于同一个应用程序域的加载堆中)方法表指针与同步块索引这8个字节(64位机上为16字节,分别占据8字节)是每个引用类型都一定会有的,为了确保类型安全性,C#是无法操作它们的
6. 类型所有父对象的实例成员(静态成员存储在类型对象中),其中,所有引用类型成员都分配4字节,因为只需要分配地址,分配顺序不定,CLR会尽享消除字段对齐带来的负面影响
7. 类型自己的实例成员(静态成员存储在类型对象中),引用类型成员分配同上

默认值

所有引用类型的默认值都为null,可以通过令某个引用类型变量null,来将它与某个堆上的对象之间的关联切断,此时,该引用类型变量将不指向任何堆上的对象,称为垃圾,等待垃圾回收

同步块索引

是类的标准配置,位于类在堆上定义的开头-4(或-8)至0字节

功能

在线程同步中用来判断对象是被使用还是闲置。默认情况下,同步块索引被赋予一个特殊值,此时对象没有被线程独占。当一个线程拿到对象,并打算对其操作时,会检查对象的同步块索引。如果索引为特殊值,说明没有任何线程正在操作它,此时这个线程获得它的操作权。同时在CLR的同步块数组中添加一个新的同步块,并将该块的所引致写入实例的同步索引值中。此时如果有其他线程来访问该实例,它就不能操作这个实例了,因为它的同步块索引值部位特殊值。当独占线程操作完毕后,同步块所索引的值被重设回特殊值。

方法表指针(类型对象)

类型对象由CLR在加载堆中创建,创建时机为加载该程序集时
类型对象重要组成部分:

  1. 类型的静态字段
  2. 方法表

创建之后这两部分都不会改变,这也是静态字段全局性的由来,它们被所有的该类型的实例共享。
可通过以下代码来验证类型对象在内存上的唯一性:

var a = new AStruct();
var b = new AStruct();
Console.WriteLine(ReferenceEquals(a.GetType(), b.GetType())); // True

方法表

类型所有的方法,包括静态方法和实例方法。

扫描二维码关注公众号,回复: 5604465 查看本文章

引用类型的复制

引用类型的复制分为深复制和浅复制(默认情况)

  • 浅复制:只会复制地址本身,然后将这个地址复制给新的变量,新的对象和旧的对象同时指向堆上旧的实例对象,更改任何一个成员的值都会影响另一个
  • 深复制:会在堆上创建新的类型实例
    浅复制验证如下:
var a = new AClass();
a.a = 1;
a.b = "hey";
var b = a;
b.a = 2;
Console.WriteLine(a.a); // 输出2

执行完毕之后内存布局如下图:
在这里插入图片描述

如何实现深拷贝

通过实现ICloneable接口并实现Clone方法

public class AClass : ICloneable
{
	public int a;
	public string b;
	public AClass(int aa, string bb)
	{
		a = aa;
		b = bb;
	}
	public object Clone()
	{
		return new AClass(a, b);
	}
}

如此实现之后AClass类支持了深复制,故其内存布局变为下图所示:
在这里插入图片描述

值类型Value Type

对应类型:结构枚举

通常来说,值类型就是字面意义上的那种值,例如整数int、浮点数float/double,布尔值等。实际上,整数、浮点数、布尔值等都是结构体

Boolean类型定义

namespace System
{
	public struct Boolean : IComparable, IComparable<Boolean>, IConvertible, IEquatable<Boolean>
	{
		public static readonly string FalseString;
		public static readonly string TrueString;

		public static Boolean Parse(string value);
		public static Boolean TryParse(string value, out Boolean result);
		public int CompareTo(Boolean value);
		public int CompareTo(object obj);
		public Boolean Equals(Boolean obj);
		public override Boolean Equals(object obj);
		public override int GetHashCode();
		public TypeCode GetTypeCode();
		public override string ToString();
		public string ToString(IFormatProvider provider);
	}
}

int类型定义

using System.Globalization;

namespace System
{
	public struct Int32 : IComparable, IComparable<Int32>, IConvertible, IEquatable<Int32>, IFormattable
	{
		public const Int32 MaxValue = 2147483647;
		public const Int32 MinValue = -2147483648;

		public static Int32 Parse(string s);
		public static Int32 Parse(string s, NumberStyles style);
		public static Int32 Parse(string s, IFormatProvider provider);
		public static Int32 Parse(string s, NumberStyles style, IFormatProvider provider);
		public static bool TryParse(string s, out Int32 result);
		public static bool TryParse(string s, NumberStyles style, IFormatProvider provider, out Int32 result);
		public Int32 CompareTo(object value);
		public Int32 CompareTo(Int32 value);
		public bool Equals(Int32 obj);
		public override bool Equals(object obj);
		public override Int32 GetHashCode();
		public TypeCode GetTypeCode();
		public override string ToString();
		public string ToString(IFormatProvider provider);
		public string ToString(string format);
		public string ToString(string format, IFormatProvider provider);
	}
}

默认值

大多数值类型默认值都为0,例如整数、浮点数的默认值为0,枚举类型的默认值也为0,char的默认值为’\0’

内存分配

分为以下三种情况讨论

  1. 值类型作为局部变量
  2. 值类型作为引用类型的成员
  3. 值类型中包含引用类型

值类型作为局部变量

普通的值类型总是分配在栈上。例如简单的int为例,int i = 1,意味着在栈上开辟了一块空间存储了这个值类型,但是int是一个结构体,它存在2个值类型的成员(最大值,最小值),这两个类型为常量(const = static readonly),故其存在于加载堆中。新建一个int不会复制其最大值和最小值,其开销永远只有4字节,即其自身,即使是64位机上int也是4字节,因为其本质为Int32
对于局部变量的值类型来说,其复制将只复制值的副本,其更改不会对原值有影响

var i = 1;
var j = i;
i = 2;
Console.WriteLine(j); // 输出1

值类型作为引用类型的成员

如果值类型为引用类型的成员,则遵从引用类型的内存分配与复制方式

public class AClass
{
	public int a;
	public string b;
}

在创建一个该类的实例时,遵从引用类型的分配方式

var a = new AClass();
a.a = 1;
a.b = "hey";

执行完上述代码后,线程栈上会开辟空间存储一个引用,其指向堆上的AClass类的实例,此时的值类型a.a = 1存储在堆上
在这里插入图片描述

值类型中包含引用类型

如果一个结构体中包含了引用类型,例如包含了一个字符串或者类,则它引用的那部分会遵从引用类型的内存分配,值类型那部分则遵从值类型创建的内存分配

例如我们有如下的类和结构体

public class AClass
{
	public int a;
	public string b;
}

public struct AStruct
{
	public AClass ac;
	public double c;
}

则新建这个值类型的实例时,它的引用类型成员则会遵守引用类型的内存分配方式,值类型成员则存储在栈上

var a = new AStruct();
a.ac = new AClass();
a.c = 1;
a.ac.a = 2;
a.ac.b = "";

此时的内存分配情况如图
在这里插入图片描述
为了弄清楚其内存结构,我们复制它,并尝试修改其成员的值

var b = a;
b.c = 999;
b.ac.a = 888;
b.ac.b = "bye";

Console.WriteLine(a.c); // 1
Console.WriteLine(a.ac.a); // 888
Console.WriteLine(a.ac.b); // bye

此时内存情况如图
在这里插入图片描述
通过如下代码可以验证a.ac和b.ac指向同一个对象,而a.c和b.c没有关系

Console.WriteLine(ReferenceEquals(a.ac, b.ac)); // True
Console.WriteLine(ReferenceEquals(a.c, b.c)); // False

合适考虑使用值类型

值类型可以认为是轻量级的引用类型,其设计目的就是提高程序性能,如果所有类型都是引用类型,则会大大降低性能,主要是因为:

  • 每次新声明变量都要创建类型对象指针和同步块索引
  • 内存分配必定牵扯到堆,增加GC压力

当结构体的全部属性都是值类型时,结构体不会和堆扯上关系(例如,int就是这样的结构体),这样可以减轻GC的压力,选择结构体可在初始化时提高性能,因为其初始化不需要生成引用类型标配的类型对象指针和同步块索引

适合使用结构体的情况

  • 当对象的所有属性都需要在创建之初即赋值时
  • 当对象的全部属性都是值类型时(如果存在引用类型,就会牵扯到内存分配到堆上的问题,无法减轻GC压力)
  • 当前对象不需要被继承时

例如二维坐标(包括两个double)、长方形(包括长、宽、高)这样的对象适合使用结构体

值类型和引用类型的区别与联系

区别

  • 所有值类型隐式派生自System.ValueType。该类确保值类型所有的成员全部分配在栈上。有三个例外:
    1. 结构体如果含有引用类型的成员,该成员也会牵扯到堆的分配
    2. 静态类型,如果一个变量是静态类型,则无论它是什么类型,都会分配到加载堆上
    3. 局部变量被捕获升级为密封类
  • 引用类型的初值为null,值类型为0
  • 对于引用类型,栈中会有一个变量名和变量类型,指向堆中对象实例的地址。值类型尽有栈中的变量名和类型,没有指向实例的指针
  • 值类型不能被继承,引用类型则可以
  • 值类型的生命周期为其定义域。值类型离开其定义域后将被立刻销毁,引用类型则会进入垃圾回收分代算法,销毁时间待定
  • 值类型的构造函数必须为所有成员赋值
  • 值类型没有同步块索引,不能作为线程同步工具

联系

  • 值类型和引用类型可以通过装箱和拆箱互相转化
  • 所有类型都派生自System.ValueType,它是System.Object的子类
  • 类和结构体都可以实现接口,结构体如int,DateTime等都实现了IComparable接口,使得他们可以比较大小

猜你喜欢

转载自blog.csdn.net/u013457933/article/details/88687144