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会对其进行优化。

相关推荐
真正的醒悟2 小时前
图解网络35
开发语言·网络·php
大连好光景2 小时前
批量匿名数据重识别(debug记录)
开发语言·python
计算机毕设VX:Fegn08952 小时前
计算机毕业设计|基于Java + vue水果商城系统(源码+数据库+文档)
java·开发语言·数据库·vue.js·spring boot·课程设计
清水白石0082 小时前
《深入 Celery:用 Python 构建高可用任务队列的实战指南》
开发语言·python
Tony Bai2 小时前
Jepsen 报告震动 Go 社区:NATS JetStream 会丢失已确认写入
开发语言·后端·golang
无敌最俊朗@2 小时前
STL-list面试剖析(面试复习4)
开发语言
bing.shao2 小时前
Golang 之 defer 延迟函数
开发语言·后端·golang
无敌最俊朗@2 小时前
Qt 多线程编程: moveToThread 模式讲解
java·开发语言
!停2 小时前
深入理解指针(4)
开发语言·javascript·ecmascript