C#类型基础

一、值类型和引用类型

1.值类型和引用类型

1)值类型

CLR提供了轻量级类型,值类型,一般在线程栈上分配,在栈内直接保存对象的值,所有的值类型都是由System.ValueType派生的,包括枚举的基类System.Enum也是派生自System.ValueType的

2)引用类型

所有的类都是引用类型,生成引用类型的对象时,会在托管堆上申请内存,来保存对象的数据,在栈上保存这个部分内存的地址,也就是说,引用类型的对象,保存的是引用(地址指针)

2.线程堆栈(Thread Stack)和托管堆(Managed Heap)

1)线程堆栈(Thread Stack)

一个进程可能有多个线程,而每个线程在创建的时候,都会分配1M大小的栈,用来存储线程运行时的数据,比如向方法传递的参数,方法内部定义的局部变量,都在栈上存储,当对象生命周期结束时,会自动释放对象在栈上的资源

2)托管堆(Managed Heap)

托管堆是由CLR管理的,我们使用new关键字创建引用类型对象时,就会在堆上给这个对象一个足够大小的连续空间,我们使用的对象保存这部分空间的地址指针,当没有对象使用这部分资源的时候,GC就会回收资源,释放内存

3.运行时关系

引用CLR via C#的配图,来说明这部分内容

首先,我们可以看到值类型,是直接保存在栈上的
其次,我们就说说引用类型
堆上的所有对象都有两个额外成员:类型对象指针和同步块索引

CLR开始在一个进程中运行时,会立刻创建为System.Type类型创建一个特殊的类型对象,它的类型对象指针指向自己

CLR利用程序集的元数据,提取类型信息,就会创建一些数据结构来表示类型本身,也就是我们在图中看见的Manager和Employee的类型对象,它们的类型对象指针指向Type类型对象

而我们生成的实例对象的类型对象指针就指向它自己的类型对象

Object.GetType方法返回的就是存储在指定对象的“类型对象指针”中的地址,这样就能够判断任何对象的真实类型

二、Object类型

所有的类型都是继承自System.Object,Object类定义的方法不多,主要分成两个部分依次说一下:

1、比较

public static bool Equals(Object objA, Object objB);
public static bool ReferenceEquals(Object objA, Object objB);

这两个方法实际都是比较的对象的引用,也就是比较的对象的“同一性”

这个时候,你也肯定想到了**操作符**,它在比较引用类型的对象的时候,也是比较它们的引用,但是在这种情况下,最好 使用ReferenceEquals方法,这样没有歧义,因为操作符经常被重载,重载后就可能影响其比较的结果

值类型的Equals方法是被重写了的,它比较的是对象的值,也就是比较的是对象的“相等性”,它重写的时候,并没有调用base.Equals,即它没有调用object的Equals方法

注意:

值类型有一种比较特殊的情况

int a = 1;
byte b = 1;
Console.WriteLine(a.Equals(b)+" "+b.Equals(a));

这个输出结果肯定是true true对不对,两个值类型的值都是1嘛,然而并不是,你自己运行一下就知道,这个的结果是 true false
这个是因为a.Equals方法因为a是int类型,所以它的参数类型,也是int,而byte是8位值,精度低于int,可以隐式转换,所以a.Equals(b)的结果是true,而b.Equals(a)正好相反,由于a的精度较高,无法隐式转换,存在数据丢失的可能,所以它的结果就是false

2、复制对象

protected Object MemberwiseClone();

MemberwiseClone方法返回一个当前对象的浅表副本
举个例子,首先说明已知条件,一个类型的实例a,调用MemberwiseClone方法生成了一个a的浅表副本b,这个类型有一个公有属性c,c是引用类型的
首先,当你使用object.ReferenceEquals(a,b)比较它们的引用,结果是false,说明,b是重新生成的对象,存储在新的内存地址,这个时候你再使用object.ReferenceEquals(a.c,b.c)的时候,你就会发现,结果是true,这就是浅表副本的特点,它会直接复制之前对象的属性的引用过来,而不是生成新的对象
还好,利用C#的序列化,深复制实现起来也比较简单

