当我将一个string
传递给函数时,是传递指向字符串内容的指针,还是像传递struct
一样将整个字符串作为堆栈参数传递给函数?
当我将一个string
传递给函数时,是传递指向字符串内容的指针,还是像传递struct
一样将整个字符串作为堆栈参数传递给函数?
传递了一个引用,但从技术上讲它并不是按引用传递。这是一个微妙但非常重要的区别。请考虑以下代码:
void DoSomething(string strLocal)
{
strLocal = "local";
}
void Main()
{
string strMain = "main";
DoSomething(strMain);
Console.WriteLine(strMain); // What gets printed?
}
要理解这里发生的事情,你需要知道三件事情:
strMain
并没有通过引用传递。它是一个引用类型,但是引用本身是按值传递的。只要你不使用ref
关键字(不包括out
参数)传递参数,就是按值传递。那么这意味着什么呢?你正在......按值传递引用。由于它是引用类型,因此只有引用被复制到堆栈上。但这是什么意思呢?
C#变量要么是引用类型,要么是值类型。C#参数要么是按引用传递,要么是按值传递。术语在这里是个问题;它们听起来像是相同的东西,但实际上不是。
如果你传递了任何类型的参数,并且没有使用ref
关键字,则你已经按值传递了。如果你按值传递,那么你真正传递的是一个副本。但如果参数是引用类型,那么你复制的东西是引用,而不是它所指向的任何内容。
下面是Main
方法的第一行:
string strMain = "main";
这行代码创建了两个东西:一个值为 main
的字符串,在内存中存储起来,以及一个名为 strMain
的引用变量,指向该字符串。
DoSomething(strMain);
现在我们将该引用传递给 DoSomething
。 我们通过值传递它,这意味着我们制作了一份副本。 它是一个引用类型,这意味着我们复制了引用,而不是字符串本身。 现在我们有两个引用,它们各自指向内存中相同的值。
这是 DoSomething
方法的顶部:
void DoSomething(string strLocal)
没有ref
关键字,所以strLocal
和strMain
是指向同一个值的两个不同引用。如果我们重新分配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
中的字符串确实会被更改,因为你没有在栈上复制它,而是直接传递了引用。
所以,尽管字符串是引用类型,但是按值传递它们意味着调用方中的字符串不会受到调用方更改的影响。但由于它们是引用类型,当你想要传递它们时,你不必复制整个字符串在内存中。
C#中的字符串是不可变的引用对象。这意味着对它们的引用被传递(按值),并且一旦创建了一个字符串,就不能修改它。生成修改版本字符串(子字符串、修剪版本等)的方法会创建原始字符串的修改副本。
字符串是一种特殊情况。每个实例都是不可变的。当您更改字符串的值时,会在内存中分配一个新的字符串。
因此,只有引用被传递到函数中,但是当字符串被编辑时,它变成了一个新实例,并且不会修改旧实例。
Uri
(类)和Guid
(结构体)也是特例。我不明白为什么System.String
表现得比其他不可变类型更像“值类型”...无论是类还是结构体。 - user166390Uri
和Guid
不同,字符串具有特殊的创建语义。你可以将字符串字面值直接赋值给一个字符串变量。字符串看起来是可变的,就像重新分配一个int
一样,但它会隐式地创建一个对象,不需要使用new
关键字。 - Enigmativity==
按值比较它们之类的事情),在用户定义的类型中可以很容易地复制。我不会描述它们的行为像值类型。 - Justin Morgan
ref
关键字有用,我只是想解释为什么在 C# 中通过值传递引用类型似乎像是传统的(即 C)按引用传递的概念(而在 C# 中通过引用传递引用类型似乎更像是通过值传递引用的引用)。 - rliuFoo(string bar)
可以被视为Foo(char* bar)
,而Foo(ref string bar)
则相当于Foo(char** bar)
(或者在C++中是Foo(char*& bar)
或Foo(string& bar)
)。当然,这不是你每天都应该这样思考,但它实际上帮助我最终理解了底层发生的事情。 - Cole Tobin