【问题标题】:Should .NET strings really be considered immutable?.NET 字符串真的应该被认为是不可变的吗?
【发布时间】:2015-08-10 23:17:55
【问题描述】:

考虑以下代码:

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;相反,我通过修改其状态或 mutating 字符串来更改其值。因此,.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
}

如果您在速度关键领域(例如编码)工作,这可能会节省巨大的内存分配/性能成本。

我猜你可能会说这不算数,因为它“使用 hack”来使指针可变,但同样是 C# 语言设计者首先支持将字符串分配给指针。 (事实上​​,这是在 all the time 内部在 StringStringBuilder 中完成的,所以从技术上讲,你可以用它制作自己的 StringBuilder。)

那么,.NET 字符串真的应该被认为是不可变的吗?

【问题讨论】:

  • 在使用公共 API 时它们是不可变的。如果您使用不安全的代码或反射来绕过该公共 API,则它们不是。
  • @MarcinJuraszek 指针公共 API 的一部分,另见我的最后一段。
  • 我说的是 string 类的公共 API - 它公开的方法、属性。
  • 致那些投反对票的人 - 请将鼠标悬停在上/下按钮上,并确保您投反对票的理由正确。不要仅仅因为您不同意或不推荐这种方法而投反对票。
  • 它确实显示了研究成果,措辞清晰,并且包含有用的知识。这才是最重要的。

标签: c# .net string immutability


【解决方案1】:

C# 语言规范的第 18.6 节(fixed 语句)专门解决了通过固定指针修改字符串的情况,并指出这样做可能会导致未定义的行为: p>

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

【讨论】:

  • 有趣的是,我只听说过 C/C++ 规范中使用的术语“未定义行为”(一直)。在 C# 中看到它是新事物。
  • @JamesKo 在 C# 规范中甚至有一个与unsafe 代码无关的未定义行为实例(我可以在快速搜索中找到的唯一一个):如果您使用自定义等待者async/await,并且您的自定义等待者多次调用延续,行为未定义。
  • 是为什么要否决这个问题...答案已明确包含在语言规范中,并且作者显然认为这很可能是一个值得关注的问题,他们提前考虑好了
  • @JohnCastleman 我的道歉;我只是想提高人们对字符串在 .NET 中并不是真正“不可变”的认识,以表明它们与幕后的 char[] 缓冲区并没有什么不同。
【解决方案2】:

我只需要玩这个并进行实验以确认 string literal 的地址是否指向相同的内存位置。

结果是:

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 确实相等,而foofooCopy 的参考不同。

看看字符串字面量如何在内存中被操纵并影响其他引用它的变量是很有趣的。存在这种行为时我们应该注意的一件事。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-03-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多