为什么 .NET 的反射这么慢? 已翻译 100%

oschina 投递于 2016/12/19 10:59 (共 12 段, 翻译完成于 12-20)
阅读 6526
收藏 64
10
加载中

大家都知道 .NET 的反射很慢,但是为什么会出现这种情况呢?这篇文章会带你寻找这个问题的真正原因。

CLR 类型系统的设计目标

原因之一是,在设计的时候反射本身就不是以高性能为目标的,可以参考Type System Overview - ‘Design Goals and Non-goals’(类型系统概览 - ‘设计目标和非目标’):

目标

  • 运行时通过快速执行(非反射)代码访问需要的信息。

  • 编译时直接访问所需要的信息来生成代码。

  • 垃圾回收/遍历栈可以访问需要信息而不需要锁或分配内存。

  • 一次只加载最少量的类型。

  • 类型加载时只加载最少需要加载的类型。

  • 类型系统的数据结构必须在 NGEN 映像中保存。

非目标

  • 元数据的所有信息能直接反射 CLR 数据结构。

  • 快速使用反射。

参阅出处相同的 Type Loader Design - ‘Key Data Structures’(类型加载器设计 - ‘关键数据结构’):

EEClass

MethodTable(方法表)数据分为“热”和“冷”两种结构,以提高工作集和缓存的利用率。MethodTable 本身只存储程序稳定状态的“热”数据。EEClass 存储“冷”数据,它们通常是类型加载、JITing或反射所需要的。每个 MethodTable 指向一个 EEClass。

边城
边城
翻译于 2016/12/19 13:14
2

反射是如何工作的?

我们已经知道反射本身就不是以快为目标来设计的,但是它为什么需要那么多时间呢?

为了说明这个问题,来看看反射调用过程中,托管代码和非托管代码的调用栈。

  • System.Reflection.RuntimeMethodInfo.Invoke(..) - 源码链接

    • 调用 System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(..)

  • System.RuntimeMethodHandle.PerformSecurityCheck(..) - 链接

    • 调用 System.GC.KeepAlive(..)

  • System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(..) - 链接

    • 调用 System.RuntimeMethodHandle.InvokeMethod(..) 的存根

  • System.RuntimeMethodHandle.InvokeMethod(..) 的存根 - 链接

即使不点击链接,想必你也能直观感受到改方法执行的大量代码。参考示例:System.RuntimeMethodHandle.InvokeMethodis 超过 400 行代码

边城
边城
翻译于 2016/12/19 13:25
0

那么,它具体在做什么?

获取方法信息

要使用反射来调用字段/属性/方法,你必须获得 FieldInfo/PropertyInfo/MethodInfo,使用这样的代码:

Type t = typeof(Person);      
FieldInfo m = t.GetField("Name");

这需要一定的成本,因为需要提取相关的元数据,并对其进行解析。运行时会帮我们维持一个内部缓存,缓存着所有字段/属性/方法。这个缓存由 RuntimeTypeCache 类实现,用法示例在 RuntimeMethodInfo 类中.

运行 gist 中的代码你可以看到缓存的何运作方式,它恰如其分地使用反射检查运行时内部!

gist 上的代码会在你使用反射获得 FieldInfo 之前输出下列内容:

Type: ReflectionOverhead.Program
Reflection Type: System.RuntimeType (BaseType: System.Reflection.TypeInfo)
m_fieldInfoCache is null, cache has not been initialised yet

不过一旦你获得字段,就会输出:

Type: ReflectionOverhead.Program
Reflection Type: System.RuntimeType (BaseType: System.Reflection.TypeInfo)
RuntimeTypeCache: System.RuntimeType+RuntimeTypeCache, 
m_cacheComplete = True, 4 items in cache
  [0] - Int32 TestField1 - Private
  [1] - System.String TestField2 - Private
  [2] - Int32 <TestProperty1>k__BackingField - Private
  [3] - System.String TestField3 - Private, Static

ReflectionOverhead.Program 看起来像这样:

class Program
{
    private int TestField1;
    private string TestField2;
    private static string TestField3;

    private int TestProperty1 { get; set; }
}

看来运行时会筛选已经创建过的东西,这意味着调用 GetFeild 或 GetFields 不需要多大代价。对于 GetMethod 和 GetProperty 来说也是如此,MethodInfo 或 PropertyInfo 会在你第一次调用的时候创建并缓存起来。

