你能通过不安全的方法改变(不可变)字符串的内容吗?

我知道字符串是不可变的,对字符串的任何更改只会在内存中创建一个新字符串(并将旧字符串标记为空闲字符串)。 但是,我想知道我的下面的逻辑是否合理,因为你实际上可以以一种圆形的方式修改字符串的内容。

const string baseString = "The quick brown fox jumps over the lazy dog!"; //initialize a new string string candidateString = new string('\0', baseString.Length); //Pin the string GCHandle gcHandle = GCHandle.Alloc(candidateString, GCHandleType.Pinned); //Copy the contents of the base string to the candidate string unsafe { char* cCandidateString = (char*) gcHandle.AddrOfPinnedObject(); for (int i = 0; i < baseString.Length; i++) { cCandidateString[i] = baseString[i]; } } 

这种方法确实改变了内容candidateString (没有在内存中创建新的candidateString),还是运行时通过我的技巧看待并将其视为普通字符串?

由于有几个要素,您的示例工作得很好:

  • candidateString存在于托管堆中,因此可以安全地进行修改。 将其与实习的baseString进行比较。 如果您尝试修改实习字符串,可能会发生意外情况。 虽然它现在似乎有用,但不能保证字符串在某些时候不会存在于写保护的内存中。 这与将常量字符串分配给C中的char*变量然后修改它非常相似。 在C中,这是未定义的行为。

  • 您在candidateString预先分配了足够的空间 – 因此您不会溢出缓冲区。

  • 字符数据存储在String类的偏移量0处。 它存储在一个等于RuntimeHelpers.OffsetToStringData的偏移量中。

     public static int OffsetToStringData { // This offset is baked in by string indexer intrinsic, so there is no harm // in getting it baked in here as well. [System.Runtime.Versioning.NonVersionable] get { // Number of bytes from the address pointed to by a reference to // a String to the first 16-bit character in the String. Skip // over the MethodTable pointer, & String // length. Of course, the String reference points to the memory // after the sync block, so don't count that. // This property allows C#'s fixed statement to work on Strings. // On 64 bit platforms, this should be 12 (8+4) and on 32 bit 8 (4+4). #if WIN32 return 8; #else return 12; #endif // WIN32 } } 

    除了…

  • GCHandle.AddrOfPinnedObject 特别适用于两种类型: string和数组类型。 它不是返回对象本身的地址,而是将偏移量返回给数据。 请参阅CoreCLR中的源代码 。

     // Get the address of a pinned object referenced by the supplied pinned // handle. This routine assumes the handle is pinned and does not check. FCIMPL1(LPVOID, MarshalNative::GCHandleInternalAddrOfPinnedObject, OBJECTHANDLE handle) { FCALL_CONTRACT; LPVOID p; OBJECTREF objRef = ObjectFromHandle(handle); if (objRef == NULL) { p = NULL; } else { // Get the interior pointer for the supported pinned types. if (objRef->GetMethodTable() == g_pStringClass) p = ((*(StringObject **)&objRef))->GetBuffer(); else if (objRef->GetMethodTable()->IsArray()) p = (*((ArrayBase**)&objRef))->GetDataPtr(); else p = objRef->GetData(); } return p; } FCIMPLEND 

总之,运行时允许您使用其数据并且不会抱怨。 毕竟你使用的是unsafe代码。 我发现运行时混乱比这更糟糕,包括在堆栈上创建引用类型;-)

