硬核 Microsoft .Net





3.00/5 (20投票s)
Microsoft 不想让你知道的
MSIL 源代码:下载 GenericPointers.zip - 770 B
垃圾回收器测试:下载 GCtests.zip - 34.86 KB
引言
在本文中,我将探讨我在微软 CLR 实现中的一些发现,以及如何利用它们,据我所知,它们也包含一些风险。
背景
我将把要讨论的主题分为两部分,第一部分是解释问题,第二部分是在 MSIL 或 C# 中实现代码。我也要在此指出,由于存在风险,我并不建议您使用这些技术。
因此,我希望读者至少对 MSIL 是什么以及如何使用 ilasm.exe 编译它有所了解。
我也希望有读者能在 Mono 上测试这些东西并分享他们的经验。
问题与解决方案
我在 C# 中使用指针时遇到了一些限制,起初我认为其中大部分都是些疯狂的想法,直到我真正需要它们。问题在于我编写了一个包含大量不安全代码块的大型库,这使得维护变得越来越困难。
我解决这个问题的方法是基于我在 CodeProject 的一个发现。一旦我把它运行起来,我就想实现所有那些我脑海中的疯狂想法……你猜怎么着?它们确实可以实现!我不会很快就使用所有这些东西,但既然这是得益于 CodeProject,我决定将它回馈给社区。
为了让大家对我们正在处理的内容有一个初步的了解,我会说,你将能够实现泛型指针,以及一些忽略类型和其他一些你可能已经忘记的细节的算术运算。
兴趣点
如前所述,我们将处理一些关于指针和类型的奇怪问题,总的来说,我们会尽可能深入地探索不安全代码……关于这一点,有些人会将不安全(unsafe)与非托管(unmanaged)混淆,但这是不对的。非托管代码运行在 CLR 之外,比如 P/Invoke。而不安全代码则运行在 CLR 内部,这意味着你不会有离开和返回非托管代码的性能开销,但它也不像其非托管等价物那样快。
我得承认,我曾想过要戴上护目镜,以防 CPU 爆炸什么的,但既然我正在写这篇文章,你可以放心,风险并没有那么大。但我们肯定会做一些可能导致应用程序崩溃的事情,就像 VB 6.0 中 VarPtr 的时代一样。
如果我们回到那个时代……我们有 VarPtr
、ObjPtr
、StrPtr
和一个名为 ArrayPtr
的奇怪技巧。让我们开始在 C# 中实现它们。我知道有用于封送处理的解决方案,但我指的不是那个。我只想出于历史原因提一下,当时还有一个 ProcPtr
。还要注意的是,我们这里不会直接讨论 ObjPtr,但我可以告诉你,VarPtr
也能做到 ObjPtr
过去做的事情。
为什么垃圾回收器(GC)无法回收字符串?
首先,GC 是可以回收对象的,我指的是指向字符串内容的 System.String 类的实例。它能够在堆中移动它,并像回收任何其他对象一样回收它。就这样。
GC 无法做的是回收被“驻留”(interned)的字符串本身的内存,也就是存储所有字符的地方。这是否意味着它不能回收任何字符串?不是。这只意味着它不能回收“驻留字符串”,其中包括 .NET 中的字符串字面量。另外,字符串字面量也可能是“冻结字符串”,稍后会详细介绍。
那么什么是驻留字符串?对于那些曾使用过 Win32 的人来说,你可能知道一种叫做 ATOMs 的东西,ATOMs 是一种对象(在那个时代的概念中),它允许我们存储和处理字符串。ATOMs 是存储常用字符串的好方法,比如你应用程序窗口的文本。对开发者来说,好消息是他们只需要存储一次字符串。所有具有相同文本的字符串实际上都指向内存中的同一区域,从而减少了内存空间开销。而且这部分字符串内存由操作系统管理,任何进程都可以利用其中的文本。
好的,这正是 System.Intern 所做的事情。我不能确定它是否使用了 ATOMs,但它的行为方式是相同的,因此所有具有相同文本的字符串都指向内存中的同一位置(可能是由操作系统管理的内存……我们可以问问 Mono 的人他们是怎么做的)。想想用 GC 来回收那块内存需要做什么……你将需要查找所有可能指向那里的字符串,这包括指向部分内容和重叠部分的字符串……所以 GC 根本不关心内部字符串。但它肯定可以回收其余的字符串,比如用 StringBuilder 创建的那些,这就是为什么使用 StringBuilder 进行字符串操作是微软鼓励的一个好习惯。记住,你可以对任何字符串调用 Intern 方法。
说到 StringBuilder,字符串之所以是不可变的,有几个原因,第一个原因来自于获取子字符串的需求。如果你需要获取一个子字符串,而字符串不是不可变的,那么当你指向另一个字符串的内部部分,而那个字符串移动到另一个地方或改变了其内容时,会发生什么?答案是你的子字符串将被破坏,而你肯定不希望这样。
因此,像旧版 VB 这样的语言会创建字符串的副本来提供子字符串,这种行为使得连接、替换和写入操作变得容易。这有什么问题呢?问题在于,子字符串和连接操作都需要复制内存,所以设计者们决定让字符串不可变,这样获取子字符串的情况就会更快。但这导致了写入时的问题(称为 C.O.W,写时复制),并且连接操作的效率会降低。
如今我们有 StringBuilder 来解决这部分问题。在不可变字符串的其他优点中,我们可以发现它们被认为是线程安全的,并且可以被驻留以节省内存。
说了这么多,考虑一下对一个非内部字符串使用 StrPtr 会涉及到什么。
关于冻结字符串,这是在 .NET 2.0 版本中添加的一项功能,允许在多个用 .NET 开发的应用程序之间共享内存。我将提供一篇讨论此问题的文章链接:http://weblog.ikvm.net/PermaLink.aspx?guid=b28aa8b7-87e3-49d7-b0aa-3cc2cb5dbac9
1) 如何实现 StrPtr
这是最简单的一个,不涉及任何新发现……所以,给你了,拿去吧。
unsafe public static int StrPtr(string a)
{
fixed (char* p = a)
{
return (int)p;
}
}
我没有在这段代码中发现任何未记录的风险。我知道的唯一失败情况是传递 null 作为参数,这种情况下你将得到 0 作为返回值。如果你试图读写一个指向 0 的指针,你会得到 NullReferenceException。
首先,是的,这很危险;其次,不,GC 不会回收这个字符串。
另外一件事……如果你需要的是一个包含字符串信息的字节数组,你可以将这项工作委托给 System.Text
命名空间中的 Encoding
类型;如果你需要一个字符数组,请使用 String.ToCharArray()
。
我收到过关于 StrPtr
的抱怨,所以我会谈谈那些我认为你已经知道的风险。
using System;
using System.Runtime;
namespace Test
{
class Program
{
static void Main(string[] args)
{
unsafe
{
char* p = (char*)Test();
int g = GC.MaxGeneration;
GCSettings.LatencyMode = GCLatencyMode.Batch;
GC.Collect(g, GCCollectionMode.Forced);
GC.WaitForPendingFinalizers();
Console.WriteLine(p[0]);
Console.WriteLine(p[1]);
Console.WriteLine(p[2]);
Console.WriteLine(p[3]);
}
Console.ReadKey();
}
unsafe public static int StrPtr(string a)
{
fixed (char* p = a)
{
return (int)p;
}
}
static private int Test()
{
string a = "SOME";
return StrPtr(a);
}
}
}
在上面的代码中,main 调用了函数 Test
,该函数声明并初始化了一个名为 a
的字符串,然后使用上面展示的 StrPtr
函数获取其指针。下一步是调用垃圾回收器来回收那个刚刚超出作用域的字符串(实际上这并不会发生)。再下一步是读取指针所指向的区域,输出如下:
S O M E
这意味着字符串仍然可用。好的,这看起来没问题。下一步是用一个更长的字符串重复这个实验(它是上面代码的一个副本),输出是:
u s i n
顺便说一下,如果你好奇的话,这就是 StrPtr
在 MSIL 中的样子:
.method public hidebysig static int32 StrPtr(string a) cil managed
{
// Code size 21 (0x15)
.maxstack 2
.locals init ([0] char* p,
[1] int32 CS$1$0000,
[2] string pinned CS$519$0001)
IL_0000: ldarg.0
IL_0001: stloc.2
IL_0002: ldloc.2
IL_0003: conv.i
IL_0004: dup
IL_0005: brfalse.s IL_000d
IL_0007: call int32 [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::get_OffsetToStringData()
IL_000c: add
IL_000d: stloc.0
IL_000e: ldloc.0
IL_000f: conv.i4
IL_0010: stloc.1
IL_0011: leave.s IL_0013
IL_0013: ldloc.1
IL_0014: ret
} // end of method Program::StrPtr
你注意到那个调用和那些局部变量了吗?我唯一想说的是,如果你以为用 fixed 来获取字符串数据而不是 ToCharArray() 就绕过了 .NET 框架函数,那你就错了。另外请注意,这个实现将来可能会改变。
请参阅 MSDN 中的 System.Runtime.CompilerServices.RuntimeHelpers.OffsetToStringData() 。
这是因为字符串被驻留了,这会产生一些后果。考虑以下代码:
using System;
namespace Test
{
class Program
{
static void Main(string[] args)
{
unsafe
{
char* p = (char*)Test();
string b = "SOME";
p[0] = 'N';
p[1] = 'U';
p[2] = 'L';
p[3] = 'L';
Console.WriteLine(b);
}
Console.ReadKey();
}
unsafe public static int StrPtr(string a)
{
fixed (char* p = a)
{
return (int)p;
}
}
static private int Test()
{
string a = "SOME";
return StrPtr(a);
}
}
}
你认为输出会是什么?如果你说是 "SOME",那你就错了,因为字符串是不可变的,.NET 会自由地在所有相等的字符串字面量之间共享同一个指针。所以输出是 "NULL"。为了避免这种情况,请使用 System.Text 命名空间中的 StringBuilder 类,它将防止你的字符串被驻留。
2) 如何实现 VarPtr(及深入探讨)
这实际上是一个有文档记录的功能。你可以使用 &
来获取指针,就像在 C++ 中一样(至少在语法上是这样),只需要记住要在一个 unsafe
块中,并且开启了 /unsafe
选项。但是你能编写一个 VarPtr
函数吗?
在开始编写这个函数之前,请允许我重申,我们正在做一件危险的事情,我不想因为你忘记了这一点而被责备。
为了创建这个函数,请记住你需要使用引用传递参数(by-reference),因为如果你使用值传递(by-value),整个想法就毫无意义了。说了这些,问题就很简单了:如果你编写一个函数,接收一个值来获取它的地址,你会把参数声明成什么类型?
- 如果你说是 object,你可能会认为需要处理装箱-拆箱,但实际上编译器只会说:“无法转换为 ref object”。
- 如果你说是与值相同的类型,那么你将需要为每一种可能的数据类型编写一个函数。那不是我们的本意。
- 如果你说是使用泛型类型,那么你就抓到重点了,现在你知道如何创建一个泛型指针吗?
很长一段时间里,我认为这是不可能的,原因是下面的代码会产生两个 CS0208 错误:“不能获取托管类型('type')的地址、大小,或声明指向它的指针”。
unsafe public static int VarPtr(T a)
{
fixed (T* p = &a)
{
return (int)p;
}
}
当然,在尝试之后,我到网上寻找 VarPtr
的替代方案,并找到了一个解决方案:
static int Ptr(object o)
{
System.Runtime.InteropServices.GCHandle GC =
System.Runtime.InteropServices.GCHandle.Alloc(o,
System.Runtime.InteropServices.GCHandleType.Pinned);
int ret = GC.AddrOfPinnedObject().ToInt32();
return ret;
}
我需要测试它,所以我设计了以下场景:
int a = 9;
unsafe
{
int b = (int)&a;
int c = VarPtr(a);
Console.WriteLine(b);
Console.WriteLine(c);
int* p = (int*)b;
int* q = (int*)c;
*p = 1;
*q = 2;
}
Console.WriteLine(a);
这是我得到的结果之一(前两个数字在每次测试中都会改变,因为它们是内存地址):
1242220 20191136 1
结尾的数字 1 告诉我,第二个指针是错误的。
你可能认为故事到此结束,但这仅仅是个开始。VarPtr
的解决方案以 MSIL 的形式出现,这是编写它的方法:
.method public hidebysig static !!T* AddressOf<T>(!!T& var) cil managed
{
.maxstack 1
ldarg.0
conv.i
ret
}
它包含在一个我称之为“GenericPointers”的库中,位于一个名为“PointerBuilder”的类里。MSIL 源代码已附在本篇文章中。
现在,使用这个从 MSIL 编译的库,我需要更改:
int c = Ptr(a);
用
int c = (int)PointerBuilder.AddressOf(ref a)
结果如下:
70642928 70642928 2
所以这就是我们的 VarPtr
,但在创建它的过程中,我们发现微软的 CLR 实现支持泛型指针……我们还能用它做什么呢?
关于风险。你将面临与任何指针相同的风险,所以壮观的崩溃取决于你。
3) 如何实现 ArrayPtr?
在 VB 6.0 中有一个问题,那就是数组并不会在 VarPtr
获取的指针所指向的位置分配其元素……所以人们设法用一个他们称为 ArrayPtr
的“技巧”来获取那个点的地址。在 .NET 中,数组也存在同样的情况。我用 MSIL 编写了一个解决方案,但我没有在这里包含它,因为经过测试我发现,我在第 2 节中展示的 Ptr 过程可以给出任何类型的字符串和数组的地址。
这是新消息,数组并未通过所有测试,所以如果你要访问一个数组,请确保也保留一份该数组的副本,因为 GC 可能会回收这个数组。
4) 如何实现 "StuctPtr"?
有很多人问过如何获取一个在 C# 中声明的结构体的指针,并逐字节地处理它,我也解决了这个问题。MSIL 中的实现如下:
.method public hidebysig static !!T* Convert<T>(native int ptr) cil managed
{
.maxstack 1
ldarga.s ptr
call instance void* [mscorlib]System.IntPtr::ToPointer()
ret
}
这个例程将允许我们将一个指针转换为一个泛型指针,利用这个能力,我们将一个指向字节数组的指针转换为指向我们想要的结构体的指针,然后将我们的结构体赋值给它。在 C# 中利用这个功能的示例代码如下:
public unsafe byte[] ToByteArray<T>(T a) where T : struct
{
int length = PointerBuilder.SizeOf<T>();
byte[] buffer = new byte[length];
fixed (byte* p = &buffer[0])
*PointerBuilder.Convert<T>(new IntPtr((void*)p)) = a;
return buffer;
}
这段代码将允许你将结构体的副本作为一个字节数组来处理。当然,你还需要将该数组转换回结构体,为此,下面的代码会很有用:
public static unsafe T ToStruct<T>(byte[] a) where T : struct
{
fixed (byte* p = &a[0])
return *(PointerBuilder.Convert<T>(new IntPtr((void*)p)));
}
好的,现在要小心传递一个长度小于结构体大小的字节数组。如果你需要一个泛型类型的 sizeof,我随文章附带的 MSIL 代码中有一个。
你可能需要泛型 sizeof 的原因在于,如果你对一个泛型类型使用 sizeof,它会报错 CS0233:“‘标识符’没有预定义的大小,因此 sizeof 只能在不安全的上下文中使用(考虑使用 System.Runtime.InteropServices.Marshal.SizeOf)”。我已经向微软反映了这个问题,未来 sizeof 有可能会支持带 struct 约束的泛型。
关于移除 struct 约束,对引用类型使用这个方法取决于你,风险自负。我个人对这种情况不感兴趣,因为你用这种方法实现的任何东西可能在微软或其他第三方的 CLR 实现中无法工作。
我想指出,我已经对垃圾回收进行了测试,没有发现任何问题,看起来 GC 足够聪明,知道在给定的内存空间中分配了不止一个东西,就像在 C# 版本的联合(union)中发生的那样。我在这里贴出来,以防你忘记了它:
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Explicit)]
public struct union
{
[FieldOffset(0)]
public int word;
[FieldOffset(0)]
public short lowrd;
[FieldOffset(1)]
public short hiword;
}
说到联合(Union),如果你不需要泛型类型,还有另一种方法可以采用:
using System;
using System.Runtime.InteropServices;
namespace Test
{
[StructLayout(LayoutKind.Explicit)]
public unsafe struct tester
{
[FieldOffset(0)]
public fixed byte data[16];
[FieldOffset(0)]
public Decimal structure;
}
class Program
{
static void Main(string[] args)
{
tester X = new tester();
X.structure = -1234567890.01234567890123456789M;
unsafe
{
for (int I = 0; I < 16; I++)
{
Console.WriteLine(X.data[I]);
}
}
Console.ReadKey();
}
}
}
上面的代码展示了如何使用不安全代码和联合来获取给定结构体的字节。这段代码在泛型版本中无法工作,因为它需要 data
字段具有可变大小,而这会响应一个错误 CS0133:“赋给‘变量’的表达式必须是常量”。(而且你还需要那个 sizeof)。
5) 如何实现数组到数组的复制?
我们都知道可以使用 CopyTo 将信息复制到另一个相同类型的数组中,但是如果我们需要将二进制数据复制到另一个类型的数组中该怎么办?(我不是指转换元素)。到目前为止,我们已经看到了如何处理一些奇怪的指针,现在我将展示一个能使内存复制更快的函数示例(无需用指针遍历整个数组)。代码如下:
public static unsafe T[] CopyToArray<T>(this byte[] a, int telements) where T : struct
{
T[] R = new T[telements];
int Z = PointerBuilder.SizeOf<T>() * telements;
var p = PointerBuilder.AddressOf<T>(R, 0);
System.Runtime.InteropServices.Marshal.Copy(a, 0, new IntPtr((void*)p), Z);
return R;
}
public static unsafe byte[] CopyToByteArray<T>(this T[] a) where T : struct
{
int length = a.Length * PointerBuilder.SizeOf<T>();
byte[] buffer = new byte[length];
var p = PointerBuilder.AddressOf<T>(a, 0);
System.Runtime.InteropServices.Marshal.Copy(new IntPtr((void*)p), buffer, 0, length);
return buffer;
}
这段代码的明星是 Marshal.Copy,它会完成我们在 VB 6.0 的那些“黑科技”时代用 RtlMoveMemory (CopyMemory) 做的工作。如果你正在从事图形方面的工作,Marshal.Copy 和 LockBits 是一个很好的组合。
这段代码被编写为扩展方法,如果你是为 .NET 2.0 构建的,添加以下代码以便它能 без изменений 运行(再次提醒,以防你忘了):
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Method)]
public sealed class ExtensionAttribute : Attribute
{
public ExtensionAttribute() { }
}
}
6) 如何实现泛型算术?
如果你写过向量类型、列表类型或矩阵类型,你可能遇到过泛型算术的问题。首先,什么是泛型算术?泛型算术是一个术语,指的是你需要对数值进行加、乘、除、减、取反、移位或求余数等算术运算,而不用关心它们是 int、short、long 还是其他任何数值类型的功能。
好了,既然如此,你什么时候需要它呢?它在一些罕见但有用的情况下是必需的,以至于我们已经习惯于认为它们是不可能的。例如,考虑编写一个函数来计算一个泛型枚举器中数字的平均值,你希望为每种数值类型都写一遍吗?当然不,你希望有一个单一的泛型函数来处理这个问题,这就是我所拥有解决方案的场景(除了 decimal 和任何其他非基本数值类型)。
这个想法很简单,既然我们已经看到了如何使用 MSIL 来处理泛型,如果我告诉它将两个泛型类型的值相加会发生什么呢?例如:
.method public hidebysig static !!T Add<T>(!!T a, !!T b) cil managed
{
.maxstack 2
ldarg.0
ldarg.1
add
ret
}
上面的代码使用 add 来将值 a 和 b 相加,请注意 add 不检查溢出,要获得检查溢出的版本,请将 add 替换为 add.ovf。其他操作也遵循相同的原则。
猜猜怎么着?它能工作!但是非常、非常危险,如果你碰巧传递了对象、字符串,甚至 decimal,它最终会导致 AccessViolationException 或更壮观的 FatalExecutionEngineError,这是一个致命错误,换句话说,无论你有多少 try-catch 语句,它都会使你的代码崩溃。
好的,为了避免这种危险,请在这段代码周围包含以下测试:
if (typeof(decimal).IsPrimitive)
{
/*...*/
}
我本可以将其包含在 MSIL 中,但反复进行这种检查可能会导致性能瓶颈。
再次为好奇的读者们展示,这是用 C# 实现的相同功能(它会产生一个 CS0019 错误:“运算符'operator'不能应用于'type'和'type'类型的操作数”):
public static T Ptr<T>(T a, T b)
{
return a + b;
}
为了结束泛型算术这个话题,我想补充一点,还有另一种方法可以实现泛型算术。它更慢但更安全,并且需要 LINQ。下面的代码是这种方法的一个工作示例:
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
namespace Test
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(add(12, 15));
Console.ReadKey();
}
private static T add(T a, T b)
{
Type t = typeof(T);
ParameterExpression _a = Expression.Parameter(t, "a");
ParameterExpression _b = Expression.Parameter(t, "b");
var R = Expression.Lambda>(
Expression.Add(
_a, _b
)
, _a, _b).Compile();
return R(a, b);
}
}
}
免责声明
可能有人会问,为什么我说微软不希望我们知道这些?嗯,情况是这样的,有人曾请求微软将这些功能添加到 .NET 或 Visual Studio 中,而微软的回应是“won't fix - by design”(不修复 - 设计使然),也许他们自己都不知道他们做了这些,但如果他们知道,他们也不希望我们知道。让我们试着想想为什么?如果他们支持一个“numeric”泛型约束,那么进行泛型算术实际上并不会不安全,关于这一点我想引用以下内容:
"
布鲁斯·埃克尔(Bruce Eckel):我能否创建一个模板函数,换句话说,一个参数是未知类型的函数?你们正在为容器添加更强的类型检查,但我能否也得到一种像 C++ 模板那样的弱类型呢?例如,我能否编写一个函数,它接受参数 A a
和 B b
,然后在代码中说 a + b
?我能说我不在乎 A
和 B
是什么,只要它们有 operator+
就行,因为这是一种弱类型。
安德斯·海尔斯伯格(Anders Hejlsberg):你真正想问的是,在约束方面你能做什么?约束,就像任何其他特性一样,如果推到极致,可能会变得任意复杂。当你思考它时,约束是一种模式匹配机制。你希望能够说,“这个类型参数必须有一个接受两个参数的构造函数,实现 operator+
,有这个静态方法,有这两个实例方法,等等。”问题是,你希望这个模式匹配机制有多复杂?
从无到宏大的模式匹配之间有一个完整的连续统一体。我们认为什么都不说是太少了,而宏大的模式匹配又变得非常复杂,所以我们介于两者之间。我们允许你指定一个约束,可以是一个类,零个或多个接口,以及一个所谓的构造函数约束。例如,你可以说,“这个类型必须实现 IFoo
和 IBar
”或者“这个类型必须继承自基类 X
”。一旦你这样做了,我们会在编译时和运行时随处进行类型检查,确保约束为真。由该约束所隐含的任何方法都可以在该类型参数类型的值上直接使用。
现在,在 C# 中,运算符是静态成员。所以,一个运算符永远不能是一个接口的成员,因此一个接口约束永远不能赋予你一个 operator+
。你唯一能赋予自己一个Number
,并且 Number
有一个接受两个 Number
的 operator+
。但是你不能抽象地说,“必须有一个 operator+
”,然后我们多态地解析那意味着什么。
"
抱歉引用了这么长一段话,但这里的上下文很重要,从抽象层面来说,这意味着我们没有对数值约束的支持,因为这种特定约束涉及的复杂性。现在想想我在这里展示的代码……它做到了!但它有很大的风险,我担心我可能还没有发现所有的风险。我不是在说:“去破解 .NET 吧”,我是想对微软说,请以安全的方式支持这个功能,并对开发者说,请谨慎使用这个功能。我想向微软展示,我们可能需要在语言和 CLR 中拥有这些特性。我之所以不说去破解 .NET 的另一个原因是,我喜欢让代码能够在第三方实现上运行的想法,如果我们想要这样,那么黑客技术就毫无意义了。
在那之后,可能又会产生一个问题,那就是这一切有什么用?我无法具体说明我们可能需要它的那种工作(记住我说过这不是为了封送处理)。我可能会首先将它用于我们从流中读取东西的所有工作,其次是为实时应用程序进行图形或声音处理,由此衍生出的另一个用途是视频游戏。好吧,关于这个问题你可能会告诉我两件事:
1) 它不安全
就像任何其他做事方式一样,有好的实践和需要避免的错误。我建议只在组件内部使用这些技术,并且测试,测试,再测试。
2) 有其他方法可以做到
我知道,“不要重复发明轮子”,但请记住,有时候了解轮子是如何制造的是有好处的,而且那个轮子是人造的,所以它并不完美,还有改进的空间。所以我有两件事要说,首先我不会仅仅因为我想处理一些危险的事情就转到 C++ 去面对一个更不安全的场景,这远非我的目标,我希望在任何可能的地方保持安全性,同时获得足够的速度和能力来满足我的需求。其次,即使我知道有第三方产品可以解决我的问题,但也是某个开发者处理了那些,为你做了那些脏活累活,让你能保持双手干净。如果我告诉你我正朝着成为那样的人努力呢?或者如果你第一次需要做一些脏活,而没有第三方产品呢?所以至少考虑一下。
最后,如果你仍然认为这毫无帮助,请记住,是多亏了我你才知道这是可能的,所以你可以把这篇文章当作一个奇闻展览,然后省去给我发消息说:“这有什么用?”。
法律声明
本文根据知识共享署名-相同方式共享(Creative Commons Attribution-Share Alike)许可发布。由于该许可规定:“如果您获得版权持有人的许可,可以放弃上述任何条件。”,我将允许您放弃“相同方式共享”部分,从而解除该许可的病毒式传播性质。我只关心那些不创建使用此代码的商业衍生产品的人,希望他们能在“特别鸣谢”中包含我的名字。