C#字符串引用类型?


163

我知道C#中的“字符串”是引用类型。这是在MSDN上。但是,此代码无法正常运行,因此应:

class Test
{
    public static void Main()
    {
        string test = "before passing";
        Console.WriteLine(test);
        TestI(test);
        Console.WriteLine(test);
    }

    public static void TestI(string test)
    {
        test = "after passing";
    }
}

由于我将字符串作为参数传递并且是引用类型,因此输出应为“传递前”或“传递后”,第二个输出语句应识别出TestI方法中的文本已更改。但是,我得到“通过之前”“通过之前”,似乎它是通过值而不是通过引用传递的。我知道字符串是不可变的,但是我看不出它怎么解释这里发生的事情。我想念什么?谢谢。


请参阅下面的乔恩(Jon)推荐的文章。您提到的行为也可以由C ++指针重现。
Sesh

MSDN中也很好的解释。
Dimi_Pel 2011年

Answers:


211

对字符串的引用按值传递。按值传递引用和按引用传递对象之间有很大的区别。不幸的是,在两种情况下都使用“引用”一词。

如果你这样做把这个字符串参考基准,它会像您期望的工作:

using System;

class Test
{
    public static void Main()
    {
        string test = "before passing";
        Console.WriteLine(test);
        TestI(ref test);
        Console.WriteLine(test);
    }

    public static void TestI(ref string test)
    {
        test = "after passing";
    }
}

现在,您需要区分对引用所引用的对象进行更改,然后对变量(例如参数)进行更改以使其引用另一个对象。我们不能对字符串进行更改,因为字符串是不可变的,但是我们可以用a StringBuilder来演示它:

using System;
using System.Text;

class Test
{
    public static void Main()
    {
        StringBuilder test = new StringBuilder();
        Console.WriteLine(test);
        TestI(test);
        Console.WriteLine(test);
    }

    public static void TestI(StringBuilder test)
    {
        // Note that we're not changing the value
        // of the "test" parameter - we're changing
        // the data in the object it's referring to
        test.Append("changing");
    }
}

有关更多详细信息,请参见我的有关参数传递的文章


2
同意,只是想弄清楚使用ref修饰符也适用于非引用类型,即两者都是非常独立的概念。
eglasius

2
@Jon Skeet喜欢您文章中的旁注。您应该以referenced这个作为答案
Nithish Inpursuit Ofhappiness

36

如果我们必须回答这个问题:字符串是一个引用类型,它的行为就像一个引用。我们传递一个保存引用的参数,而不是实际的字符串。问题出在函数中:

public static void TestI(string test)
{
    test = "after passing";
}

该参数test保留对该字符串的引用,但它是一个副本。我们有两个指向字符串的变量。并且由于任何使用字符串的操作实际上都会创建一个新对象,因此我们将本地副本指向该新字符串。但是原始test变量没有改变。

建议放置ref在函数声明和调用工作中的解决方案,因为我们不会传递test变量的值,而只会传递对其的引用。因此,函数内部的任何更改都将反映原始变量。

我想在末尾重复:字符串是引用类型,但是由于它是不可变的,所以该行test = "after passing";实际上创建了一个新对象,并且我们的变量副本test更改为指向新字符串。


25

正如其他人所述,String.NET中的类型是不可变的,其引用是通过值传递的。

在原始代码中,此行执行后:

test = "after passing";

test不再引用原始对象。我们已经创建了一个 String对象,并被分配test为在托管堆上引用该对象。

我感到很多人被绊倒了,因为没有可见的正式构造函数来提醒他们。在这种情况下,它是在幕后发生的,因为该String类型在构造方式方面具有语言支持。

因此,这就是为什么在方法test范围之外看不到更改的原因TestI(string)-我们已经按值传递了引用,现在值已更改!但是,如果String引用是通过引用传递的,则当引用更改时,我们将在TestI(string)方法范围之外看到它。

无论是REF需要在这种情况下关键字。我觉得该out关键字可能更适合这种特殊情况。

class Program
{
    static void Main(string[] args)
    {
        string test = "before passing";
        Console.WriteLine(test);
        TestI(out test);
        Console.WriteLine(test);
        Console.ReadLine();
    }

    public static void TestI(out string test)
    {
        test = "after passing";
    }
}

ref =在函数外部初始化,out =在函数内部初始化,换句话说;ref是双向的,out是仅输出的。因此,一定要使用ref。
保罗·扎赫拉

@PaulZahra:out需要在方法内分配代码才能进行编译。ref没有这样的要求。同样,out在方法之外初始化参数-此答案中的代码是一个反例。
Derek W

应该澄清- out参数可以在方法外部初始化,但不必如此。在这种情况下,我们希望初始化out参数以说明有关string.NET类型的性质的观点。
Derek W

9

实际上,对于任何对象而言,它都是相同的,即,作为引用类型和按引用传递是c#中的两个不同事物。

