C#字符串引用类型?

Posted

技术标签:

【中文标题】C#字符串引用类型?【英文标题】:C# string reference type? 【发布时间】:2010-11-08 22:44:58 【问题描述】:

我知道 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++ 指针重现。 MSDN 也有很好的解释。 In C#, why is String a reference type that behaves like a value type?的可能重复 【参考方案1】:

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

如果你传递字符串引用by引用,它将按你的预期工作:

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";
    

现在您需要区分对引用所引用的对象进行更改和对变量(例如参数)进行更改以使其引用不同的对象。我们不能对字符串进行更改,因为字符串是不可变的,但我们可以用 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");
    

更多详情请见my article on parameter passing。

【讨论】:

同意,只是想明确一点,使用 ref 修饰符也适用于非引用类型,即两者是完全不同的概念。 @Jon Skeet 喜欢您文章中的旁注。你应该把 referenced 作为你的答案【参考方案2】:

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

public static void TestI(string test)

    test = "after passing";

参数test 持有对字符串的引用,但它是一个副本。我们有两个变量指向字符串。而且因为任何对字符串的操作实际上都会创建一个新对象,所以我们将本地副本指向新字符串。但是原来的test变量并没有改变。

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

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

【讨论】:

【参考方案3】:

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

在原始代码中,只要这行执行:

test = "after passing";

那么test 不再引用原始对象。我们创建了一个new String 对象并分配test 以引用托管堆上的该对象。

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

因此,这就是为什么对 test 的更改在 TestI(string) 方法的范围之外不可见的原因 - 我们已经按值传递了引用,现在该值已更改!但是如果 String 引用是通过引用传递的,那么当引用发生变化时,我们会看到它超出了 TestI(string) 方法的范围。

在这种情况下需要 refout 关键字。我觉得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 是 out-only。所以肯定应该使用 ref。 @PaulZahra: out 需要在编译代码的方法中赋值。 ref 没有这个要求。 out 参数也在方法之外初始化 - 这个答案中的代码是一个反例。 应该澄清 - out 参数可以在方法之外初始化,但不是必须的。在这种情况下,我们要初始化 out 参数以说明 .NET 中 string 类型的本质。【参考方案4】:

一张图胜过千言万语”。

我这里有一个简单的例子,和你的情况类似。

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

事情是这样的:

第 1 行和第 2 行:s1s2 变量引用相同的 "abc" 字符串对象。 第 3 行:因为strings are immutable,所以"abc" 字符串对象不修改自身(到"def"),而是创建了一个新的"def" 字符串对象,然后s1 引用它。 第 4 行:s2 仍然引用 "abc" 字符串对象,这就是输出。

【讨论】:

目前为止最好的解决方案,只是快速轻松地理解它的最简单方法。干得好。 是""一张图抵千言""【参考方案5】:

实际上,对于任何对象来说,它都是相同的,即作为引用类型和通过引用传递在 c# 中是两种不同的东西。

这可行,但无论类型如何都适用:

public static void TestI(ref string test)

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

【讨论】:

【参考方案6】:

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

变量是一个容器。

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

修改值类型变量会改变它包含的实例。 修改引用类型变量会改变它指向的实例。

单独的引用类型变量可以指向同一个实例。 因此,同一个实例可以通过指向它的任何变量进行变异。

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

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

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

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

总结:

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

【讨论】:

【参考方案7】:

以上答案很有帮助,我想添加一个示例,我认为该示例清楚地展示了当我们在没有 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.
        

【讨论】:

这个解释最适合我。所以基本上我们通过值传递所有东西,尽管变量本身是值或引用类型,除非我们使用 ref 关键字(或 out)。这在我们的日常编码中并不突出,因为我们通常不会将对象设置为 null 或传入它们的方法内的不同实例,而是设置它们的属性或调用它们的方法。在“字符串”的情况下,将其设置为一个新实例一直都在发生,但是新的更新是不可见的,这给未经训练的眼睛一个错误的解释。如有错误请指正。【参考方案8】:

对于好奇的头脑并完成对话: 是的,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";  

请记住:

虽然字符串是引用类型,但等式运算符(==!=) 被定义为比较字符串对象的值,而不是 参考文献。

【讨论】:

【参考方案9】:

试试:


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

【讨论】:

您的答案应该不仅仅包含代码。它还应该包含对其工作原理的解释。【参考方案10】:

我相信您的代码类似于以下代码,并且您不应该期望该值会因为同样的原因而发生变化:

 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
 

【讨论】:

【参考方案11】:

另一种绕过字符串行为的方法。仅使用 ONE 元素的字符串数组并操作该元素。

class Test

    public static void Main()
    
        string[] test = new string[1] "before passing";
        Console.WriteLine(ref test);
        TestI(test);
        Console.WriteLine(ref test);
    

    public static void TestI(ref string[] test)
    
        test[0] = "after passing";
    

【讨论】:

以上是关于C#字符串引用类型?的主要内容,如果未能解决你的问题,请参考以下文章

c#中值类型与引用类型的值传递与引用传递

证明字符串是 C# 中的引用类型

30天C#基础巩固-----值类型/引用类型,泛型,空合并操作符(??),匿名方法

C#数据类型

C# 8 中的 Non-Nullable 引用类型的默认值是多少?

值类型和引用类型