.NET中字符串是如何传递的?

149

当我将一个string传递给函数时,是传递指向字符串内容的指针,还是像传递struct一样将整个字符串作为堆栈参数传递给函数?

3个回答

340

传递了一个引用,但从技术上讲它并不是按引用传递。这是一个微妙但非常重要的区别。请考虑以下代码:

void DoSomething(string strLocal)
{
    strLocal = "local";
}
void Main()
{
    string strMain = "main";
    DoSomething(strMain);
    Console.WriteLine(strMain); // What gets printed?
}

要理解这里发生的事情,你需要知道三件事情:

  1. C#中字符串是引用类型。
  2. 它们是不可变的,所以每当你执行看起来像是在更改字符串的操作时,实际上是创建了一个全新的字符串,并将引用指向它,而原来的字符串则被丢弃。
  3. 尽管字符串是引用类型,但strMain并没有通过引用传递。它是一个引用类型,但是引用本身是按值传递的。只要你不使用ref关键字(不包括out参数)传递参数,就是按值传递。

那么这意味着什么呢?你正在......按值传递引用。由于它是引用类型,因此只有引用被复制到堆栈上。但这是什么意思呢?

按值传递引用类型:你已经在这样做了

C#变量要么是引用类型,要么是值类型。C#参数要么是按引用传递,要么是按值传递。术语在这里是个问题;它们听起来像是相同的东西,但实际上不是。

如果你传递了任何类型的参数,并且没有使用ref关键字,则你已经按值传递了。如果你按值传递,那么你真正传递的是一个副本。但如果参数是引用类型,那么你复制的东西是引用,而不是它所指向的任何内容。

下面是Main方法的第一行:

string strMain = "main";

这行代码创建了两个东西:一个值为 main 的字符串,在内存中存储起来,以及一个名为 strMain 的引用变量,指向该字符串。

DoSomething(strMain);

现在我们将该引用传递给 DoSomething 。 我们通过值传递它,这意味着我们制作了一份副本。 它是一个引用类型,这意味着我们复制了引用,而不是字符串本身。 现在我们有两个引用,它们各自指向内存中相同的值。

调用方内部

这是 DoSomething 方法的顶部:

void DoSomething(string strLocal)

没有ref关键字,所以strLocalstrMain是指向同一个值的两个不同引用。如果我们重新分配strLocal...

strLocal = "local";   

......我们没有改变存储的值;我们取出名为strLocal的引用并将其指向全新的字符串。这样做会对strMain产生什么影响? 没有影响。它仍然指向旧的字符串。

string strMain = "main";    // Store a string, create a reference to it
DoSomething(strMain);       // Reference gets copied, copy gets re-pointed
Console.WriteLine(strMain); // The original string is still "main" 

不可变性

暂停一下,让我们改变一下情景。想象一下,我们不是在处理字符串,而是一些可变的引用类型,比如你创建的类。

class MutableThing
{
    public int ChangeMe { get; set; }
}
如果您跟随引用objLocal指向的对象,您可以更改它的属性:follow
void DoSomething(MutableThing objLocal)
{
     objLocal.ChangeMe = 0;
} 

内存中仍然只有一个MutableThing,复制的引用和原始引用仍然指向它。MutableThing本身的属性已经改变

void Main()
{
    var objMain = new MutableThing();
    objMain.ChangeMe = 5; 
    Console.WriteLine(objMain.ChangeMe); // it's 5 on objMain

    DoSomething(objMain);                // now it's 0 on objLocal
    Console.WriteLine(objMain.ChangeMe); // it's also 0 on objMain   
}

啊,但是字符串是不可变的!没有ChangeMe属性可以设置。你不能像使用C风格的char数组那样在C#中执行strLocal[3] = 'H'; 你必须构造一个全新的字符串。改变strLocal唯一的方法是将引用指向另一个字符串,这意味着您对strLocal所做的任何操作都无法影响strMain。值是不可变的,而引用是副本。

通过引用传递引用

为了证明有区别,下面是当你通过引用传递引用时会发生什么:

void DoSomethingByReference(ref string strLocal)
{
    strLocal = "local";
}
void Main()
{
    string strMain = "main";
    DoSomethingByReference(ref strMain);
    Console.WriteLine(strMain);          // Prints "local"
}

