当前位置: 首页 > news >正文

C#、类型基础

前言

前面我介绍了C#多线程和异步编程的相关内容。本篇文章将介绍C#中,使用类型和CRL时需要掌握的基础知识。包括CLR工作流程、类型转换等内容,最后还会解释类型、对象、线程栈和托管堆在运行时的关系。

传动门:

C#、多线程

C#、异步编程

CLR

在说后面的内容之前,我们要简单介绍一下CLR。全称是 Common Language Runtime,翻译过来就是公共语言运行库。CLR是.NET框架的核心组件,负责管理代码的执行,提供内存管理、线程管理、异常处理、垃圾回收、安全性等服务。

CLR的工作流程
  1. 编译:C#代码被编译为中间语言(IL)和元数据,存储在程序集(Assembly)中。
  2. 加载:CLR加载程序集,读取IL代码和元数据。
  3. JIT编译:CLR通过即时编译器(JIT,Just-In-Time)将IL代码转换为机器码。
  4. 执行:CLR执行编译后的机器码,管理内存、线程和异常。

CLR将C#代码编译为中间语言(IL),所以CLR是支持跨语言的,支持多种 .NET 语言(如 C#、VB.NET、F#),并允许它们互操作,因为它们最终都被编译为相同的 IL 代码。

CLR的核心功能
  • 代码执行:CLR负责将C#等.NET语言编译的中间语言(IL,Intermediate Language)代码转换为机器码并执行。
  • 内存管理:CLR自动管理内存分配和回收,通过垃圾回收机制(GC)释放不再使用的对象。
  • 线程管理:CLR提供多线程支持,管理线程的创建、调度和同步。
  • 异常处理:CLR提供统一的异常处理机制,支持跨语言异常处理。
  • 类型安全:CLR确保代码的类型安全,防止非法类型转换和内存访问。
  • 等等

总结下来就是,CLR将我们写的C#代码,编译并执行为计算机可以运行的机器码,同时管理应用程序的运行环境,包括内存管理、线程管理、异常处理等内容

但是上面说的CRL是支持跨语言,不支持跨平台的,只能在Windows上运行。现在我们说的.NET5/6/7/8...。都是支持跨平台,可以在Linux、macOS上运行。所以微软后面为了支持跨平台,提出了CoreCLR

CoreCLR相较于CLR其实基础功能是没有变的,包括内存、内存管理等。主要区别是CoreCLR支持跨平台,同时还支持现代应用场景(如云原生、微服务、容器化等等)。

所有类型都从System.Object派生

运行时(就是CLR)要求每个类型最终都是从System.Object类型派生。也就是说下面两个类型完全一致。

public class Test { } //显式继承System.Object public class Test: System.Object { }

由于所有类型最终都从System.Object类型派生,所以每个类型的每个对象都保证了一组最基本的方法。如下表所示:

方法说明
Equals比较当前对象与指定对象是否相等。引用相等性检查(即比较两个对象是否引用同一内存地址)可以通过在自定义类中重写此方法,以实现值相等性比较。
GetHashCode返回对象的哈希码,用于支持哈希表,基于对象的内存地址生成哈希码。如果重写了 Equals 方法,通常也需要重写 GetHashCode,以确保相等的对象具有相同的哈希码。
ToString返回对象的字符串表示形式,默认返回对象的完全限定类型名称(例如 "Namespace.ClassName")通常需要重写此方法,以返回更有意义的字符串表示。
GetType返回当前对象的运行时类型,通常返回的 Type 对象可以用于反射,获取类型信息(如属性、方法等)。但需要注意的一点是,这是一个非虚方法,不能重写。

我们平时定义一个对象后,会通过new来创建该对象,比如以下方式:

Test test = new Test();