这将起作用,但是无论哪种类型都适用:

public static void TestI(ref string test)

同样关于字符串是一种引用类型,它也是一种特殊的类型。它的设计是不可变的,因此它的所有方法都不会修改实例(它们会返回一个新实例)。它还具有一些额外的性能。


7

这是一种思考值类型,按值传递,引用类型和按引用传递之间的区别的好方法:

变量是一个容器。

值类型变量包含一个实例。引用类型变量包含指向存储在其他位置的实例的指针。

修改值类型变量会使它包含的实例发生变异。修改引用类型变量会使其指向的实例发生变异。

单独的引用类型变量可以指向同一实例。因此,可以通过指向该实例的任何变量对其进行突变。

值传递参数是具有内容的新副本的新容器。引用传递的参数是具有原始内容的原始容器。

当按值传递值类型参数时:重新分配参数的内容对作用域没有影响,因为容器是唯一的。修改参数对范围没有影响,因为实例是一个独立的副本。

当引用类型的参数按值传递时:重新分配参数的内容对作用域没有影响,因为容器是唯一的。修改参数的内容会影响外部范围,因为复制的指针指向共享实例。

当任何参数通过引用传递时:重新分配参数的内容会影响外部作用域,因为容器是共享的。修改参数的内容会影响外部范围,因为内容是共享的。

结论:

字符串变量是引用类型的变量。因此,它包含一个指向存储在其他位置的实例的指针。当按值传递时,将复制其指针,因此修改字符串参数将影响共享实例。但是,字符串实例没有可变属性,因此无论如何都不能修改字符串参数。通过引用传递时,指针的容器是共享的,因此重新分配仍然会影响外部作用域。


6

一张图片值一千字 ”。

我这里有一个简单的示例,它与您的情况类似。

string s1 = "abc";
string s2 = s1;
s1 = "def";
Console.WriteLine(s2);
// Output: abc

这是发生了什么:

在此处输入图片说明

  • 第1行和第2行:s1s2变量引用同一"abc"字符串对象。
  • 第3行:由于字符串是不可变的,因此"abc"字符串对象不会自行修改(为"def"),而是"def"会创建一个新的字符串对象,然后对其进行s1引用。
  • 第4行:s2仍然引用"abc"字符串对象,因此是输出。

5

上面的答案是有帮助的,我只想添加一个示例,我认为它清楚地说明了当我们传递不带ref关键字的参数时会发生什么,即使该参数是引用类型:

MyClass c = new MyClass(); c.MyProperty = "foo";

CNull(c); // only a copy of the reference is sent 
Console.WriteLine(c.MyProperty); // still foo, we only made the copy null
CPropertyChange(c); 
Console.WriteLine(c.MyProperty); // bar


private void CNull(MyClass c2)
        {          
            c2 = null;
        }
private void CPropertyChange(MyClass c2) 
        {
            c2.MyProperty = "bar"; // c2 is a copy, but it refers to the same object that c does (on heap) and modified property would appear on c.MyProperty as well.
        }

1
这种解释对我来说是最好的。因此,基本上,尽管变量本身是值或引用类型,除非我们使用ref关键字(或out),否则我们将按值传递所有内容。对于我们的日常编码而言,这并不重要,因为我们通常不将对象设置为null或将对象传入传入方法的其他实例中,而是设置其属性或调用其方法。在“字符串”的情况下,将其始终设置为新实例会发生,但是更新是不可见的,这会给未经训练的眼睛带来错误的解释。如果有错请指正。
ΕГИІИО

3

对于好奇的人并完成对话: 是的,String是引用类型

unsafe
{
     string a = "Test";
     string b = a;
     fixed (char* p = a)
     {
          p[0] = 'B';
     }
     Console.WriteLine(a); // output: "Best"
     Console.WriteLine(b); // output: "Best"
}

但是请注意,此更改仅在不安全的块中起作用!因为字符串是不可变的 (来自MSDN):

创建字符串对象后,其内容无法更改,尽管语法使它看起来像可以执行此操作。例如,当您编写此代码时,编译器实际上会创建一个新的字符串对象来保存新的字符序列,并将该新对象分配给b。然后,字符串“ h”可以进行垃圾回收。

string b = "h";  
b += "ello";  

请记住:

尽管字符串是引用类型,但定义了相等运算符(==!=)以比较字符串对象(而不是引用)的值。


0

我相信您的代码类似于以下代码,并且您不应出于与此处相同的原因而期望值发生更改:

 public static void Main()
 {
     StringWrapper testVariable = new StringWrapper("before passing");
     Console.WriteLine(testVariable);
     TestI(testVariable);
     Console.WriteLine(testVariable);
 }

 public static void TestI(StringWrapper testParameter)
 {
     testParameter = new StringWrapper("after passing");

     // this will change the object that testParameter is pointing/referring
     // to but it doesn't change testVariable unless you use a reference
     // parameter as indicated in other answers
 }

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.