如果您的最终字符串短于分配的字符串,请记住所有字符(偏移Length添加一个\0 。 这不会溢出,每个字符串在末尾都有一个隐式空字符,以简化互操作方案。


现在看看StringBuilder如何创建一个字符串,这里是StringBuilder.ToString

 [System.Security.SecuritySafeCritical] // auto-generated public override String ToString() { Contract.Ensures(Contract.Result() != null); VerifyClassInvariant(); if (Length == 0) return String.Empty; string ret = string.FastAllocateString(Length); StringBuilder chunk = this; unsafe { fixed (char* destinationPtr = ret) { do { if (chunk.m_ChunkLength > 0) { // Copy these into local variables so that they are stable even in the presence of race conditions char[] sourceArray = chunk.m_ChunkChars; int chunkOffset = chunk.m_ChunkOffset; int chunkLength = chunk.m_ChunkLength; // Check that we will not overrun our boundaries. if ((uint)(chunkLength + chunkOffset) <= ret.Length && (uint)chunkLength <= (uint)sourceArray.Length) { fixed (char* sourcePtr = sourceArray) string.wstrcpy(destinationPtr + chunkOffset, sourcePtr, chunkLength); } else { throw new ArgumentOutOfRangeException("chunkLength", Environment.GetResourceString("ArgumentOutOfRange_Index")); } } chunk = chunk.m_ChunkPrevious; } while (chunk != null); } } return ret; } 

是的,它使用不安全的代码,是的,你可以使用fixed来优化你的代码,因为这种类型的固定比分配GC句柄轻得多:

 const string baseString = "The quick brown fox jumps over the lazy dog!"; //initialize a new string string candidateString = new string('\0', baseString.Length); //Copy the contents of the base string to the candidate string unsafe { fixed (char* cCandidateString = candidateString) { for (int i = 0; i < baseString.Length; i++) cCandidateString[i] = baseString[i]; } } 

当您使用fixed ,GC只发现一个对象需要在收集过程中偶然发现它时被固定。 如果没有收集,GC甚至不参与。 使用GCHandle ,每次都会在GC中注册一个句柄。

正如其他人所指出的那样,在一些罕见的情况下,改变String对象很有用。 我举一个例子,下面是一个有用的代码片段。

用例/背景

虽然每个人都应该是.NET一直提供的非常出色的字符编码支持的忠实粉丝,但有时候最好还是减少开销,特别是如果在8位(传统)字符和托管字符串之间进行大量往返(即典型的互操作场景)。

正如我所暗示的那样, .NET特别强调你必须明确指定一个文本Encoding用于非Unicode字符数据到/来自托管String对象的任何/所有转换。 这种严格控制在外围是非常值得称道的,因为它确保一旦你在托管运行时内部有字符串,你就不必担心; 一切都只是宽泛的Unicode。 甚至UTF-8在这个原始境界中也被大量放逐。

(相比之下,回想一下其他一些流行的脚本语言,这个语言使这个领域变得很糟糕,最终导致了几年的并行2.x3.x版本,这都归功于后者的大量Unicode更改。)

因此.NET将所有混乱推送到互操作边界,一旦进入内部就强制执行Unicode(UTF-16),但这种理念要求完成编码/解码工作(“一劳永逸”)是彻底严格的因此,.NET编码/编码器类可能成为性能瓶颈。 如果您将大量文本从宽(Unicode)移动到简单的固定7或8位窄ANSI,ASCII等(请注意,我不是在谈论MBCS或UTF-8,您要使用它们编码器!),.NET编码范例似乎有点矫枉过正。

此外,可能是您不知道或不关心指定Encoding 。 也许你所关心的是对16位Char低字节快速准确的往返。 如果你看一下.NET源代码 ,即使System.Text.ASCIIEncoding在某些情况下也可能过于庞大。


代码片段……

细字符串:直接存储在托管字符串中的8位字符,每个宽Unicode字符一个“瘦字符”,在往返过程中不会打扰字符编码/解码。

所有这些方法都忽略/去除每个16位Unicode字符的高位字节,仅按原样传输每个低字节。 显然,只有在那些高位不相关的情况下,才能在往返之后成功恢复Unicode文本。

 ///  Convert byte array to "thin string"  public static unsafe String ToThinString(this byte[] src) { int c; var ret = String.Empty; if ((c = src.Length) > 0) fixed (char* dst = (ret = new String('\0', c))) do dst[--c] = (char)src[c]; // fill new String by in-situ mutation while (c > 0); return ret; } 

在刚刚显示的方向上,通常将本机数据带入托管,您通常没有托管字节数组,因此,为了调用此函数,您可以处理原始本机字节,而不是仅仅为了调用此函数而分配临时数据。直接进入托管字符串。 和以前一样,这会绕过所有字符编码。

为清楚起见,省略了这种不安全function所需的(明显的)范围检查:

 public static unsafe String ToThinString(byte* pSrc, int c) { var ret = String.Empty; if (c > 0) fixed (char* dst = (ret = new String('\0', c))) do dst[--c] = (char)pSrc[c]; // fill new String by in-situ mutation while (c > 0); return ret; } 

这里String变换的优点是可以通过直接写入最终分配来避免临时分配。 即使你通过使用stackalloc避免额外的分配,当你最终调用String(Char*, int, int)构造函数时,会有一个不必要的重复复制:显然你无法将数据关联起来使用String对象进行费力的准备,直到完成后才存在!


为了完整……

这是镜像代码,它反转操作以获取字节数组(即使这个方向没有用于说明字符串变异技术)。 这是您通常用于托管.NET运行时发送Unicode文本的方向,供旧版应用程序使用。

 ///  Convert "thin string" to byte array  public static unsafe byte[] ToByteArr(this String src) { int c; byte[] ret = null; if ((c = src.Length) > 0) fixed (byte* dst = (ret = new byte[c])) do dst[--c] = (byte)src[c]; while (c > 0); return ret ?? new byte[0]; }