当我将 a 传递string
给函数时,是传递了指向字符串内容的指针,还是像 astruct
那样将整个字符串传递给堆栈上的函数?
3 回答
一个引用被传递;但是,它在技术上不是通过引用传递的。这是一个微妙但非常重要的区别。考虑以下代码:
void DoSomething(string strLocal)
{
strLocal = "local";
}
void Main()
{
string strMain = "main";
DoSomething(strMain);
Console.WriteLine(strMain); // What gets printed?
}
要了解这里发生的情况,您需要知道三件事:
- 字符串是 C# 中的引用类型。
- 它们也是不可变的,所以任何时候你做一些看起来你正在改变字符串的事情,你不是。一个全新的字符串被创建,引用指向它,旧的被丢弃。
- 即使字符串是引用类型,
strMain
也不是通过引用传递的。它是一个引用类型,但引用本身是按值传递的。任何时候你传递一个没有ref
关键字的参数(不计算out
参数),你已经通过值传递了一些东西。
所以这一定意味着你正在......按值传递引用。由于它是引用类型,因此只有引用被复制到堆栈中。但是,这是什么意思?
按值传递引用类型:你已经在做
C# 变量是引用类型或值类型。C# 参数要么通过引用传递,要么通过值传递。术语在这里是个问题。这些听起来像是同一件事,但事实并非如此。
如果你传递了一个 ANY 类型的参数,并且你没有使用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
对它指向的对象的引用,则可以更改其属性:
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# 中,你不能strLocal[3] = 'H'
像使用 C 风格的char
数组那样做;您必须改为构建一个全新的字符串。更改的唯一方法strLocal
是将引用指向另一个字符串,这意味着您所做的任何事情都不会strLocal
影响strMain
. 值是不可变的,引用是副本。
通过引用传递引用
为了证明存在差异,当您通过引用传递引用时会发生以下情况:
void DoSomethingByReference(ref string strLocal)
{
strLocal = "local";
}
void Main()
{
string strMain = "main";
DoSomethingByReference(ref strMain);
Console.WriteLine(strMain); // Prints "local"
}
这一次,in 中的字符串Main
确实发生了变化,因为您传递了引用而没有将其复制到堆栈上。
因此,即使字符串是引用类型,通过值传递它们也意味着被调用者中发生的任何事情都不会影响调用者中的字符串。但是由于它们是引用类型,因此当您想要传递它时,您不必将整个字符串复制到内存中。
更多资源:
- 这是我读过的关于 C# 中引用类型和值类型之间区别的最佳文章,以及为什么引用类型与引用传递的参数不同。
- 像往常一样,Eric Lippert 也有几篇关于这个主题的优秀博客文章。
- 他在不变性方面也有一些很棒的东西。
C# 中的字符串是不可变的引用对象。这意味着对它们的引用是(按值)传递的,一旦创建了字符串,就不能修改它。生成字符串修改版本(子字符串、修剪版本等)的方法会创建原始字符串的修改副本。
字符串是特殊情况。每个实例都是不可变的。当您更改字符串的值时,您将在内存中分配一个新字符串。
所以只有引用被传递给你的函数,但是当字符串被编辑时,它变成了一个新实例并且不会修改旧实例。