这也是CLR要求创建对象的方式。那么new到底做了哪些事呢,下面详细介绍一下:

  1. 计算类型以及所有基类型(一直到System.Object)中定义的所有实例字段需要的字节数。托管堆上每个对象都需要一些额外成员,包括类型对象指针和同步块索引。CRL利用这些成员管理对象。额外成员的字节数要计入对象大小。
  2. 从托管堆中分配类型要求的字节数,从而分配对象的内存,分配的所有字节都设为0。
  3. 初始化对象的 类型对象指针 和 同步块索引。
  4. 调用类型的实例构造器(构造器其实就是构造函数,如果没有显式构造函数,会默认创建一个无参构造函数),每个类型的构造器都负责初始化该类型的实例字段,最终会调用到System.Object的构造器,该构造器什么都不做,简单地返回。

new执行了所有这些操作之后,返回指向新建对象一个引用(就是我们常说的指针)。在前面的代码中,该指针保存到变量test中,后者具有Test类型。

顺便说一句,没有new操作符对应的delete操作符,也就是说没有办法显式释放为对象分配的内存。CLR采用垃圾回收机制(也就是GC),能自动检测到一个对象不再被使用或访问,并自动释放该对象的内存。GC后面会单独再做介绍。

这里简单说下,GC就是常说的垃圾回收,主要回收一些托管资源,比如一个普通的类,它的对象会存放在托管堆中、string Name 也是一个托管资源,因为 string 本质上也是一个引用类型,由 GC 管理,还有经常用的List<T>,都是托管资源。

非托管资源,需要手动释放的资源,比如数据库连接、网络请求、读取文件等等。如果不手动释放,会造成内存的OOM(内存溢出)。

类型转换

CLR最重要的特性之一就是类型安全。在运行时,CLR总是知道对象的类型是什么。调用GetType方法就可以知道对象的确切类型。

我们开发人员有时候需要将对象从一种类型转换成另一种类型。CLR允许将对象转换成它的实际类型或者它的任何基类型,向基类型转换被认为是一种安全的隐式转换。然而,将对象转换为它的某个派生类型时,C#要求开发人员只能进行显示转换,因为这种转换可能在运行时失败。

