前几天的时候看到了 吕毅 大佬写的博客为什么 C# 的 string.Empty 是一个静态只读字段,而不是一个常量呢? , 非常感谢吕毅大佬的分享,在文章的末尾大佬提到了通过反射修改 String.Empty 的可能,于是我打算自己实践一下。
反射修改 首先上一个我自己封装的 ReflectionExtension 类,方便直接进行反射操作(这里只展示部分代码,完整代码会附上下载链接)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 namespace System.Reflection { public static class ReflectionExtension { public const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly; public static void SetField (this object instance, string fieldName, object value ) { var type = instance.GetType(); var field = type.GetField(fieldName) ?? type.GetField(fieldName, Flags); field?.SetValue(instance, value ); } } }
新建项目的框架为 .NET Framework 3.5,使用以下代码完成反射修改 String.Empty :
1 2 3 4 5 6 7 8 9 10 11 12 13 static void Main (string [] args ){ Console.WriteLine($"Empty : {string .Empty} " ); string .Empty.SetField("m_stringLength" , 3 ); "" .SetField("Empty" ,"CBA" ); Console.WriteLine($"Empty Reflection : {string .Empty} " ); Console.ReadLine(); }
运行结果如图: 有效! 查看一下 String.cs 的源码 .NET Framework 3.5 以下版本中的 String.Empty 就是一个普通的共有只读字段。
下面将框架版本升级到 4.0 以上再次尝试: 同样的方法在 4.0 以上的版本就行不通了,转到源码看一下: String.Empty 上增加了 __DynamicallyInvokable
Attribute,查找资料:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Type invocableAttribute = GetType("__DynamicallyInvokableAttribute" , false ); if (invocableAttribute != null ){ Contract.Assert(((MetadataToken)invocableAttribute.MetadataToken).IsTypeDef); ConstructorInfo ctor = invocableAttribute.GetConstructor(Type.EmptyTypes); Contract.Assert(ctor != null ); int token = ctor.MetadataToken; Contract.Assert(((MetadataToken)token).IsMethodDef); flags |= (ASSEMBLY_FLAGS)token & ASSEMBLY_FLAGS.ASSEMBLY_FLAGS_TOKEN_MASK; }
根据注释中的介绍,这个特性只是某种用于提升速度的标记,看来大约是跟 CLR 有关,与这个 Attribute 并没有什么关系。
不安全代码 下面加入不安全代码,使用指针来实现我们的设置 String.Empty 。 首先打开项目属性,设置生成中的允许不安全代码 选项 代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 using System.Reflection;namespace System { public static class StringHelper { public static unsafe void SetValue (this string str, string newStr ) { str.SetField("m_stringLength" , newStr.Length); fixed (char * pe = str) { for (var i = 0 ; i < newStr.Length; i++) pe[i] = newStr[i]; pe[newStr.Length] = '\0' ; } } } }
在 .NET Framework 4.7.2 下重新运行下面的测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static void Main (string [] args ){ Console.WriteLine($"Empty : {string .Empty} " ); string .Empty.SetField("m_stringLength" , 3 ); "" .SetField("Empty" ,"CBA" ); Console.WriteLine($"Empty Reflection : {string .Empty} " ); string .Empty.SetValue("ABC" ); Console.WriteLine($"Empty Unsafe : {string .Empty} " ); Console.ReadLine(); }
不安全代码可以在最新版 Framework 上实现修改 String.Empty。
总结 在各个平台下运行测试代码,结论如下:
平台 反射 不安全代码 .NET Framework 1.0 - 3.5 √ √ .NET Framework 4.0 - 4.7.2 × √ .NET Core 1.0 - 2.0 × √ .NET Core 2.1 - 2.2 × × .NET Core 3.0 preview FieldAccessException Cannot set initonly static field ‘Empty’ after type ‘System.String’ is initialized.×