.NET字符串真的应该被视为不可变吗?

9

考虑以下代码:

unsafe
{
    string foo = string.Copy("This can't change");

    fixed (char* ptr = foo)
    {
        char* pFoo = ptr;
        pFoo[8] = pFoo[9] = ' ';
    }

    Console.WriteLine(foo); // "This can   change"
}

这将创建一个指向foo的第一个字符的指针,将其重新分配为可变的,并将8和9位置上的字符更改为' '

请注意,我实际上从未重新分配foo;相反,我通过修改其状态或突变字符串的值来改变它的值。因此,.NET字符串是可变的。

事实上,以下代码可以正常工作:

unsafe
{
    string bar = "Watch this";

    fixed (char* p = bar)
    {
        char* pBar = p;
        pBar[0] = 'C';
    }

    string baz = "Watch this";
    Console.WriteLine(baz); // Unrelated, right?
}

这将会输出"Catch this",因为字符串字面量会被共享。

这有很多适用场景,例如:

string GetForInputData(byte[] inputData)
{
    // allocate a mutable buffer...
    char[] buffer = new char[inputData.Length];

    // fill the buffer with input data

    // ...and a string to return
    return new string(buffer);
}

被替换为:

string GetForInputData(byte[] inputData)
{
    // allocate a string to return
    string result = new string('\0', inputData.Length);

    fixed (char* ptr = result)
    {
        // fill the result with input data
    }

    return result; // return it
}

这可以在速度关键的领域(例如编码)中节省潜在的大量内存分配/性能成本。
我想你可以说这不算,因为它“使用了一个技巧”来使指针可变,但反过来说,正是 C# 语言设计者支持首先将字符串赋值给指针。(实际上,这在 String 和 StringBuilder 内部 一直 做,因此从技术上讲,您可以使用此方法制作自己的 StringBuilder。)
那么,.NET 字符串真的应该被认为是不可变的吗?

1
我正在谈论 string 类的公共 API - 它所暴露的方法和属性。 - MarcinJuraszek
@MarcinJuraszek,@Daniel.A.White:那么为什么Microsoft不让fixed返回例如ReadOnlyPtr<char>的字符串,这样就无法强制转换为可变的呢? - James Ko
2
对于那些给负评的人,请将鼠标悬停在上下按钮上,确保你是出于正确的原因进行负评。不要仅仅因为你不同意或不推荐这种方法而进行负评。 - Matt Johnson-Pint
4
这个内容表明了研究的努力,表述清晰,拥有实用知识。这才是最重要的。 - Matt Johnson-Pint
@JohnCastleman 感谢提供链接!抱歉,我之所以问这个问题是因为我不知道人们已经知道了这个。搜索“.net字符串不可变性”基本上毫无结果,因为你只会得到所有这些关于什么是不可变性/为什么.NET字符串是“不可变”的问题,所以我只是想增加一下大家的意识。 - James Ko
显示剩余12条评论
2个回答

6

《C# 语言规范》的第 18.6 节(fixed 语句)特别针对通过固定指针修改字符串的情况进行了说明,并指出这样做可能会导致未定义的行为:

通过固定指针修改托管类型的对象可能会导致未定义的行为。例如,由于字符串是不可变的,因此程序员有责任确保由指向固定字符串的指针引用的字符不被修改。


有意思,我只听说过 C/C++ 规范(一直)使用“未定义行为”这个术语。在 C# 中看到它是新鲜事。 - James Ko
1
@JamesKo 在C#规范中甚至有一个未定义行为的实例与unsafe代码无关(我在快速搜索中找到的唯一一个):如果您使用自定义等待者与async/await,并且您的自定义等待者多次调用继续操作,则行为是未定义的。 - user743382
这就是为什么要对这个问题进行投反对票的原因...答案在语言规范中明确涵盖,并且显然被作者认为是一个非常值得关注的问题,他们提前考虑了这个问题。 - John Castleman
@JohnCastleman 我很抱歉;我只是想增加人们对于 .NET 中字符串并不是真正“不可变”的意识,以展示它们在底层上与 char[] 缓冲区没有什么不同。 - James Ko

1
我只是不得不尝试并进行实验,以确认“字符串字面量”的地址是否指向相同的内存位置。
结果如下:
string foo = "Fix value?"; //New address: 0x02b215f8
string foo2 = "Fix value?"; //Points to same address: 0x02b215f8
string fooCopy = string.Copy(foo); //New address: 0x021b2888

fixed (char* p = foo)
{
    p[9] = '!';
}

Console.WriteLine(foo);
Console.WriteLine(foo2);
Console.WriteLine(fooCopy);

//Reference is equal, which means refering to same memory address
Console.WriteLine(string.ReferenceEquals(foo, foo2)); //true

//Reference is not equal, which creates another string in new memory address
Console.WriteLine(string.ReferenceEquals(foo, fooCopy)); //false

我们可以看到,foo 初始化一个字符串字面量,它指向我的PC上的0x02b215f8内存地址。将同一个字符串字面量分配给foo2会引用同一内存地址。而创建该相同字符串字面量的副本则会生成一个新的字符串。通过string.ReferenceEquals() 的进一步测试发现,foofoo2 是相等的,但是与 fooCopy 不同。

有趣的是,我们可以看到如何在内存中操作字符串字面量并影响其它变量只是对其进行引用。这种行为存在时,我们应该非常小心。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接