.net中的字符串在内存中的存储方式-kb88凯时官网登录

来自:网络
时间:2024-06-10
阅读:

毫无疑问,字符串是我们使用频率最高的类型。但是如果我问大家一个问题:“一个字符串对象在内存中如何表示的?”,我相信绝大部分人回答不上来。我们今天就来讨论这个问题。

一、字符串对象的内存布局

从“值类型”和“引用类型”来划分,字符串自然属于引用类型的范畴,所以一个字符串对象自然采用引用类型的内存布局。我在很多文章中都介绍过引用类型实例的内存布局(《》 和《》,总的来说整个内存布局分三块:objheader typehandle payload。对于一般的引用类型实例来说,最后一部分存放的就是该实例所有字段的值,但是字符串有点特别,它有哪些字段呢?

说到这里,可能有人想去反编译一下string类型,看看它定义了那些字段。其实没有必要,字符串这个类型有点特别,它的payload部分由两部分组成:字符串长度(不是字节长度) 编码的文本,下图揭示了字符串对象的内存布局。那么具体采用怎样的编码方式呢?可能很多人会认为是utf-8,实在不然,它采用的是utf-16,大部分字符通过两个字节来表示,少数的则需要使用四个字节。至于字节序,自然是使用小端字节序。

.net中的字符串在内存中的存储方式

二、以二进制的方式创建一个string对象

在《》中,我们通过构建一个字节数组来表示创建的对象,现在我们依然可以采用类似的方式来创建一个真正的string对象。如下所示的asstring方法用来将用于承载字符串实例的字节数组转换成一个string对象,至于这个字节数组的构建,则有createstring方法完成。createstring方法根据指定的字符串内容创建一个string对象,并利用输出参数返回该对象映射在内存中的字节数组。

static unsafe string createstring(string value, out byte[] bytes)
{
    var bytecount = encoding.unicode.getbytecount(value);
    // objheader   typehandle   length   encoded string
    var size = sizeof(nint)   sizeof(nint)   sizeof(int)   bytecount;
    bytes = new byte[size];
    // typehandle
    binaryprimitives.writeint64littleendian(bytes.asspan(sizeof(nint)), typeof(string).typehandle.value.toint64());
    // length
    binaryprimitives.writeint32littleendian(bytes.asspan(sizeof(nint) * 2), value.length);
    // encoded string
    encoding.unicode.getbytes(value).copyto(bytes, 20);
    return asstring(bytes);
}
static unsafe string asstring(byte[] bytes)
{
    string s = null!;
    unsafe.write(unsafe.aspointer(ref s), new intptr(unsafe.aspointer(ref bytes[8])));
    return s;
}

由于我们需要创建一个字节数组来表示string对象,所以必须先计算出这个字节数组的长度。我们在上面说过,string类型采用utf-16/unicode编码方式,所以我们调用encoding.unicode的getbytecont方法可以计算出指定的字符串编码后的字节数。在此基础上我们还需要加上通过一个整数(sizeof(int))表示字符串长度和typehandle(sizeof(nint))和objheader(sizeof(nint),含padding),就是整个string实例在内存中占用的字节数。

接下来我们填充string类型的typehandle的值(string类型方法表地址)、字符串长度和编码后的字节,最终将填充好的字节数组作为参数调用asstring方法,返回的就是我们创建的string对象。createstring方法针字符串对象的创建可以通过如下的代码来验证。

var literal = "foobar";
string s = createstring(literal, out var bytes);
debug.assert(literal == s);

对于上面定义的asstring方法来说,作为输入参数的字节数组字符串实例的内存片段,所以该方法针对同一个数组返回的都是同一个实例,如下的演示代码证明了这一点。

var literal = "foobar";
createstring(literal, out var bytes);
var s1 = asstring(bytes);
var s2 = asstring(bytes);
debug.assert(referenceequals(s1,s2));

三、字符串的“可变性”

我们都知道字符串一经创建就不会改变,但是对于上面创建的字符串来说,由于我们都将承载字符串实例的内存字节都拿捏住了,那还不是想怎么改就怎么改。比如在如下所示的代码片段中,我们将同一个字符串的文本从“foo”改成了“bar”。

var literal = "foo";
var s = createstring(literal, out var bytes);
debug.assert(s == "foo");
encoding.unicode.getbytes("bar").copyto(bytes, 20);
debug.assert(s == "bar");
返回顶部
顶部
网站地图