我知道字符串是不可变的,对字符串的任何更改只会在内存中创建一个新字符串(并将旧字符串标记为空闲字符串).但是,我想知道我的下面的逻辑是否合理,你实际上可以以一种循环方式修改字符串的内容.
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];
}
}
Run Code Online (Sandbox Code Playgroud)
这种方法确实改变了内容candidateString(没有在内存中创建新的candidateString),还是运行时通过我的技巧看待它并将其视为普通字符串?
Luc*_*ski 11
由于有几个要素,您的示例工作得很好:
candidateString生活在托管堆中,因此可以安全地进行修改.将此与baseString实习相比较.如果您尝试修改实习字符串,可能会发生意外情况.虽然它现在似乎有用,但无法保证字符串在某些时候不会存在于写保护的内存中.这与将常量字符串分配给char*C中的变量然后修改它非常相似.在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
}
}
Run Code Online (Sandbox Code Playgroud)
除了...
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
Run Code Online (Sandbox Code Playgroud)总之,运行时允许您使用其数据并且不会抱怨.unsafe毕竟你正在使用代码.我发现运行时混乱比这更糟糕,包括在堆栈上创建引用类型;-)
如果您的最终字符串短于分配的字符串,请记住\0 在所有字符(偏移量Length)之后添加一个.这不会溢出,每个字符串在末尾都有一个隐式空字符,以简化互操作方案.
现在来看看如何StringBuilder创建一个字符串,这里是StringBuilder.ToString:
[System.Security.SecuritySafeCritical] // auto-generated
public override String ToString() {
Contract.Ensures(Contract.Result<String>() != 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;
}
Run Code Online (Sandbox Code Playgroud)
是的,它使用不安全的代码,是的,你可以用你的优化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];
}
}
Run Code Online (Sandbox Code Playgroud)
使用时fixed,GC只会在收集过程中发现一个物体在发现它时需要固定.如果没有收集,GC甚至不参与.使用时GCHandle,每次都会在GC中注册一个句柄.
正如其他人指出的那样,String在某些罕见的情况下,改变对象是有用的。我在下面给出了一个带有有用代码片段的示例。
用例/背景
尽管每个人都应该是 .NET 一直提供的非常出色的字符编码支持的忠实粉丝,但有时减少开销可能会更好,特别是在 8 位(旧)字符和托管字符串之间进行大量往返时(即通常的互操作场景)。
正如我所暗示的,特别强调的是,您必须为非 Unicode 字符数据与托管 String 对象之间的任何/所有转换显.NET式指定文本。Encoding这种对外围的严格控制确实值得称赞,因为它确保一旦您在托管运行时中拥有该字符串,您就永远不必担心;一切都只是 Unicode(技术上是UCS-2)。
相比之下,考虑一下其他某种流行的脚本语言,它在整个领域出了名的糟糕,导致了并行2.x和3.x版本的持续传奇,这一切都是由于后者增加了对 Unicode 的支持。
一旦你进入内部,通过强制执行 Unicode,.NET将所有混乱推到互操作边界,在那里它被“一劳永逸”地完成,但这种哲学意味着编码/解码工作是急切和详尽的,而不是“懒惰”以及更多在您的程序的控制下。因此,.NET 编码/编码器类可能成为性能瓶颈。如果您要将大量文本从宽文本 (Unicode) 移动到简单的固定 7 位或 8 位窄 ANSI、ASCII 等(请注意,我不是在谈论 MBCS 或 UTF-8,您需要在其中使用编码器!),.NET 编码范式可能看起来有点大材小用。
此外,您可能不知道或不关心指定Encoding. 也许您关心的只是 16 位 的低字节的快速且准确的往返Char。如果您查看 .NET 源代码,甚至System.Text.ASCIIEncoding在某些情况下也可能过于庞大。
代码片段...
细字符串:直接存储在托管String中的 8 位字符 ,每个宽 Unicode 字符一个“细字符”,无需在往返期间进行字符编码/解码。
所有这些方法都只是忽略/剥离每个 16 位字符的高字节Unicode,仅按原样传输每个低字节。显然,只有在那些高位不相关的情况下,才能在往返之后成功恢复 Unicode 文本。
/// <summary> Convert byte array to "thin string" </summary>
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;
}
Run Code Online (Sandbox Code Playgroud)
按照刚刚所示的方向,通常是将本机数据引入托管,您通常没有托管字节数组,因此您可以处理原始本机字节,而不是仅仅为了调用此函数而分配一个临时数组直接转换为托管字符串。和以前一样,这会绕过所有字符编码。
为了清楚起见,省略了此不安全函数中所需的(明显的)范围检查:
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;
}
Run Code Online (Sandbox Code Playgroud)
这里突变的优点String是您可以通过直接写入最终分配来避免临时分配。即使您要通过使用来避免额外的分配stackalloc,当您最终调用构造函数时,也会有不必要的重新复制整个事情String(Char*, int, int):显然没有办法将您刚刚辛苦准备的数据与String不存在的对象关联起来直到你完成!
为了完整性...
这是镜像代码,它反转操作以返回字节数组(即使这个方向恰好没有说明字符串突变技术)。这是您通常用于将 Unicode 文本发送出托管运行时的方向.NET,以供旧版应用程序使用。
/// <summary> Convert "thin string" to byte array </summary>
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];
}
Run Code Online (Sandbox Code Playgroud)