边城
边城
翻译于 2016/12/19 13:58
0

参数校验和错误处理

得到 MethodInfo 之后,如果调用它的 Invoke 方法,会要处理很多事项。假设编写代码如下:

PropertyInfo stringLengthField = 
    typeof(string).GetProperty("Length", 
        BindingFlags.Instance | BindingFlags.Public);
var length = stringLengthField.GetGetMethod().Invoke(new Uri(), new object[0]);

如果运行上述代码,会得到下面的异常:

System.Reflection.TargetException: Object does not match target type.
   at System.Reflection.RuntimeMethodInfo.CheckConsistency(..)
   at System.Reflection.RuntimeMethodInfo.InvokeArgumentsCheck(..)
   at System.Reflection.RuntimeMethodInfo.Invoke(..)
   at System.Reflection.RuntimePropertyInfo.GetValue(..)

这是因为我们获得了 String 类 Length 属性的 PropertyInfo,但是却在 Uri 对象上调用它,显然,这是个错误的类型!

此外,你还必须在调用方法时对传递给方法的参数进行校验。为了能传递参数,反射 API 使用了一个 object 的数组作为参数,其中每一个元素表示一个参数。所以,如果你使用反射来调用 Add(int x, int y) 方法,你得调用 methodInfo.Invoke(.., new [] { 5, 6 })。运行时会对传入参数的数量和类型进行检查,在这个示例中你要确保是 2 个 int 类型的参数。这些工作不好的地方是常常需要装箱,这会增加额外的成本。希望这在将来会降到最低

Viyi
Viyi
翻译于 2016/12/19 21:44
2

安全性检查

另一个主要任务是多重安全性检查。例如,你不允许使用反射来任意调用你想调用的方法。这里存在一些限制的或 ‘危险方法’,只能由可信度高的 .NET 框架代码调用。除了黑名单外,还有动态安全检查,它由调用时必须检查的的当前代码访问安全权限决定。

Tocy
Tocy
翻译于 2016/12/19 16:53
0

反射机制耗时多少?

了解反射的实际操作后,我们来看看实际耗时。请注意,这些基准测试是通过反射直接比较读/写属性来完成的。在 .NET 中属性是一对 Get/Set 方法,这是由编译器生成的,但当属性只包含一个简单的内嵌字段时,.NET JIT 会使用内联 Get/Set 方法以提升性能。这意味着使用反射访问属性可能会遇到反射性能最差的情况,但它会被选择是因为这是最常见的用例,数据位于 ORMsJson 序列化/反序列化库对象映射工具中。

