C# sealed密封 追本溯源

一、简介

在 C# 中,sealed 关键字用于定义一个密封类或者密封方法,也就是不能被继承的类。这样做的目的是为了限制类的继承,确保类的行为和实现不会被改变。

此外,密封类也有助于提升代码的安全性和可维护性。由于密封类不能被继承,开发者可以更好地控制类的行为,避免不必要的继承和重写带来的风险。

**sealed**类是 C# 提供的一个强大工具,它帮助开发者更好地控制类的设计和行为。

(过多基础知识介绍就不在此赘述了,今天探讨的问题点是 sealed密封类是否会带来性能上的提升,这是一个非常有意思的话题。 )

二、类从元数据到 CLR 运行时

1) 编译器输出:源代码 → IL + 元数据(Assembly / PE)

C#语言被编译成 IL(中间语言)并写入 PE(Portable Executable)文件中。同时生成 元数据(Metadata):类型定义(TypeDef)、字段(FieldDef)、方法(MethodDef)、参数、属性、接口实现、常量、引用到其他模块(TypeRef/MemberRef/AssemblyRef)等。

1.1)Roslyn

Roslyn简介:Roslyn = C# / VB 的"编译器 + 代码分析框架",而且本身就是用 C# 写的。在 .NET 中它不仅负责把代码编译成 IL,还把"编译过程本身"开放出来让你可以读语法树、编辑语法树、生成代码、做分析、做重写。

在 Roslyn / 编译器语境里:**使用编译器自身的能力来编译或生成编译器本身。**这种方式称之为自举。自举的意义在于见证语言的完整性、减少依赖。

1.2)核心类MetadataWriter

1.2.1)PopulateTypeDefTableRows

PopulateTypeDefTableRows方法会把中所有的类型写入 TypeDef 表 ,并为每个类型生成 Metadata 所需的行数据。(MetadataWriter还包含很多方法,PopulateTypeDefTableRows只会把类型写入TypeDef 类型引用表中。还有其他的PopulateMethodDefTableRows()、PopulateCustomAttributeTableRows()等方法,会写入对应的方法和特性引用表中,最终这些引用表合成Metadata。就不在此详细讲解了。因为实在是有太多东西了。。。。。。)

1.2.2)WriteMetadataAndIL

WriteMetadataAndIL会构建所有生成的Metadata,通过emit生成IL。把Metadata和IL拼成最终的二进制块,交给 PEWriter 写入 PE 文件。最终我们将得到.dll或者.exe文件。

2)方法引用的内联和展开

2.1)内联(inlining)

内联:方法调用在最终生成的 IL 或 JIT 机器码中直接替换成方法体的代码,不再有调用指令。常用于小方法,提高性能。

2.2)展开(普通调用 / 不内联)

调用保留 call 指令,执行时需要跳转到被调用方法,维护调用栈和参数。

2.3)call和callvirt

call:编译期直接确定方法地址,调用点直接跳转到方法体,不需要虚表查找。

callvirt:编译期只知道方法 slot(在 vtable 或接口表),运行时根据对象类型查找实际实现。

2.4)抽象类抽象方法

我们来写一个小的demo:

cs 复制代码
namespace ConsoleApp1
{
    public abstract class AbstractTest
    {
        public abstract void Test();
    }

    class AbstractClass : AbstractTest
    {
        public override void Test()
        {
            Console.WriteLine("Hi");
        }
    }

    public class ConsoleWriteLine
    {
        void Test(AbstractClass abs)
        {
            abs.Test();
        }
    }
}

通过ILSpy我们可以看到,ConsoleWriteLine这个类在调用抽象方法时使用的是callvirt。而callvirt会先去vtable表中去检索,找到虚方法或者抽象方法的具体实现地址在哪里。

3)sealed密封类

3.1)虚拟化**(Virtualization)**

虚拟化:在 C# / CLR 里主要指方法调用的多态机制,也就是 运行时动态分派(dynamic dispatch),允许子类重写父类方法。让调用者在编译期无法确定具体方法体,而是在运行时根据对象类型决定调用哪个实现。

虚拟化的实现方法是-->虚方法表(vtable)+ callvirt 指令。关键目的是支持多态。

3.2)去虚拟化(Devirtualization)

如果对象类型在运行期唯一可确定,例如 sealed 类-->JIT 可以去掉虚拟化机制。这样对比虚拟化的优势是,不在进行vtable表检索,减少调用性能下降。

4)RunTime中的sealed

4.1)RunTime

我们可以看到Runtime会把sealed密封类标记为 "CORINFO_FLG_FINAL"

检测到是:sealed密封类;static静态类;不是抽象类,就会停止检索。Runtime JIT会对其进行优化。

相关推荐
FRing036 分钟前
vue2干货总结7(自定义指令、插槽、单页应用程序、路由的使用(VueRouter))
开发语言·前端·vue.js·vue2
雪落漂泊10 分钟前
C++算法 二分查找篇(上)
开发语言·c++·算法
波力海苔夹心脆6751 小时前
C# WPF 资源字典详解:ResourceDictionary、MergedDictionaries 与自定义控件案例
开发语言·c#·wpf·入门教程·resource·资源字典
不会算法的笨小孩2 小时前
异常的使用以及会出现的问题
开发语言·c++·c++11·异常
AI导出鸭PC端3 小时前
Perplexity批量导出没有PDF模式?AI导出鸭插件补上最后一块拼图
人工智能·pdf·c#·ai导出鸭
Hhy_11074 小时前
《C++深度解构06》手撕 string —— 从底层内存管理到深浅拷贝彻底吃透
c语言·开发语言·c++·学习·visual studio
starzy19904 小时前
深入理解Scala函数定义:从方法到函数的类型跨越
开发语言·scala
starzy19904 小时前
深入理解Scala递归函数:从栈溢出到尾调用优化
开发语言·scala
三*一4 小时前
Mapbox‑GL JS 多边形折点编辑工具:实现原理、功能拆解与踩坑总结
开发语言·javascript·ecmascript
YSL0701244 小时前
C++基础
开发语言·c++