通用枚举到int的C#非装箱转换?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/1189144/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
C# non-boxing conversion of generic enum to int?
提问by Jeff Sharp
Given a generic parameter TEnum which always will be an enum type, is there any way to cast from TEnum to int without boxing/unboxing?
给定一个始终是枚举类型的通用参数 TEnum,是否有任何方法可以在不装箱/拆箱的情况下将 TEnum 转换为 int?
See this example code. This will box/unbox the value unnecessarily.
请参阅此示例代码。这将不必要地装箱/拆箱该值。
private int Foo<TEnum>(TEnum value)
where TEnum : struct // C# does not allow enum constraint
{
return (int) (ValueType) value;
}
The above C# is release-mode compiled to the following IL (note boxing and unboxing opcodes):
上面的 C# 是发布模式编译为以下 IL(注意装箱和拆箱操作码):
.method public hidebysig instance int32 Foo<valuetype
.ctor ([mscorlib]System.ValueType) TEnum>(!!TEnum 'value') cil managed
{
.maxstack 8
IL_0000: ldarg.1
IL_0001: box !!TEnum
IL_0006: unbox.any [mscorlib]System.Int32
IL_000b: ret
}
Enum conversion has been treated extensively on SO, but I could not find a discussion addressing this specific case.
枚举转换已在 SO 上得到广泛处理,但我找不到针对此特定情况的讨论。
采纳答案by Drew Noakes
I'm not sure that this is possible in C# without using Reflection.Emit. If you use Reflection.Emit, you could load the value of the enum onto the stack and then treat it as though it's an int.
我不确定在 C# 中不使用 Reflection.Emit 是否可行。如果您使用 Reflection.Emit,您可以将枚举的值加载到堆栈上,然后将其视为一个 int。
You have to write quite a lot of code though, so you'd want to check whether you'll really gain any performance in doing this.
但是,您必须编写相当多的代码,因此您需要检查这样做是否真的会获得任何性能。
I believe the equivalent IL would be:
我相信等效的 IL 将是:
.method public hidebysig instance int32 Foo<valuetype
.ctor ([mscorlib]System.ValueType) TEnum>(!!TEnum 'value') cil managed
{
.maxstack 8
IL_0000: ldarg.1
IL_000b: ret
}
Note that this would fail if your enum derived from long
(a 64 bit integer.)
请注意,如果您的枚举源自long
(64 位整数),这将失败。
EDIT
编辑
Another thought on this approach. Reflection.Emit can create the method above, but the only way you'd have of binding to it would be via a virtual call (i.e. it implements a compile-time known interface/abstract that you could call) or an indirect call (i.e. via a delegate invocation). I imagine that both of these scenarios would be slower than the overhead of boxing/unboxing anyway.
关于这种方法的另一种想法。Reflection.Emit 可以创建上面的方法,但是绑定到它的唯一方法是通过虚拟调用(即它实现了可以调用的编译时已知接口/抽象)或间接调用(即通过委托调用)。我想这两种情况无论如何都会比装箱/拆箱的开销慢。
Also, don't forget that the JIT is not dumb and may take care of this for you. (EDITsee Eric Lippert's comment on the original question -- he says the jitter does not currently perform this optimisation.)
另外,不要忘记 JIT 并不笨,可能会为您处理这个问题。(编辑请参阅 Eric Lippert 对原始问题的评论——他说抖动目前不执行此优化。)
As with all performance related issues: measure, measure, measure!
与所有与性能相关的问题一样:测量,测量,再测量!
回答by Cecil Has a Name
I guess you can always use System.Reflection.Emit to create a dynamic method and emit the instructions that do this without boxing, although it might be unverifiable.
我想您总是可以使用 System.Reflection.Emit 来创建一个动态方法并发出无需装箱即可执行此操作的指令,尽管它可能无法验证。
回答by Michael B
I know I'm way late to the party, but if you just need to do a safe cast like this you can use the following using Delegate.CreateDelegate
:
我知道我参加聚会已经很晚了,但是如果您只需要进行这样的安全转换,您可以使用以下命令Delegate.CreateDelegate
:
public static int Identity(int x){return x;}
// later on..
Func<int,int> identity = Identity;
Delegate.CreateDelegate(typeof(Func<int,TEnum>),identity.Method) as Func<int,TEnum>
now without writing Reflection.Emit
or expression trees you have a method that will convert int to enum without boxing or unboxing. Note that TEnum
here must have an underlying type of int
or this will throw an exception saying it cannot be bound.
现在无需编写Reflection.Emit
或表达式树,您就有了一种无需装箱或拆箱即可将 int 转换为 enum 的方法。请注意,TEnum
这里必须有一个底层类型,int
否则这将抛出一个异常,表明它不能被绑定。
Edit: Another method that works too and might be a little less to write...
编辑:另一种方法也有效,可能写得少一点......
Func<TEnum,int> converter = EqualityComparer<TEnum>.Default.GetHashCode;
This works to convert your 32bit or lessenum from a TEnum to an int. Not the other way around. In .Net 3.5+, the EnumEqualityComparer
is optimized to basically turn this into a return (int)value
;
这可以将 32 位或更少的枚举从 TEnum 转换为 int。不是反过来。在 .Net 3.5+ 中,EnumEqualityComparer
优化了基本上把它变成了 return (int)value
;
You are paying the overhead of using a delegate, but it certainly will be better than boxing.
您正在支付使用委托的开销,但它肯定会比拳击更好。
回答by NSGaga-mostly-inactive
...I'm even 'later' : )
......我什至“稍后”:)
but just to extend on the previous post (Michael B), which did all the interesting work
但只是为了扩展上一篇文章(迈克尔 B),它做了所有有趣的工作
and got me interested into making a wrapper for a generic case (if you want to cast generic to enum actually)
并让我有兴趣为通用案例制作包装器(如果您想将通用转换为实际枚举)
...and optimized a bit... (note: the main point is to use 'as' on Func<>/delegates instead - as Enum, value types do not allow it)
...并进行了一些优化...(注意:重点是在 Func<>/delegates 上使用 'as' - 作为 Enum,值类型不允许这样做)
public static class Identity<TEnum, T>
{
public static readonly Func<T, TEnum> Cast = (Func<TEnum, TEnum>)((x) => x) as Func<T, TEnum>;
}
...and you can use it like this...
......你可以像这样使用它......
enum FamilyRelation { None, Father, Mother, Brother, Sister, };
class FamilyMember
{
public FamilyRelation Relation { get; set; }
public FamilyMember(FamilyRelation relation)
{
this.Relation = relation;
}
}
class Program
{
static void Main(string[] args)
{
FamilyMember member = Create<FamilyMember, FamilyRelation>(FamilyRelation.Sister);
}
static T Create<T, P>(P value)
{
if (typeof(T).Equals(typeof(FamilyMember)) && typeof(P).Equals(typeof(FamilyRelation)))
{
FamilyRelation rel = Identity<FamilyRelation, P>.Cast(value);
return (T)(object)new FamilyMember(rel);
}
throw new NotImplementedException();
}
}
...for (int) - just (int)rel
... for (int) - 只是 (int)rel
回答by nawfal
This is similar to answers posted here, but uses expression trees to emit il to cast between types. Expression.Convert
does the trick. The compiled delegate (caster) is cached by an inner static class. Since source object can be inferred from the argument, I guess it offers cleaner call. For e.g. a generic context:
这类似于此处发布的答案,但使用表达式树发出 il 以在类型之间进行转换。Expression.Convert
诀窍。编译后的委托(caster)由内部静态类缓存。由于可以从参数中推断出源对象,我猜它提供了更清晰的调用。例如,通用上下文:
static int Generic<T>(T t)
{
int variable = -1;
// may be a type check - if(...
variable = CastTo<int>.From(t);
return variable;
}
The class:
班上:
/// <summary>
/// Class to cast to type <see cref="T"/>
/// </summary>
/// <typeparam name="T">Target type</typeparam>
public static class CastTo<T>
{
/// <summary>
/// Casts <see cref="S"/> to <see cref="T"/>.
/// This does not cause boxing for value types.
/// Useful in generic methods.
/// </summary>
/// <typeparam name="S">Source type to cast from. Usually a generic type.</typeparam>
public static T From<S>(S s)
{
return Cache<S>.caster(s);
}
private static class Cache<S>
{
public static readonly Func<S, T> caster = Get();
private static Func<S, T> Get()
{
var p = Expression.Parameter(typeof(S));
var c = Expression.ConvertChecked(p, typeof(T));
return Expression.Lambda<Func<S, T>>(c, p).Compile();
}
}
}
You can replace the caster
func with other implementations. I will compare performance of a few:
您可以将caster
func替换为其他实现。我将比较几个的性能:
direct object casting, ie, (T)(object)S
caster1 = (Func<T, T>)(x => x) as Func<S, T>;
caster2 = Delegate.CreateDelegate(typeof(Func<S, T>), ((Func<T, T>)(x => x)).Method) as Func<S, T>;
caster3 = my implementation above
caster4 = EmitConverter();
static Func<S, T> EmitConverter()
{
var method = new DynamicMethod(string.Empty, typeof(T), new[] { typeof(S) });
var il = method.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
if (typeof(S) != typeof(T))
{
il.Emit(OpCodes.Conv_R8);
}
il.Emit(OpCodes.Ret);
return (Func<S, T>)method.CreateDelegate(typeof(Func<S, T>));
}
Boxed casts:
盒装演员表:
int
toint
object casting -> 42 ms
caster1 -> 102 ms
caster2 -> 102 ms
caster3 -> 90 ms
caster4 -> 101 msint
toint?
object casting -> 651 ms
caster1 -> fail
caster2 -> fail
caster3 -> 109 ms
caster4 -> failint?
toint
object casting -> 1957 ms
caster1 -> fail
caster2 -> fail
caster3 -> 124 ms
caster4 -> failenum
toint
object casting -> 405 ms
caster1 -> fail
caster2 -> 102 ms
caster3 -> 78 ms
caster4 -> failint
toenum
object casting -> 370 ms
caster1 -> fail
caster2 -> 93 ms
caster3 -> 87 ms
caster4 -> failint?
toenum
object casting -> 2340 ms
caster1 -> fail
caster2 -> fail
caster3 -> 258 ms
caster4 -> failenum?
toint
object casting -> 2776 ms
caster1 -> fail
caster2 -> fail
caster3 -> 131 ms
caster4 -> fail
int
到int
对象投射 -> 42 ms
caster1 -> 102 ms
caster2 -> 102 ms
caster3 -> 90 ms
caster4 -> 101 msint
到int?
对象转换 -> 651 ms
caster1 -> 失败
caster2 -> 失败
caster3 -> 109 ms
caster4 -> 失败int?
到int
对象转换 -> 1957 ms
caster1 -> 失败
caster2 -> 失败
caster3 -> 124 ms
caster4 -> 失败enum
到int
对象转换 -> 405 ms
caster1 -> 失败
caster2 -> 102 ms
caster3 -> 78 ms
caster4 -> 失败int
到enum
对象转换 -> 370 ms
caster1 -> 失败
caster2 -> 93 ms
caster3 -> 87 ms
caster4 -> 失败int?
到enum
对象转换 -> 2340 ms
caster1 -> 失败
caster2 -> 失败
caster3 -> 258 ms
caster4 -> 失败enum?
到int
对象转换 -> 2776 ms
caster1 -> 失败
caster2 -> 失败
caster3 -> 131 ms
caster4 -> 失败
Expression.Convert
puts a direct cast from source type to target type, so it can work out explicit and implicit casts (not to mention reference casts). So this gives way for handling casting which is otherwise possible only when non-boxed (ie, in a generic method if you do (TTarget)(object)(TSource)
it will explode if it is not identity conversion (as in previous section) or reference conversion (as shown in later section)). So I will include them in tests.
Expression.Convert
将直接从源类型转换为目标类型,因此它可以计算出显式和隐式转换(更不用说引用转换了)。所以这为处理强制转换提供了方式,否则只有在非装箱时才有可能(即,在通用方法中,如果你这样做,(TTarget)(object)(TSource)
它会爆炸,如果它不是身份转换(如前一节)或引用转换(如后一节所示) ))。所以我会将它们包含在测试中。
Non-boxed casts:
非盒装演员:
int
todouble
object casting -> fail
caster1 -> fail
caster2 -> fail
caster3 -> 109 ms
caster4 -> 118 msenum
toint?
object casting -> fail
caster1 -> fail
caster2 -> fail
caster3 -> 93 ms
caster4 -> failint
toenum?
object casting -> fail
caster1 -> fail
caster2 -> fail
caster3 -> 93 ms
caster4 -> failenum?
toint?
object casting -> fail
caster1 -> fail
caster2 -> fail
caster3 -> 121 ms
caster4 -> failint?
toenum?
object casting -> fail
caster1 -> fail
caster2 -> fail
caster3 -> 120 ms
caster4 -> fail
int
到double
对象转换 -> 失败
caster1 -> 失败
caster2 -> 失败
caster3 -> 109 ms
caster4 -> 118 msenum
到int?
对象转换 -> 失败
caster1 -> 失败
caster2 -> 失败
caster3 -> 93 ms
caster4 -> 失败int
到enum?
对象转换 -> 失败
caster1 -> 失败
caster2 -> 失败
caster3 -> 93 ms
caster4 -> 失败enum?
到int?
对象转换 -> 失败
caster1 -> 失败
caster2 -> 失败
caster3 -> 121 ms
caster4 -> 失败int?
到enum?
对象转换 -> 失败
caster1 -> 失败
caster2 -> 失败
caster3 -> 120 ms
caster4 -> 失败
For the fun of it, I tested a few reference type conversions:
为了好玩,我测试了一些引用类型转换:
PrintStringProperty
tostring
(representation changing)object casting -> fail (quite obvious, since it is not cast back to original type)
caster1 -> fail
caster2 -> fail
caster3 -> 315 ms
caster4 -> failstring
toobject
(representation preserving reference conversion)object casting -> 78 ms
caster1 -> fail
caster2 -> fail
caster3 -> 322 ms
caster4 -> fail
PrintStringProperty
到string
(表示改变)对象转换 -> 失败(很明显,因为它没有转换回原始类型)
caster1 -> 失败
caster2 -> 失败
caster3 -> 315 ms
caster4 -> 失败string
toobject
(表示保留引用转换)对象转换 -> 78 ms
caster1 -> 失败
caster2 -> 失败
caster3 -> 322 ms
caster4 -> 失败
Tested like this:
测试如下:
static void TestMethod<T>(T t)
{
CastTo<int>.From(t); //computes delegate once and stored in a static variable
int value = 0;
var watch = Stopwatch.StartNew();
for (int i = 0; i < 10000000; i++)
{
value = (int)(object)t;
// similarly value = CastTo<int>.From(t);
// etc
}
watch.Stop();
Console.WriteLine(watch.Elapsed.TotalMilliseconds);
}
Note:
笔记:
My estimate is that unless you run this at least a hundred thousand times, it's not worth it, and you have almost nothing to worry about boxing. Mind you caching delegates has a hit on memory. But beyond that limit, the speed improvement is significant, especially when it comes to casting involving nullables.
But the real advantage of the
CastTo<T>
class is when it allows casts that are possible non-boxed, like(int)double
in a generic context. As such(int)(object)double
fails in these scenarios.I have used
Expression.ConvertChecked
instead ofExpression.Convert
so that arithmetic overflows and underflows are checked (ie results in exception). Since il is generated during run time, and checked settings are a compile time thing, there is no way you can know the checked context of calling code. This is something you have to decide yourself. Choose one, or provide overload for both (better).If a cast doesn't exist from
TSource
toTTarget
, exception is thrown while the delegate is compiled. If you want a different behaviour, like get a default value ofTTarget
, you can check type compatibility using reflection before compiling delegate. You have the full control of the code being generated. Its going to be extremely tricky though, you have to check for reference compatibility (IsSubClassOf
,IsAssignableFrom
), conversion operator existence (going to be hacky), and even for some built in type convertibility between primitive types. Going to be extremely hacky. Easier is to catch exception and return default value delegate based onConstantExpression
. Just stating a possibility that you can mimic behaviour ofas
keyword which doesnt throw. Its better to stay away from it and stick to convention.
我的估计是,除非你至少运行十万次,否则不值得,而且你几乎没有什么可担心的拳击。请注意,缓存委托会影响内存。但是超出这个限制,速度的提高是显着的,尤其是在涉及 nullables 的强制转换时。
但
CastTo<T>
该类的真正优势在于它允许可能非装箱的强制转换,例如(int)double
在通用上下文中。因此(int)(object)double
在这些场景中失败。我使用了
Expression.ConvertChecked
代替,Expression.Convert
以便检查算术上溢和下溢(即导致异常)。由于 il 是在运行时生成的,并且检查设置是编译时的事情,因此您无法知道调用代码的检查上下文。这是你必须自己决定的事情。选择一个,或为两者提供过载(更好)。如果从
TSource
to不存在强制转换TTarget
,则在编译委托时会引发异常。如果您想要不同的行为,例如获取默认值TTarget
,您可以在编译委托之前使用反射检查类型兼容性。您可以完全控制正在生成的代码。不过,这将非常棘手,您必须检查引用兼容性 (IsSubClassOf
,IsAssignableFrom
)、转换运算符是否存在(将变得很棘手),甚至是原始类型之间的某些内置类型可转换性。将是非常hacky。更容易的是捕获异常并返回基于ConstantExpression
. 只是说明您可以模仿as
不会抛出的关键字行为的可能性。最好远离它并坚持惯例。
回答by silver
I hope that I'm not too late...
我希望我不会太迟......
I think that you should consider to solve your problem with a different approach instead of using Enums try to creating a class with a public static readonly properties.
我认为您应该考虑使用不同的方法来解决您的问题,而不是使用 Enums 尝试创建一个具有公共静态只读属性的类。
if you will use that approach you will have an object that "feels" like an Enum but you will have all the flexibility of a class which means that you can override any of the operators.
如果您将使用这种方法,您将拥有一个“感觉”像 Enum 的对象,但您将拥有类的所有灵活性,这意味着您可以覆盖任何运算符。
there are other advantages like making that class a partial which will enable you to define the same enum in more then one file/dll which makes it possible to add values to a common dll without recompiling it.
还有其他优点,例如使该类成为部分类,这将使您能够在多个文件/dll 中定义相同的枚举,从而可以将值添加到公共 dll 中而无需重新编译它。
I couldn't find any good reason not to take that approach (this class will be located in the heap and not on the stack which is slower but it's worth it)
我找不到任何不采用这种方法的好理由(这个类将位于堆中,而不是位于较慢但值得的堆栈中)
please let me know what you think.
请让我知道你的想法。
回答by MooLim Lee
Here is a simplest and fastest way.
(with a little restriction. :-) )
这是一个最简单和最快的方法。
(有一点限制。:-))
public class BitConvert
{
[StructLayout(LayoutKind.Explicit)]
struct EnumUnion32<T> where T : struct {
[FieldOffset(0)]
public T Enum;
[FieldOffset(0)]
public int Int;
}
public static int Enum32ToInt<T>(T e) where T : struct {
var u = default(EnumUnion32<T>);
u.Enum = e;
return u.Int;
}
public static T IntToEnum32<T>(int value) where T : struct {
var u = default(EnumUnion32<T>);
u.Int = value;
return u.Enum;
}
}
Restriction:
This works in Mono. (ex. Unity3D)
限制:
这适用于 Mono。(例如 Unity3D)
More information about Unity3D:
ErikE's CastTo class is a really neat way to solve this problem.
BUT it can't be used as is in Unity3D
有关 Unity3D 的更多信息:
ErikE 的 CastTo 类是解决此问题的非常巧妙的方法。
但它不能在 Unity3D 中使用
First, it have to be fixed like below.
(because that the mono compiler can't compile the original code)
首先,它必须像下面一样固定。
(因为mono编译器无法编译原代码)
public class CastTo {
protected static class Cache<TTo, TFrom> {
public static readonly Func<TFrom, TTo> Caster = Get();
static Func<TFrom, TTo> Get() {
var p = Expression.Parameter(typeof(TFrom), "from");
var c = Expression.ConvertChecked(p, typeof(TTo));
return Expression.Lambda<Func<TFrom, TTo>>(c, p).Compile();
}
}
}
public class ValueCastTo<TTo> : ValueCastTo {
public static TTo From<TFrom>(TFrom from) {
return Cache<TTo, TFrom>.Caster(from);
}
}
Second, ErikE's code can't be used in AOT platform.
So, my code is the best solution for Mono.
其次,ErikE 的代码不能在 AOT 平台上使用。
所以,我的代码是 Mono 的最佳解决方案。
To commenter 'Kristof':
I am sorry that I didn't write all the details.
致评论者“克里斯托夫”:
很抱歉我没有写下所有细节。
回答by Eideren
Here's a very straight forward solution with C# 7.3's unmanaged generic type constraint:
这是一个非常直接的解决方案,其中包含 C# 7.3 的非托管泛型类型约束:
using System;
public static class EnumExtensions<TEnum> where TEnum : unmanaged, Enum
{
/// <summary>
/// Converts a <typeparam name="TEnum"></typeparam> into a <typeparam name="TResult"></typeparam>
/// through pointer cast.
/// Does not throw if the sizes don't match, clips to smallest data-type instead.
/// So if <typeparam name="TResult"></typeparam> is smaller than <typeparam name="TEnum"></typeparam>
/// bits that cannot be captured within <typeparam name="TResult"></typeparam>'s size will be clipped.
/// </summary>
public static TResult To<TResult>( TEnum value ) where TResult : unmanaged
{
unsafe
{
if( sizeof(TResult) > sizeof(TEnum) )
{
// We might be spilling in the stack by taking more bytes than value provides,
// alloc the largest data-type and 'cast' that instead.
TResult o = default;
*((TEnum*) & o) = value;
return o;
}
else
{
return * (TResult*) & value;
}
}
}
/// <summary>
/// Converts a <typeparam name="TSource"></typeparam> into a <typeparam name="TEnum"></typeparam>
/// through pointer cast.
/// Does not throw if the sizes don't match, clips to smallest data-type instead.
/// So if <typeparam name="TEnum"></typeparam> is smaller than <typeparam name="TSource"></typeparam>
/// bits that cannot be captured within <typeparam name="TEnum"></typeparam>'s size will be clipped.
/// </summary>
public static TEnum From<TSource>( TSource value ) where TSource : unmanaged
{
unsafe
{
if( sizeof(TEnum) > sizeof(TSource) )
{
// We might be spilling in the stack by taking more bytes than value provides,
// alloc the largest data-type and 'cast' that instead.
TEnum o = default;
*((TSource*) & o) = value;
return o;
}
else
{
return * (TEnum*) & value;
}
}
}
}
Requires unsafe toggle in your project configuration.
需要在项目配置中进行不安全切换。
Usage:
用法:
int intValue = EnumExtensions<YourEnumType>.To<int>( yourEnumValue );
Edit: Replaced Buffer.MemoryCopy
by simple pointer cast from dahall's suggestion.
编辑:替换Buffer.MemoryCopy
为来自 dahall 建议的简单指针。
回答by C0DEF52
If you'd like to speed up conversion, restricted to use unsafe code and can't emit IL you may want to consider to make generic class as abstract and implement conversion in derived classes. For example, when you code for Unity engine you probably want to build IL2CPP targets which are not compatible with emit. Here is an example of how it can be implemented:
如果您想加快转换速度,限制使用不安全代码并且不能发出 IL,您可能需要考虑将泛型类设为抽象类并在派生类中实现转换。例如,当您为 Unity 引擎编码时,您可能想要构建与发射不兼容的 IL2CPP 目标。这是一个如何实现的示例:
// Generic scene resolver is abstract and requires
// to implement enum to index conversion
public abstract class SceneResolver<TSceneTypeEnum> : ScriptableObject
where TSceneTypeEnum : Enum
{
protected ScenePicker[] Scenes;
public string GetScenePath ( TSceneTypeEnum sceneType )
{
return Scenes[SceneTypeToIndex( sceneType )].Path;
}
protected abstract int SceneTypeToIndex ( TSceneTypeEnum sceneType );
}
// Here is some enum for non-generic class
public enum SceneType
{
}
// Some non-generic implementation
public class SceneResolver : SceneResolver<SceneType>
{
protected override int SceneTypeToIndex ( SceneType sceneType )
{
return ( int )sceneType;
}
}
I tested boxing vs. virtual method and got 10x speed up for virtual method approach on macOS for both Mono and IL2CPP targets.
我测试了拳击与虚拟方法,并在 macOS 上为 Mono 和 IL2CPP 目标的虚拟方法方法提速了 10 倍。