以下是由 BenchmarkDotNet 提供的原始结果,后面是在2个单独的表中显示的相同结果。 (全部Benchmark代码由此下载 

Tocy
Tocy
翻译于 2016/12/19 17:28
0

读取属性值(‘Get’)

写属性值(‘Set’)

我们可以清楚地看到,正常的反射代码(GetViaReflection/SetViaReflection)比直接访问属性(GetViaProperty/SetViaProperty)要慢得多。 其他结果,我们还要进一步分析。

设置

首先我们从 aTestClass 开始,代码如下:

public class TestClass
{
    public TestClass(String data)
    {
        Data = data;
    }

    private string data;
    private string Data
    {
        get { return data; }
        set { data = value; }
    }
}

以及下面的通用代码,这里包含了所有可用的选项:

// Setup code, done only once 
TestClass testClass = new TestClass("A String");
Type @class = testClass.GetType();
BindingFlag bindingFlags = BindingFlags.Instance | 
                           BindingFlags.NonPublic | 
                           BindingFlags.Public;

正常的反射

首先我们使用常规基准代码来表示我们的起始情况和“最坏情况”:

[Benchmark]public string GetViaReflection()
{
    PropertyInfo property = @class.GetProperty("Data", bindingFlags);
    return (string)property.GetValue(testClass, null);
}
Tocy
Tocy
翻译于 2016/12/19 17:36
1

选择1 - 缓存 PropertyInfo

接下来,我们通过保存引用至 PropertyInfo 以获得速度上的少量提升,而不是每次都去获取。但即使这样,与直接访问属性相比,也仍然慢得多,这就表明在反射的“调用”部分成本很高。

// Setup code, done only once
PropertyInfo cachedPropertyInfo = @class.GetProperty("Data", bindingFlags);

[Benchmark]
public string GetViaReflection()
{    
    return (string)cachedPropertyInfo.GetValue(testClass, null);
}

选择2 - 使用 FastMember

这里使用了 Marc Gravell 优秀的 Fast Member 库,这个库用起来很简单!

// Setup code, done only once
TypeAccessor accessor = TypeAccessor.Create(@class, allowNonPublicAccessors: true);

[Benchmark]
public string GetViaFastMember()
{
    return (string)accessor[testClass, "Data"];
}

注意,与其他选择稍有不同,它创建了一个 TypeAccessor 来访问类型中的所有属性,而不仅是某一个。这带来的负面影响是会导致运行时间变长,因为它在内部首先要为你请求的属性(这个例子中是‘Data’)创建委托,然后再获取其值。不过这种开销是很小的,FastMember 仍然比其它反射方法更快,也更易用。所以我建议你先去看看。

这个选择及随后的选择将反射代码转换委托,这样就可以直接调用而不再需要每次都进行反射,速度因此得到提升!

必须指出创建一个委托需要一定的成本(可以从 ‘相关阅读’ 了解更多)。总之,速度提升是因为我们在其中进行过一次大投入(安全检查等)并保存了一个强类型的委托,之后我们只要稍微付出一点就可以一次次调用。如果反射只进行一次,那你大可不必使用这些技术。但是如果你只进行一次反射操作,它也不会出现性能瓶颈,你就完全不用在乎它会变慢!

通过委托读某个属性仍然不如直接访问来得快,因为 .NET JIT 不会将对委托方法的调用进行内联优化,而直接访问属性则会。因此即使使用委托,我们也需要为调用方法付出成本,而直接访问属性就不会。

Viyi
Viyi
翻译于 2016/12/19 22:35
1

选项3——创建代理(Delegate)

在这个选项中,我们使用 CreateDelegate 函数来将 PropertyInfo 转换为常规的 delegate:

// Setup code, done only once
PropertyInfo property = @class.GetProperty("Data", bindingFlags);
Func<TestClass, string> getDelegate = 
    (Func<TestClass, string>)Delegate.CreateDelegate(
             typeof(Func<TestClass, string>), 
             property.GetGetMethod(nonPublic: true));

[Benchmark]
public string GetViaDelegate()
{
    return getDelegate(testClass);
}

它的缺点是你必须知道编译时的具体类型,也就是上面的代码中的 Func<TestClass,string> 部分(如果使用 Func<object,string>,编译器会抛出一个异常!)。不过,在大多数情况下,使用反射不会遇到这么多麻烦。

有效避免麻烦,请参阅 MagicMethodHelper 代码(在 Jon Skeet 发布的“Making Reflection fly and exploring delegates“博客中),或阅读下面的选项 4 或 5。

Tocy
Tocy
翻译于 2016/12/19 17:14
0

选项4——编译表达式树(Compiled Expression Trees)

这里我们生成一个 delegate,但不同的是我们可以传入一个 object,所以我们会看到“选项4”的限制。我们使用支持动态代码生成的 .NET Expression tree API

// Setup code, done only once
PropertyInfo property = @class.GetProperty("Data", bindingFlags);
ParameterExpression = Expression.Parameter(typeof(object), "instance");
UnaryExpression instanceCast = 
    !property.DeclaringType.IsValueType ? 
        Expression.TypeAs(instance, property.DeclaringType) : 
        Expression.Convert(instance, property.DeclaringType);
Func<object, object> GetDelegate = 
    Expression.Lambda<Func<object, object>>(
        Expression.TypeAs(
            Expression.Call(instanceCast, property.GetGetMethod(nonPublic: true)),
            typeof(object)), 
        instance)
    .Compile();

[Benchmark]
public string GetViaCompiledExpressionTrees()
{
    return (string)GetDelegate(testClass);
}

关于 Expression 的全部代码可以从“Faster Reflection using Expression Trees(使用表达式树的快速反射机制)“博客下载。

Tocy
Tocy
翻译于 2016/12/19 16:58
0
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
加载中

评论(31)

d
dogvane
评论怎么停了?
JunJian-Work
JunJian-Work
评论永远比正文更来劲~~~
中国首席鉴黄师
中国首席鉴黄师
自举什么意思都不知道,不过能被那么多新闻然后用来吹比还是挺牛逼的
妈她亲我
妈她亲我

引用来自“shitalpig”的评论

放你麻痹臭屁,c#比Java好多了

引用来自“eechen”的评论

那么好,还学人家跨平台,好定西应该用来吸引开发者使用Windows Server呀,让Windows Server独占呀,明显没人用被边缘化了只好跨平台,吹牛逼爱忽悠的微软在浏览器和移动时代hold不住了,被Java和PHP联合绞杀的节奏。

引用来自“妈她亲我”的评论

php能自举吗

引用来自“eechen”的评论

说得好像C草能自举似的

引用来自“妈她亲我”的评论

你的意思是因为C#不能自举,所以拍黄片也不能?

引用来自“eechen”的评论

C草不能自举那你还瞎哔哔个毛线

引用来自“asdf32ad”的评论

c♯新一代编译器是roslyn就是c♯写的,这算不算自举?

引用来自“eechen”的评论

PHP又不用编译,自举个蛋呀. 底层C做得那么好,闲得蛋疼才自举呀. PHP的朴实,其他语言只能望其项背. 某些语言靠吹牛逼提高性能,PHP7是靠不断的底层C编程优化提高性能. PHP不仅为了证明PHP,同时也证明了C是Web开发上的巨大作用. 所以甚至有鸟哥等用C开发供PHP项目使用的MVC框架. 这些对性能的帮助跟你那个自举比,那个更有现实意义?

引用来自“妈她亲我”的评论

有种你不需要用PHP解释器啊

引用来自“eechen”的评论

软粉真是神逻辑,笑喷!

引用来自“妈她亲我”的评论

C#可以写自己的编译器,有能耐你用PHP写个解释器

引用来自“eechen”的评论

C#可以写自己的编译器,有能耐你用C#写个解释器? 鸟哥可以自己用C写PHP解释器,又能耐你用C#写编译器?

引用来自“妈她亲我”的评论

鸟哥可以用PHP写个PHP解释器不?

引用来自“eechen”的评论

适用C的场景为什么鸟哥要用PHP写?软粉的逗逼逻辑.

引用来自“妈她亲我”的评论

php号称最好的语言,还要用别的语言来实现解释器。。。也不过如此。

引用来自“eechen”的评论

不服呀,不服你用CSharp写一个PHP出来呀,哈哈。
哈哈哈哈哈,这话应该我对你说。php那么牛逼,你倒是用php写个C#出来啊。
eechen
eechen

引用来自“shitalpig”的评论

放你麻痹臭屁,c#比Java好多了

引用来自“eechen”的评论

那么好,还学人家跨平台,好定西应该用来吸引开发者使用Windows Server呀,让Windows Server独占呀,明显没人用被边缘化了只好跨平台,吹牛逼爱忽悠的微软在浏览器和移动时代hold不住了,被Java和PHP联合绞杀的节奏。

引用来自“妈她亲我”的评论

php能自举吗

引用来自“eechen”的评论

说得好像C草能自举似的

引用来自“妈她亲我”的评论

你的意思是因为C#不能自举,所以拍黄片也不能?

引用来自“eechen”的评论

C草不能自举那你还瞎哔哔个毛线

引用来自“asdf32ad”的评论

c♯新一代编译器是roslyn就是c♯写的,这算不算自举?

引用来自“eechen”的评论

PHP又不用编译,自举个蛋呀. 底层C做得那么好,闲得蛋疼才自举呀. PHP的朴实,其他语言只能望其项背. 某些语言靠吹牛逼提高性能,PHP7是靠不断的底层C编程优化提高性能. PHP不仅为了证明PHP,同时也证明了C是Web开发上的巨大作用. 所以甚至有鸟哥等用C开发供PHP项目使用的MVC框架. 这些对性能的帮助跟你那个自举比,那个更有现实意义?

引用来自“妈她亲我”的评论

有种你不需要用PHP解释器啊

引用来自“eechen”的评论

软粉真是神逻辑,笑喷!

引用来自“妈她亲我”的评论

C#可以写自己的编译器,有能耐你用PHP写个解释器

引用来自“eechen”的评论

C#可以写自己的编译器,有能耐你用C#写个解释器? 鸟哥可以自己用C写PHP解释器,又能耐你用C#写编译器?

引用来自“妈她亲我”的评论

鸟哥可以用PHP写个PHP解释器不?

引用来自“eechen”的评论

适用C的场景为什么鸟哥要用PHP写?软粉的逗逼逻辑.

引用来自“妈她亲我”的评论

php号称最好的语言,还要用别的语言来实现解释器。。。也不过如此。
不服呀,不服你用CSharp写一个PHP出来呀,哈哈。
魂祭心
魂祭心
反射慢主要是元数据的查找,解析以及运行时调用的参数类型匹配转换等等,现在很多框架里面也用了反射,速度也不是完全不可以接受,反射invoke确实有些慢,实在忍受不了,作者也提供了很多方法,简单就是cache,复杂点可以转换委托
妈她亲我
妈她亲我

引用来自“shitalpig”的评论

放你麻痹臭屁,c#比Java好多了

引用来自“eechen”的评论

那么好,还学人家跨平台,好定西应该用来吸引开发者使用Windows Server呀,让Windows Server独占呀,明显没人用被边缘化了只好跨平台,吹牛逼爱忽悠的微软在浏览器和移动时代hold不住了,被Java和PHP联合绞杀的节奏。

引用来自“妈她亲我”的评论

php能自举吗

引用来自“eechen”的评论

说得好像C草能自举似的

引用来自“妈她亲我”的评论

你的意思是因为C#不能自举,所以拍黄片也不能?

引用来自“eechen”的评论

C草不能自举那你还瞎哔哔个毛线

引用来自“asdf32ad”的评论

c♯新一代编译器是roslyn就是c♯写的,这算不算自举?

引用来自“eechen”的评论

PHP又不用编译,自举个蛋呀. 底层C做得那么好,闲得蛋疼才自举呀. PHP的朴实,其他语言只能望其项背. 某些语言靠吹牛逼提高性能,PHP7是靠不断的底层C编程优化提高性能. PHP不仅为了证明PHP,同时也证明了C是Web开发上的巨大作用. 所以甚至有鸟哥等用C开发供PHP项目使用的MVC框架. 这些对性能的帮助跟你那个自举比,那个更有现实意义?

引用来自“妈她亲我”的评论

有种你不需要用PHP解释器啊

引用来自“eechen”的评论

软粉真是神逻辑,笑喷!

引用来自“妈她亲我”的评论

C#可以写自己的编译器,有能耐你用PHP写个解释器

引用来自“eechen”的评论

C#可以写自己的编译器,有能耐你用C#写个解释器? 鸟哥可以自己用C写PHP解释器,又能耐你用C#写编译器?

引用来自“妈她亲我”的评论

鸟哥可以用PHP写个PHP解释器不?

引用来自“eechen”的评论

适用C的场景为什么鸟哥要用PHP写?软粉的逗逼逻辑.
php号称最好的语言,还要用别的语言来实现解释器。。。也不过如此。
eechen
eechen

引用来自“shitalpig”的评论

放你麻痹臭屁,c#比Java好多了

引用来自“eechen”的评论

那么好,还学人家跨平台,好定西应该用来吸引开发者使用Windows Server呀,让Windows Server独占呀,明显没人用被边缘化了只好跨平台,吹牛逼爱忽悠的微软在浏览器和移动时代hold不住了,被Java和PHP联合绞杀的节奏。

引用来自“妈她亲我”的评论

php能自举吗

引用来自“eechen”的评论

说得好像C草能自举似的

引用来自“妈她亲我”的评论

你的意思是因为C#不能自举,所以拍黄片也不能?

引用来自“eechen”的评论

C草不能自举那你还瞎哔哔个毛线

引用来自“asdf32ad”的评论

c♯新一代编译器是roslyn就是c♯写的,这算不算自举?

引用来自“eechen”的评论

PHP又不用编译,自举个蛋呀. 底层C做得那么好,闲得蛋疼才自举呀. PHP的朴实,其他语言只能望其项背. 某些语言靠吹牛逼提高性能,PHP7是靠不断的底层C编程优化提高性能. PHP不仅为了证明PHP,同时也证明了C是Web开发上的巨大作用. 所以甚至有鸟哥等用C开发供PHP项目使用的MVC框架. 这些对性能的帮助跟你那个自举比,那个更有现实意义?

引用来自“妈她亲我”的评论

有种你不需要用PHP解释器啊

引用来自“eechen”的评论

软粉真是神逻辑,笑喷!

引用来自“妈她亲我”的评论

C#可以写自己的编译器,有能耐你用PHP写个解释器

引用来自“eechen”的评论

C#可以写自己的编译器,有能耐你用C#写个解释器? 鸟哥可以自己用C写PHP解释器,又能耐你用C#写编译器?

引用来自“妈她亲我”的评论

鸟哥可以用PHP写个PHP解释器不?
适用C的场景为什么鸟哥要用PHP写?软粉的逗逼逻辑.
妈她亲我
妈她亲我

引用来自“shitalpig”的评论

放你麻痹臭屁,c#比Java好多了

引用来自“eechen”的评论

那么好,还学人家跨平台,好定西应该用来吸引开发者使用Windows Server呀,让Windows Server独占呀,明显没人用被边缘化了只好跨平台,吹牛逼爱忽悠的微软在浏览器和移动时代hold不住了,被Java和PHP联合绞杀的节奏。

引用来自“妈她亲我”的评论

php能自举吗

引用来自“eechen”的评论

说得好像C草能自举似的

引用来自“妈她亲我”的评论

你的意思是因为C#不能自举,所以拍黄片也不能?

引用来自“eechen”的评论

C草不能自举那你还瞎哔哔个毛线

引用来自“asdf32ad”的评论

c♯新一代编译器是roslyn就是c♯写的,这算不算自举?

引用来自“eechen”的评论

PHP又不用编译,自举个蛋呀. 底层C做得那么好,闲得蛋疼才自举呀. PHP的朴实,其他语言只能望其项背. 某些语言靠吹牛逼提高性能,PHP7是靠不断的底层C编程优化提高性能. PHP不仅为了证明PHP,同时也证明了C是Web开发上的巨大作用. 所以甚至有鸟哥等用C开发供PHP项目使用的MVC框架. 这些对性能的帮助跟你那个自举比,那个更有现实意义?

引用来自“妈她亲我”的评论

有种你不需要用PHP解释器啊

引用来自“eechen”的评论

软粉真是神逻辑,笑喷!

引用来自“妈她亲我”的评论

C#可以写自己的编译器,有能耐你用PHP写个解释器

引用来自“eechen”的评论

C#可以写自己的编译器,有能耐你用C#写个解释器? 鸟哥可以自己用C写PHP解释器,又能耐你用C#写编译器?
鸟哥可以用PHP写个PHP解释器不?
eechen
eechen

引用来自“shitalpig”的评论

放你麻痹臭屁,c#比Java好多了

引用来自“eechen”的评论

那么好,还学人家跨平台,好定西应该用来吸引开发者使用Windows Server呀,让Windows Server独占呀,明显没人用被边缘化了只好跨平台,吹牛逼爱忽悠的微软在浏览器和移动时代hold不住了,被Java和PHP联合绞杀的节奏。

引用来自“妈她亲我”的评论

php能自举吗

引用来自“eechen”的评论

说得好像C草能自举似的

引用来自“妈她亲我”的评论

你的意思是因为C#不能自举,所以拍黄片也不能?

引用来自“eechen”的评论

C草不能自举那你还瞎哔哔个毛线

引用来自“asdf32ad”的评论

c♯新一代编译器是roslyn就是c♯写的,这算不算自举?

引用来自“eechen”的评论

PHP又不用编译,自举个蛋呀. 底层C做得那么好,闲得蛋疼才自举呀. PHP的朴实,其他语言只能望其项背. 某些语言靠吹牛逼提高性能,PHP7是靠不断的底层C编程优化提高性能. PHP不仅为了证明PHP,同时也证明了C是Web开发上的巨大作用. 所以甚至有鸟哥等用C开发供PHP项目使用的MVC框架. 这些对性能的帮助跟你那个自举比,那个更有现实意义?

引用来自“妈她亲我”的评论

有种你不需要用PHP解释器啊

引用来自“eechen”的评论

软粉真是神逻辑,笑喷!

引用来自“妈她亲我”的评论

C#可以写自己的编译器,有能耐你用PHP写个解释器
C#可以写自己的编译器,有能耐你用C#写个解释器? 鸟哥可以自己用C写PHP解释器,又能耐你用C#写编译器?
返回顶部
顶部