public void T1() { //隐式转换 Object o = new Employee(); //显式转换 Employee e = (Employee)Object; }

但有的时候,代码编译的时候没有问题,在运行时转换类型会报错,比如下面的代码:

public void T2() { var now = DateTime.Now; PromoteEmployee(now ); } public void PromoteEmployee(Object o) { Employee e = (Employee )o; ...... }

正常我们肯定不会写这种代码,但是可能也会出现类似的情况。上述代码编译肯定没问题,但是在运行的时候肯定会抛异常,针对这种情况,C#使用isas操作符来转型。

is操作符

is操作符用于检查对象是否兼容于指定类型,返回布尔值true或false。注意,is永远不会抛出异常,如果对象引用的是null,is操作符总是返回false,因为没有可检查其类型的对象。

具体用法如下所示:

public void T3() { Object o= new Object(); if(o is Employee) { Employee e = (Employee)o; ...... } }

as操作符

有了is,为啥又有as了呢,这个区别我们留着下面再说。as操作符用于返回对同一个对象的非null引用。跟is不同的是,as要么返回该对象的引用,要么返回null。

具体用法如下所示:

public void T4() { Employee e = o as Employee(); if(e != null) { ...... } }

as操作符的工作方式与强制类型转换一样,只是它永远不会抛出异常。相反如果对象不能转换,则返回null。

主要区别

  • is操作符:is在CLR中实际检查了两次对象类型。首先会核实是否兼容于指定类型,如果返回true,则在if内部中进行类型转换时,CLR会再次核实是否引用了指定类型。这是因为CLR必须遍历继承层次结构,用每个基类型去核对指定的类型。
  • as操作符:as在CLR中只检查一次对象类型,只会核实是否兼容于指定类型。如果是,那就返回对象的引用,如果不是就返回null。

总结来说,is相较于as增加了安全性,但相对于as性能更低一些。

运行时的相互关系

在介绍类型、对象、线程栈、托管堆在运行时的相互关系前,我们先说一下线程栈和托管堆。这也是我们经常听到的。

托管堆

托管堆是CLR管理的一块内存区域,用于存储引用类型对象,比如类的实例、数组等等。目前大家只需要知道是存储什么类型的对象就行,具体后面在GC垃圾回收的文章中,再详细介绍托管堆。每个.NET应用程序都有自己的托管堆。

线程栈

线程栈是操作系统为每个线程分配的一块内存区域,用于存储线程执行过程中的局部变量、方法调用信息和执行上下文。线程栈是线程私有的,每个线程都有自己的栈空间。

线程创建时会分配1MB的栈,栈空间用于向方法传递实参,方法内部定义的局部变量也在栈上。

托管堆与线程栈的区别
内容托管堆线程栈
存储内容引用类型对象(如类实例、数组)局部变量、方法调用信息
内存管理由垃圾回收器(GC)管理自动管理,方法结束时释放
分配速度相对线程栈较慢,需要查找可用内存非常快
生命周期由垃圾回收器决定与方法调用绑定
线程私有否(托管堆由所有线程共享)
类型和对象
  • 类型是数据的抽象表示,定义了数据的结构、行为和约束。总体来说就是引用类型和值类型。
  • 对象就是类型的实例。

下面来具体介绍一下类型、对象、线程栈、托管堆在运行时的相互关系。

下面展示一段C#代码(代码没有任何实际意义),来介绍线程栈和运行时的关系。

public void M1() { string name = "Tom"; M2(name); ...... return; } public void M2(string s) { int len = s.Length; ...... return; }

根据上面的代码,Windows会加载一个CLR的程序进程。我们知道一个进程可能有多个线程。线程创建的时候,会分配一个1MB的栈,如下图所示:

栈是从高位内存地址向低位内存地址构建。上述图中的线程栈,有背景颜色的部分,表示已经被一部分数据占用了(也有栈初始化时的一些内容)。

在执行到方法时,栈会初始化创建prologue(序幕)代码和 epilogue(尾声)代码,主要作用是可以返回到调用者代码,继续往下执行。其中prologue和epilogue代码在方法执行完毕后,会对进行清理。

第一步:在执行到M1方法时,它的prologue代码在线程栈上分配局部变量name的内存,如图所示:

第二步:M1调用M2方法,将局部变量name作为实参传递。这造成那么局部变量中的地址被压入栈。M2方法内部使用参数变量s标识栈位置。最后还会将返回地址压入栈,被调用的方法在结束之后应返回至该位置。如下图所示:

第三步:M2方法开始执行,同理它的prologue代码在线程栈中为局部变量len(如果还有其他变量)分配内存。然后M2方法内部的代码开始执行,最终会抵达return处,此时CPU的指针被设置成栈中的返回地址。就是会返回到调用M2之前M1之后的代码,然后M1继续执行。如下图所示:

上面说的相关类型都是int、string,那如果是对象呢,线程栈上又是怎么记录的。说到对象,肯定离不开托管堆,下面来一起说一下。

假设有下面两个类:

public class Employee { public int GetYearsEmployed() { ...... } public virtual string GetProgressReport() { ...... } public static Employee Lookup(string name) { ...... } } public class Manager : Employee { public override string GetProgressReport() { } }

跟上面M1和M2一样,在Windows上已经启动一个进程,CLR已经加载,托管堆也初始化完成。这是有个请求,创建了一个线程(包括1MB的栈空间),该线程要调用M3方法:

public void M3() { Employee e; int year; e = new Manager(); e = Employee.Lookup("Tom"); year = e.GetYearsEmployed(); e.GetProgressReport(); ...... return; }

第一步:JIT编译器将M3的IL代码转换成本机CPU指令时,会注意到M3内部引用的所有类型,包括Employee、int、Manager等等。CLR要确认定义的这些类型的所有程序集已经加载完成。然后利用程序集的元数据(就是这些类型的有关信息)提取相关类型的信息,创建一些数据结构来表示类型本身。如下图所示:

上面我们说过堆上所有对象都包含两个额外成员:类型对象指针 和 同步块索引,同时每个类型对象最后还包含一个方法表。在方法表中,类型定义的每个方法都有对应的记录项。

这里要说一下类型对象,类型对象包含该类型的元数据(如字段、方法、属性等),它是类型的描述信息,通常存储在方法表中,并在类首次被访问时初始化。(不会被GC回收掉

第二步:现在要正式的调用M3方法,线程也已经创建好了,如下所示:

作为prologue(序幕)代码,CLR自动将所有局部变量初始化为null或者0。

第三步:M3方法内部new了一个Manager对象,这时会在托管堆中创建一个Manager类型的实例。如下图所示:

可以看出,和所有对象一样,Manager对象也有类型对象指针和同步块索引。该对象还包含必要的字节来容纳Manager类型定义的所有实例数据字段,包括父类Employee和Object定义的所有实例字段。任何时候在堆上新建对象,CLR都自动初始化内部的 类型对象指针 成员来引用和对象对应的类型对象

在new之后,返回Manager对象的内存地址,并将该地址保存到变量e中。(e在线程栈上)

这里总结一下,类型对象和对象

  • 类型对象 在类型首次使用时创建,且在整个进程中共享,也存储在托管堆中,不会被GC掉。
  • 对象 在每次new时都会创建,并存储在托管堆中,等到GC 回收时销毁。

第四步:M3的下一行代码调用Employee的静态方法Lookup。调用静态方法时,CLR会定位与定义静态方法的类型对应的类型对象。然后JIT编译器在类型对象的方法表中查找与被调用方法对应的记录项,对方法进行JIT编译(如果需要的话),再调用JIT编译好的代码。上面的例子中,假定Lookup方法要查找数据库来查询Tom,Tom并且还是公司的一名经理,所以Lookup内部代码还包含Manager对象的构造,用于查询Tom,然后返回该对象的地址。该地址保存到局部变量e中,如下图所示:

注意,e不再是第一个Manager对象。事实上,由于第一个Manager对象没有变量引用该对象,所以它是未来垃圾回收的主要目标 。

第五步:M3代码继续执行,下一行代码调用Employee的实例方法GetYearsEmployed。调用实例方法时,JIT编译器会找到与 发出调用的那个变量的类型对应的类型对象。此时该类型对象是Employee。诶不对,不应该是Manager类型对象吗?这是因为Manager类型对象中的方法表,没有对应的GetYearsEmployed实例方法。JIT编译器会回溯类的层次结构(一直回溯到Object),并在沿途的每个类型中查找该方法。之所以这样回溯,是因为每个类型对象都有一个字段引用了它的基类型(也就是继承的父类),只是在图中没有显示,这里大家知道就可以了。

同理,JIT编译器在类型对象的方法表中找到了方法的记录项,对方法进行JIT编译(如果需要的话),再调用编译好的JIT代码。假设Tom在公司工作了10年,那么返回10,并赋值给栈中的year变量。如下图所示:

第六步:M3代码继续执行,下一行代码调用Employee的虚方法GetProgressReport。调用虚方法时,JIT编译器要在方法中生成一些额外代码,方法每次调用都会执行这些代码。这些代码首先检查发出调用的变量,并跟随地址来到发出调出的对象。变量e引用的是Manager对象,然后,代码检查出对象内部的 类型对象指针 ,该指针指向对象的实际类型。最后,代码在实际类型对象的方法表中找到对应的记录项。同理,对方法进行JIT编译(如果需要的话),再调用JIT编译好的代码。如下图所示:

注意,如果变量e引用的是Employee,那么最终执行的是Employee的GetProgressReport方法。

总结

CLR主要就是管理我们发布的应用程序,包括内存管理、线程管理、异常处理、垃圾回收等等。

CLR创建线程时,会在内存中分配1MB的栈。

线程栈和运行时(CLR)的关系。线程栈中记录方法中定义的值类型变量,还包括方法的返回地址、方法入参、当前方法的执行状态、寄存器值执行上下文等等信息。其中如果变量是引用类型,还会记录指针,指向托管堆中类的实例对象。

当 .NET 程序启动时,CLR 会在进程的虚拟地址空间中申请一块内存,作为托管堆的初始区域。不同的操作系统,初始化内存大小不一样,需要注意的是,负责托管堆的内存,是根据运行时动态变化的,具体等后面介绍GC的时候再说。

托管堆中存放了,所有引用类型的类型对象和实例对象,这个上面也说过了。

大家可能对上面说的JIT编译的代码有点疑惑,这里简单说一下。在托管堆中类型对象末尾会记录该类中所有的方法项。这里的方法项里面不包含代码的,只是一个引用地址,指向具体的代码地址。然后通过该地址,找打具体的代码,然后JIT编译(通常是在第一次调用该方法时编译,后续就不需要)。如果已经编译好了那就直接用。

http://www.jsqmd.com/news/1295301/

相关文章:

  • Claude Code 终极指南:如何用自然语言命令提升你的开发效率300%
  • WorkshopDL终极指南:如何免费下载Steam创意工坊模组,实现跨平台游戏模组自由
  • 焦作温县文武学校怎么选?一文讲透王战军太极文武学校的学费、招生和优势 - Luckyone王
  • 2026年广西超级个体TOP10诚信OPC排名:谁将引领行业新风向?
  • 3步终极解决方案:让Zotero自动获取学术文献PDF的完整指南
  • IPv6 城市级归属地查询-ipv6城市级接口-api接口
  • OpenWork高级技巧:自定义插件开发与工作区配置教程
  • 2026杭州中央空调安装公司哪家靠谱?服务商综合分析与选择参考 - 商业新知
  • Forza-Mods-AIO:终极地平线游戏增强工具的革命性突破
  • 【单片机毕业设计推荐】基于 STM32/51 单片机的 12864 称重计价装置设计与实现 基于单片机的 HX711 称重计价终端系统设计(021104)
  • 2026年安徽工贸预科班报名攻略:公办院校背书,正规流程注册统招学籍 - 最新资讯
  • 武汉凡谷电子职业技术学校怎么联系招生办? - 升学择校早知道
  • Linux系统信息命令
  • 终极风扇控制解决方案:Fan Control让Windows电脑真正安静下来
  • 如何打造你的专属漫画图书馆:开源阅读器Aidoku的5大亮点
  • Power BI 主题模板:告别单调报表,3分钟打造专业数据可视化
  • 3步解锁Wand完整专业功能:告别2小时限制的终极解决方案
  • 免费.brd文件查看器OpenBoardView:硬件工程师的电路板分析利器
  • 盐城宝藏女装店探店:富康路“初见刘小姐的店”,解锁温柔轻熟风的高级穿搭 - 钦扬网络
  • ES-Client:企业级Elasticsearch集群管理与数据可视化平台
  • 2026长三角乳胶弹力绳源头厂家推荐:品质稳定与高复购意愿供应商盘点 - 品牌品鉴馆
  • 图片压缩不损画质的3种方法(附在线工具)
  • 计算机网络笔记(计算机网络体系结构)
  • 一个提示词,一个完整工作流:Elastic 的 AI agent 为你编写自动化流程
  • 如何快速部署AI原生网关:Higress云原生API网关的完整实践指南
  • Agate虚拟主机配置:多域名站点部署与证书管理技巧
  • HCIA学习第二天
  • Flutter 扫描二维码
  • 深度解析:基于内存补丁技术的微信QQ防撤回完整指南
  • 终极GTA5防崩溃菜单:YimMenu完整使用教程与安全防护指南