dotnet 5 从 IL 层面分析协变返回类型新特性
在 C# 9.0 里面添加的一个新特性是支持协变返回类型,也就说子类重写了基类的抽象或虚拟方法,可以在返回值里面返回协变的类型,也就是返回值的类型可以是继承原本子类返回值类型的子类。本文将来从 IL 的层面和运行时告诉大家这个新特性为什么需要 dotnet 5.0 才能支持
在开始之前,必须说明的是 C# 语言和 .NET 框架是分开的,不能因为 C# 9.0 用到了某些只有在 dotnet 5.0 的运行时才能提供的功能就说 C# 和 .NET 绑定。实际上在 .NET Framework 4.5 都能使用大量的 C# 9.0 语法。准确来说是 C# 9.0 语法里面的有一些新的特性需要在新的运行时和框架下才能使用起来,此部分新特性将需要 .NET 5.0 的支持,其他的部分只需要编译器支持就可以,依然可以在旧版本的 .NET 运行
本文依然是底层知识,本文内容不适合新手阅读。如果不想了解底层的原理,那么只需要知道这个新特性需要 IL 的支持,因为生成的 IL 代码语法上和之前的相同,但 IL 代码逻辑和之前不兼容。因为 IL 逻辑的变更,自然也需要 CLR 运行时的特别支持。这个新特性需要 IL 和运行时的支持,在旧版本的 .NET 是不能使用的
在开始之前,大家看一下新的语法的写法。如以下代码,从 Animal 继承的 Tiger 类重写了 GetFood 方法,但是在 Tiger 的 GetFood 方法的方法返回值和 Animal 的 GetFood 方法定义的不相同
abstract class Animal
{
public virtual Food GetFood()
{
return null;
}
}
class Tiger : Animal
{
public override Meat GetFood() => new Meat();
}
或者说是如下写法,在 Animal 类的 GetFood 是抽象的方法
abstract class Animal
{
public abstract Food GetFood();
}
class Tiger : Animal
{
public override Meat GetFood() => new Meat();
}
上面两个代码的不同在于 Animal 类使用的是 abstract 或 virtual 的方法被重写,在重写的时候可以返回协变的类。以下是返回值 Food 类型定义
public class Food
{
}
public class Meat : Food
{
}
可以看到 Meat 是继承 Food 的类型,也就是说允许子类的返回值类型是重写的方法的子类。这是一个不错的特性,可惜在 .NET Framework 下是用不了的,因为需要 CLR 运行时和框架的支持
上面开源,可以在 github 或 gitee 下载全部代码
先从 IL 的层面来聊聊这个新特性的不同,如下面的 C# 代码,这是不使用新特性的方法
abstract class Animal
{
public abstract Food GetFood();
}
class Tiger : Animal
{
public override Food GetFood() => new Meat();
}
上面代码生成的 IL 代码大概如下
.method public hidebysig virtual instance class Lindexi.Food
GetFood() cil managed
{
.maxstack 8
// [20 43 - 20 53]
IL_0000: newobj instance void Lindexi.Meat::.ctor()
IL_0005: ret
} // end of method Tiger::GetFood
上面的 IL 代码咱现在还不需要去阅读他,接下来生成一下使用新特性的如以下 C# 代码的 IL 代码
abstract class Animal
{
public abstract Food GetFood();
}
class Tiger : Animal
{
public override Meat GetFood() => new Meat();
}
上面 C# 生成的 IL 代码如下
.method public hidebysig virtual newslot instance class Lindexi.Meat
GetFood() cil managed
{
.custom instance void [System.Runtime]System.Runtime.CompilerServices.PreserveBaseOverridesAttribute::.ctor()
= (01 00 00 00 )
.override method instance class Lindexi.Food Lindexi.Animal::GetFood()
.maxstack 8
// [20 43 - 20 53]
IL_0000: newobj instance void Lindexi.Meat::.ctor()
IL_0005: ret
} // end of method Tiger::GetFood
对比和没有使用新特性的 IL 代码,从方法的定义上,就可以看到一些不同点,下面是两个 IL 的对比
.method public hidebysig virtual instance class Lindexi.Food GetFood() cil managed
.method public hidebysig virtual newslot instance class Lindexi.Meat GetFood() cil managed
可以看到使用新特性的 IL 代码多了 newslot
关键字,这个 IL 关键词其实就相当于使用 new
关键字进行重写子类的方法,可以认为和子类的方法是两个不同的方法。但实际上又是在做继承方法,在 IL 的设计里面,为了让方法返回值不相同,此时就使用 newslot
关键字表示这是一个新的独立的方法,但又不能让这个方法和原本的代码逻辑不同,因此又需要让这个子类方法继承基类方法,于是就再加上了以下两行代码
.custom instance void [System.Runtime]System.Runtime.CompilerServices.PreserveBaseOverridesAttribute::.ctor() = (01 00 00 00 )
.override method instance class Lindexi.Food Lindexi.Animal::GetFood()
上面 IL 的第一句话是添加了 PreserveBaseOverridesAttribute 这个特性,也就是在 Roslyn 生成 IL 逻辑自动给这个函数加上了 PreserveBaseOverridesAttribute 特性,相当于以下代码
class Tiger : Animal
{
[PreserveBaseOverridesAttribute]
public override Meat GetFood() => new Meat();
}
上面的 PreserveBaseOverridesAttribute 特性是自动添加的,不需要手动加上。在这个方法的下一句是 .override method
表示实际这个方法是有继承某个方法的,代码如下
.override method instance class Lindexi.Food Lindexi.Animal::GetFood()
通过上面的 IL 代码就可以在 CLR 找到重写的方法
上面代码的 PreserveBaseOverridesAttribute 特性是 .NET 5 框架提供的类型,也就是说 .NET Framework 4.8 等是没有这个类的
接着从 CLR 层面来讲这个新特性,如上面 IL 代码,和原本的 IL 不是兼容的,需要 CLR 层面做一些逻辑才能了解上面的 IL 的逻辑含义。需要说明的是,上面 IL 的语法含义依然是兼容的,但是逻辑含义不是兼容的,需要运行时做一些逻辑才能了解这个 IL 代码表示 GetFood 方法继承的方法
在 src\coreclr\vm\class.cpp
的 ClassLoader::PropagateCovariantReturnMethodImplSlots
方法里面是处理这个新特性的核心逻辑,在 PropagateCovariantReturnMethodImplSlots 方法会先判断是否存在 PreserveBaseOverridesAttribute 特性,如果存在那么继续通过 IL 里面记录的 .override method
找到实际的关系,代码如下
void ClassLoader::PropagateCovariantReturnMethodImplSlots(MethodTable* pMT)
{
CONTRACTL
{
STANDARD_VM_CHECK;
PRECONDITION(CheckPointer(pMT));
}
CONTRACTL_END;
// 以下代码的逻辑是如果 MethodImpl 具有 PreserveBaseOverridesAttribute 特性,则将重写的 MethodImpl 传播到所有适用的虚表空间槽。在 C# 的抽象或虚拟方法都相当于定义了函数的虚表,存放在虚表空间槽。 这是为了确保如果我们使用基类型方法之一的签名来调用覆盖方法,我们仍然执行覆盖方法。
// 例如下面注释的代码例子
//
// Propagate an overriding MethodImpl to all applicable vtable slots if the MethodImpl
// has the PreserveBaseOverrides attribute. This is to ensure that if we use the signature of one of
// the base type methods to call the overriding method, we still execute the overriding method.
//
// Consider this case:
//
// class A
// {
// RetType VirtualFunction() { }
// }
// class B : A
// {
// [PreserveBaseOverrides]
// DerivedRetType VirtualFunction() { .override A.VirtualFuncion }
// }
// class C : B
// {
// MoreDerivedRetType VirtualFunction() { .override A.VirtualFunction }
// }
//
// NOTE: Typically the attribute would be added to the MethodImpl on C, but was omitted in this example to
// illustrate how its presence on a MethodImpl on the base type can propagate as well. In other words,
// think of it as applying to the vtable slot itself, so any MethodImpl that overrides this slot on a
// derived type will propagate to all other applicable vtable slots.
//
// Given an object of type C, the attribute will ensure that:
// callvirt RetType A::VirtualFunc() -> executes the MethodImpl on C
// callvirt DerivedRetType B::VirtualFunc() -> executes the MethodImpl on C
// callvirt MoreDerivedRetType C::VirtualFunc() -> executes the MethodImpl on C
//
// Without the attribute, the second callvirt would normally execute the MethodImpl on B (the MethodImpl on
// C does not override the vtable slot of B's MethodImpl, but only overrides the declaring method's vtable slot.
//
// Validation not applicable to interface types and value types, since these are not currently
// supported with the covariant return feature
if (pMT->IsInterface() || pMT->IsValueType())
return;
MethodTable* pParentMT = pMT->GetParentMethodTable();
if (pParentMT == NULL)
return;
// Propagate overriding MethodImpls to applicable vtable slots if the declaring method has the attribute
if (pMT->GetClass()->HasVTableMethodImpl())
{
MethodTable::MethodDataWrapper hMTData(MethodTable::GetMethodData(pMT, FALSE));
for (WORD i = 0; i < pParentMT->GetNumVirtuals(); i++)
{
if (pMT->GetRestoredSlot(i) == pParentMT->GetRestoredSlot(i))
{
// The real check is that the MethodDesc's must not match, but a simple VTable check will
// work most of the time, and is far faster than the GetMethodDescForSlot method.
_ASSERTE(pMT->GetMethodDescForSlot(i) == pParentMT->GetMethodDescForSlot(i));
continue;
}
MethodDesc* pMD = pMT->GetMethodDescForSlot(i);
MethodDesc* pParentMD = pParentMT->GetMethodDescForSlot(i);
if (pMD == pParentMD)
continue;
// If the bit is not set on this method, but we reach here because it's been set on the method at the same slot on
// the base type, set the bit for the current method to ensure any future overriding method down the chain gets checked.
if (!pMD->RequiresCovariantReturnTypeChecking() && pParentMD->RequiresCovariantReturnTypeChecking())
pMD->SetRequiresCovariantReturnTypeChecking();
// The attribute is only applicable to MethodImpls. For anything else, it will be treated as a no-op
if (!pMD->IsMethodImpl())
continue;
// Search if the attribute has been applied on this vtable slot, either by the current MethodImpl, or by a previous
// MethodImpl somewhere in the base type hierarchy.
bool foundAttribute = false;
MethodTable* pCurrentMT = pMT;
while (!foundAttribute && pCurrentMT != NULL && i < pCurrentMT->GetNumVirtuals())
{
MethodDesc* pCurrentMD = pCurrentMT->GetMethodDescForSlot(i);
// The attribute is only applicable to MethodImpls. For anything else, it will be treated as a no-op
if (pCurrentMD->IsMethodImpl())
{
// 下面两个变量是没有使用的,只是让 GetCustomAttribute 函数可以调
BYTE* pVal = NULL;
ULONG cbVal = 0;
// 这里就是判断是否存在特性,如果存在,那么设置 foundAttribute 变量
if (pCurrentMD->GetCustomAttribute(WellKnownAttribute::PreserveBaseOverridesAttribute, (const void**)&pVal, &cbVal) == S_OK)
foundAttribute = true;
}
pCurrentMT = pCurrentMT->GetParentMethodTable();
}
if (!foundAttribute)
continue;
// Search for any vtable slot still pointing at the parent method, and update it with the current overriding method
for (WORD j = i; j < pParentMT->GetNumVirtuals(); j++)
{
MethodDesc* pCurrentMD = pMT->GetMethodDescForSlot(j);
if (pCurrentMD == pParentMD)
{
// This is a vtable slot that needs to be updated to the new overriding method because of the
// presence of the attribute.
pMT->SetSlot(j, pMT->GetSlot(i));
_ASSERT(pMT->GetMethodDescForSlot(j) == pMD);
hMTData->UpdateImplMethodDesc(pMD, j);
}
}
}
}
}
上面代码在 dotnet 的运行时开源仓库里面,请看 https://github.com/dotnet/runtime/ 源代码
在 Mono 里面,当前的 Mono 也是放在 https://github.com/dotnet/runtime/ 里面,也对这个新特性做了自己的实现,在 Mono 的 src\mono\mono\metadata\class-init.c
里面将会使用如下代码判断某个方法是否有 PreserveBaseOverridesAttribute 特性
gboolean
mono_class_setup_method_has_preserve_base_overrides_attribute (MonoMethod *method)
{
MonoImage *image = m_class_get_image (method->klass);
/* FIXME: implement well known attribute check for dynamic images */
if (image_is_dynamic (image))
return FALSE;
return method_has_wellknown_attribute (method, "System.Runtime.CompilerServices", "PreserveBaseOverridesAttribute", TRUE);
}
如果判断存在 PreserveBaseOverridesAttribute 也就在 src\mono\mono\metadata\class-setup-vtable.c
的 mono_class_setup_vtable_general
方法里面进行后续的逻辑,因为 mono_class_setup_vtable_general 方法太长了,而我对 Mono 的实现也不熟悉,更多细节还请大家阅读源代码
特别感谢 少珺 小伙伴给我的协助
文档请看
dotnet 5 从 IL 层面分析协变返回类型新特性的更多相关文章
- Java协变返回类型
今天看到句话:“支持重写方法时返回协变类型”. 那么什么事协变类型?在网上找了找资料,大体上明白了. Java 5.0添加了对协变返回类型的支持,即子类覆盖(即重写)基类方法时,返回的类型可以是基类方 ...
- 理解Java中的协变返回类型
在面向对象程序设计中,协变返回类型指的是子类中的成员函数的返回值类型不必严格等同于父类中被重写的成员函数的返回值类型,而可以是更 "狭窄" 的类型. Java 5.0添加了对协变返 ...
- c++ 类覆盖方法中的协变返回类型
c++ 类覆盖方法中的协变返回类型 在C++中,只要原来的返回类型是指向类的指针或引用,新的返回类型是指向派生类的指针或引用,覆盖的方法就可以改变返回类型.这样的类型称为协变返回类型(Covarian ...
- Java 协变返回类型
协变返回类型表示在导出类的被覆盖方法可以返回基类方法的返回类型的某种导出类型 //: polymorphism/covarianreturn.java package object; class Gr ...
- 协变返回类型---《C++必知必会》 条款 31
一般来说,一个重写的函数与被它重写的函数具有相同的返回类型. 然而,这个规则对于“协变返回类型(covariant return type)“的情形来说有所放松.也就是说,如果B是一个类类型,并且一 ...
- Covariant Returen Types(协变返回类型)
基类virtual func返回类型为某个类(class Super)的ptr或ref,子类重写的virtual func返回类型可改为该类子类(class Sub : public Super)的p ...
- 深度分析:java8的新特性lambda和stream流,看完你学会了吗?
1. lambda表达式 1.1 什么是lambda 以java为例,可以对一个java变量赋一个值,比如int a = 1,而对于一个方法,一块代码也是赋予给一个变量的,对于这块代码,或者说被赋给变 ...
- typedef 返回类型(*Function)(参数表) ——typedef函数指针
//首先看一下函数指针怎么用 #include <iostream> using namespace std; //定义一个函数指针pFUN,它指向一个返回类型为char,有一个整型的参数 ...
- 通过从代码层面分析Linux内核启动来探知操作系统的启动过程
通过从代码层面分析Linux内核启动来探知操作系统的启动过程 前言说明 本篇为网易云课堂Linux内核分析课程的第三周作业,我将围绕Linux 3.18的内核中的start_kernel到init进程 ...
- springMVC源码分析--ViewNameMethodReturnValueHandler返回值处理器(三)
之前两篇博客springMVC源码分析--HandlerMethodReturnValueHandler返回值解析器(一)和springMVC源码分析--HandlerMethodReturnValu ...
随机推荐
- Three.js中加载和渲染3D Tiles
1. 引言 3D Tiles 是 3D GIS 中常见的三维数据格式,能否用Three.js来加载渲染呢?肯定是可以,Three.js只是一个WebGL框架,渲染数据肯定可以,但是加载.解析数据得手动 ...
- 记录--源码视角,Vue3为什么推荐使用ref而不是reactive
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 ref 和 reactive 是 Vue3 中实现响应式数据的核心 API.ref 用于包装基本数据类型,而 reactive 用于处理对 ...
- 记录--有关uni-app如何实现路由拦截的知识分享
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 前言 随着业务的需求,项目需要支持H5.各类小程序以及IOS和Android,这就需要涉及到跨端技术,不然每一端都开发一套,人力成本和维护 ...
- vue,vuex,element实现无限tab页效果
直接撸代码 ?满足你 码云地址 效果图 tab页由来 甲方爸爸的更改需求,无力反抗 分析代码 懒的写,直接撸就行 参考文章 点我
- modelsim常用操作之波形仿真
modelsim波形仿真的新手问题 1.实验目的 在刚接触modelsim时,被其繁复的操作流程所困,一度只能依靠在quartus中修改代码编译后再重启modelsim,自动导入才能得到波形.这样的操 ...
- Linux是什么与如何学习
重点回顾 操作系统(Operation System) 主要在管理与驱动硬件,因此必须要能够管理内存.管理装置. 负责行程管理以及系统呼叫等等.因此,只要能够让硬件准备妥当(Ready)的情况, 就是 ...
- 闲来无事-esp32cam实现延时摄影
扯淡时间 在上一篇文章中我提了一嘴,打算使用esp32cam实现一个延迟摄影,奈何存在各种硬件问题,商家发了好几个地板都不好使(就是那个拼多多商家的问题,还说我供电不稳,我特意买了独立供电的hub), ...
- #zkw线段树#洛谷 3792 由乃与大母神原型和偶像崇拜
题目 给你一个长为 \(n\) 的序列 \(a\) 每次两个操作: 修改 \(x\) 位置的值为 \(y\) 查询区间 \([l,r]\) 是否可以重排为值域上连续的一段 分析 直接维护区间最大值和最 ...
- C# 发布你的程序包到Nuget
1.新建一个.NET Standard 的类库项目 2.选择项目属性,在 package 栏目下填写我们的nuget包信息 3.选择我们的项目,点击"Pack" 打包 主要注意的是 ...
- 【#HDC2022】HarmonyOS体验官活动正式开启,赶快投稿赢限量奖品吧!
1. [活动简介] HDC 2022 于11月4日线上线下正式开启.历时一年,在无数开发者的共同努力下,我们汇聚了HarmonyOS生态的新成果.新体验.新开放能力,邀你参与到HarmonyOS的 ...