这一次,Main 中的字符串确实会被更改,因为你没有在栈上复制它,而是直接传递了引用。

所以,尽管字符串是引用类型,但是按值传递它们意味着调用方中的字符串不会受到调用方更改的影响。但由于它们是引用类型,当你想要传递它们时,你不必复制整个字符串在内存中。

更多资源:


3
抱歉,@TheLight在此处的说法是错误的:“引用类型默认情况下是按引用传递的。”默认情况下,所有参数都是按值传递的,但对于引用类型来说,这意味着传递引用的值。你把引用类型和引用参数混淆了,这是可以理解的,因为这之间有一个非常混淆的区别。请参见在此处按值传递引用类型的部分。您提供的链接文章是相当正确的,但实际上支持我的观点。 - Justin Morgan
1
@JustinMorgan 不是要挑起一个已经结束的评论串,但我认为TheLight的评论在C语言中是有道理的。在C语言中,数据只是一块内存。引用是指向该内存块的指针。如果你将整个内存块传递给函数,那就是“按值传递”。如果你传递指针,则称为“按引用传递”。在C#中,没有传递整个内存块的概念,因此他们重新定义了“按值传递”来表示传递指针。这似乎是错误的,但指针也只是一块内存而已!对我来说,术语相当随意。 - rliu
2
@JustinMorgan 我同意混合使用 C 和 C# 术语是不好的,但是,虽然我喜欢 Lippert 的帖子,但我不认为将引用视为指针会特别混淆任何内容。该博客文章描述了将引用视为指针会赋予它过多的权力。我知道 ref 关键字有用,我只是想解释为什么在 C# 中通过值传递引用类型似乎像是传统的(即 C)按引用传递的概念(而在 C# 中通过引用传递引用类型似乎更像是通过值传递引用的引用)。 - rliu
@roliu - 你说得对,它们有相似之处,当我们(像我们中的许多人一样)从C/C++转到C#时,很难避免进行比较。我认为除了术语的重要性外,我们在大多数事情上都达成了共识。为了避免混淆,我们可以谈论按共享调用(C#)与按引用调用(C)。实际上,这个问题是为什么重要的完美例子:使用按引用调用语义,被调用者的操作将更改原始字符串。 - Justin Morgan
3
你说得没错,但我认为@roliu提到的是这样一个函数 Foo(string bar) 可以被视为 Foo(char* bar),而 Foo(ref string bar) 则相当于 Foo(char** bar)(或者在C++中是 Foo(char*& bar)Foo(string& bar))。当然,这不是你每天都应该这样思考,但它实际上帮助我最终理解了底层发生的事情。 - Cole Tobin
显示剩余5条评论

28

C#中的字符串是不可变的引用对象。这意味着对它们的引用被传递(按值),并且一旦创建了一个字符串,就不能修改它。生成修改版本字符串(子字符串、修剪版本等)的方法会创建原始字符串的修改副本


14

字符串是一种特殊情况。每个实例都是不可变的。当您更改字符串的值时,会在内存中分配一个新的字符串。

因此,只有引用被传递到函数中,但是当字符串被编辑时,它变成了一个新实例,并且不会修改旧实例。


4
在这方面,字符串并不是一个特例。很容易创建不可变对象,其语义可能与字符串相同(也就是说,一个类型的实例没有公开的方法来改变它...)。 - user166390
1
按照这个逻辑,那么Uri(类)和Guid(结构体)也是特例。我不明白为什么System.String表现得比其他不可变类型更像“值类型”...无论是类还是结构体。 - user166390
4
UriGuid不同,字符串具有特殊的创建语义。你可以将字符串字面值直接赋值给一个字符串变量。字符串看起来是可变的,就像重新分配一个int一样,但它会隐式地创建一个对象,不需要使用new关键字。 - Enigmativity
3
字符串是一个特殊情况,但它与这个问题没有关联。无论是值类型、引用类型还是其他类型,在这个问题中都会表现相同。 - Kirk Broadhurst
唯一使字符串成为特例的是C#支持将它们写成字面量,正如@KirkBroadhurst所指出的那样,这并不相关。其他所有内容,包括它们类似于“值类型”的行为(我假设你指的是像==按值比较它们之类的事情),在用户定义的类型中可以很容易地复制。我不会描述它们的行为像值类型。 - Justin Morgan
显示剩余2条评论

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