[Serializable]
public class CustomType<T>
{
 public CustomType<T> GetDeepClone()
 {
     MemoryStream stream = new MemoryStream();
     BinaryFormatter formatter = new BinaryFormatter();
     formatter.Serialize(stream, this);
     stream.Position = 0;
     return formatter.Deserialize(stream) as CustomType<T>;
 }
}

三、基元类型


我们在写代码的时候应该经常可以看见string类型和String类型,实际,string是C#的关键字,而String是FCL里的类型,string是直接映射到String的,所以二者没有区别

四、类型转换

1.基元类型转换

1)强制类型转换

按照上如,可以发现类型间的位数是由差别的,也就是, 每种类型的精度不同
精度低的可以隐式转换成精度高的类型,例如

int a = 1;
long b = a;

这样不会报错,a可以直接赋值给long类型的b

而精度高的类型赋值给精度低的类型,就需要显示转换,因为它要确认你是主动放弃高出部分的精度

long b = 2;
int a = (int)b;

这里有一些特殊的就是:
(1)string类型无法使用强制类型转换为数字
(2)char类型可以强转成数字,但是结果不是char的内容,而是它的内容所代表的ASCII码,例如 

2)type.Parse(这里的type是类型名,如float,int等)

基元类型都有一个Parse的静态方法,用来把作为参数传入的string转换成当前的类型,例如

float.Parse(“31.2”)

这个方式有一个不好的地方,就是会出现异常
如果字符串的内容为Null ,则抛出ArgumentNullException
如果字符串内容不是数字,则抛出FormatException

3)type.TryParse

float a;
float.TryParse(“31.2”, out a);

这个方法有两个参数,第二个参数就是转换后的值,若是字符串没有问题,就会a就是我们要的值,若是有问题,a就被赋值为0,这个方法的返回值是bool,表示是否转换成功

4)Convert

这个类用于将一个基本数据类型转换为另一个基本数据类型,可自己在编辑器里查看它的定义
##2.引用类型转换
1)强制转换
引用类型也是可以强制转换的,例如

object o = new object();
Parent parent = new Parent();
Parent temp = (Parent)o;

但是这样,是有风险的,因为如果o的对象不是parent对象装箱而来的话,这样就会报错
我们在实际应用的时候,应该使用is操作符,来提升代码的稳定性

object o = new object();
Parent parent = new Parent();
if (o is Parent)
{
Parent temp = (Parent)o;
}

先用is判断,对象是否和要转换的类型兼容,然后再进行转换
1)as操作符
它的工作方式实际和强转一样,但是它的有点就是,若是转换失败,它不会报错,而是把对象赋值为null

object o = new object();
Parent parent = new Parent();
Parent temp = o as Parent;

我们代码的时候,尽量采取这种有安全保障的方式,有利于提高代码的稳定性
#五、类中的字段布局
CLR可以按照选择的方式,排列类型的字段,从而提升性能

[ComVisible(true)]
public enum LayoutKind
{
    //保持你自己的布局
    Sequential = 0,
    //利用偏移量排列字段
    Explicit = 2,
    //自动布局
    Auto = 3
}

C#编辑器默认引用类型使用LayoutKind.Auto,值类型使用LayoutKind.Sequential
因为编译器团队认为和非托管代码互操作时会经常用到结构,如果你创建的类型不与非托管代码互操作,就像像上图一样使用LayoutKind.Auto,覆盖默认选项,这样可以提升这个值类型的性能

当使用LayoutKind.Explicit 时,值类型中每个字段必须标记[FieldOffset(offsetValue)],这个偏移量是指在这个字段的第一个字节距离实例的起始处的偏移量,例如

这代表a和b都是从实例的第一个字节开始,也就是说,它们在内存上的位置是相互重叠的,它们两个的值始终一致
这样就实现了类似c中的共用体的功能

这里需要注意偏移量的设置,FieldOffset(0)指这个字段的第一个字节对应实例的第一个字节,但是,FieldOffset(1)对应的并不是实例的第二个字节,而是对应实例的第三个字节,以此类推,FieldOffset(2)对应的是实例的第五个字节

我会在我的公众号上推送新的博文,也可以帮大家解答问题
微信公众号 Andy and Unity 搜索名称或扫描二维码
在这里插入图片描述
希望我们能共同成长,共同进步

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值