【问题标题】:String object is really by reference? [duplicate]字符串对象真的是通过引用吗? [复制]
【发布时间】:2010-12-08 13:05:24
【问题描述】:

我正在学习(新手).NET,但我有一些疑问。

从一本书的例子中我了解到字符串是对象然后是引用类型。

所以,我做了这个测试,结果与我的预期不同:

我真的很好奇,这是一个例外,因为“字符串”是特殊类型吗?

class Program
{
    static void Main(string[] args)
    {
        SByte a = 0;
        Byte b = 0;
        Int16 c = 0;
        Int32 d = 0;
        Int64 e = 0;
        string s = "";
        Exception ex = new Exception();

        object[] types = { a, b, c, d, e, s, ex };

        // C#
        foreach (object o in types)
        {
            string type;
            if (o.GetType().IsValueType)
                type = "Value type";
            else
                type = "Reference Type";
            Console.WriteLine("{0}: {1}", o.GetType(), type);
        }

        // Test if change
        string str = "I'll never will change!";

        Program.changeMe(str);

        Console.WriteLine(str);
    }

    public static string changeMe(string param)
    {
        param = "I have changed you!!!";

        return ""; // no return for test
    }
}

输出:

System.SByte: Value type
System.Byte: Value type
System.Int16: Value type
System.Int32: Value type
System.Int64: Value type
System.String: Reference Type
System.Exception: Reference Type
I'll never will change!

【问题讨论】:

  • 可能值得您花时间reading this article 了解字符串如何表现得很奇怪但实际上很简单,这对我来说无疑是大开眼界!

标签: c# .net string reference


【解决方案1】:

String 确实是一个引用类型。但是,当您的 Main 方法调用 changeMe(str) 时,.NET 会将 str 引用的副本传递给 param 参数中的 changeMe。 changeMe 然后修改这个副本以引用“我已经改变了你!!!”,但原来的 str 引用仍然指向“我永远不会改变”。

作为引用类型意味着如果您更改了传递字符串的状态,调用者将看到这些更改。 (您不能对字符串执行此操作,因为字符串是不可变的,但您可以对其他引用类型执行此操作,例如 Control。)但是重新分配参数不会更改调用方传入的 value参数,即使该值是引用。

【讨论】:

  • 引用(指针)的副本还是字符串(所有字符)本身的副本?
  • 参考的副本。创建字符串的副本会很昂贵,而且没有必要,因为字符串是不可变的(因此可以安全地共享引用)。
【解决方案2】:

字符串是引用。

changeMe 不会更改字符串,它只是在该函数中重新分配一个本地引用(指针)。

现在,如果您将字符串作为 ref arg 传递,您将获得更多乐趣:

public static string changeMe(ref string param) {
    param = "I have changed you!!!";
    return ""; // no return for test
}

【讨论】:

    【解决方案3】:

    这与字符串是值类型或引用类型无关。你的changeMe方法的参数没有标记ref,所以如果方法改变它,调用者将看不到改变。

    试试吧:

    public static string changeMe(ref string param)
    {
        param = "I have changed you!!!";
    
        return ""; // no return for test
    }
    

    【讨论】:

    • 我没有放“ref”,因为这个想法是为了发现为什么“reference”类型不会改变。我认为这可能是多余的。 “带有 ref 参数的引用类型”。
    • 我认为您混淆了两个独立的概念:值/引用类型,以及按值/按引用传递参数。查看此链接了解更多详情:yoda.arachsys.com/csharp/parameters.html
    • -1:对于所有其他引用类型,使用 byval 语义传递它意味着您不能更改调用者复制指向的堆上的哪个对象,这并不意味着您不能更改变量指向的堆上的对象。因为字符串是不可变的,所以它们在这方面表现得像值类型,即使它们是引用类型,但这是通过 CLR 中的特殊附加编码完成的
    • 这与我的回答并不矛盾......这里没有修改字符串本身(无论如何都不可能),但是 param 变量被更改为引用另一个字符串。不,字符串不会“表现得像值类型”,它们的行为与任何其他不可变引用类型完全一样......请参阅 psasik 答案中的引用
    • @Charles:字符串的行为类似于“CLR 中的异常附加编码”的值类型是不正确的。字符串的唯一特别之处在于,与任何其他不可变类型一样,实现者省略了所有 setter 方法和其他在构造函数之外修改对象的方法。我不会认为省略方法是“特殊的附加编码”。它只是选择省略会修改字符串的方法。那是更少的编码,而不是“额外的”编码,当然也不是“例外”。
    【解决方案4】:

    Program.changeMe(str) 没有导致str设置为“我改变了你!!!”的原因就是虽然string是引用类型,但是引用是按值传递的。

    所以 param 是引用的副本,它在 Program.changeMe(param) 的范围内被更改,然后在方法结束时被丢弃。

    引用str只是复制,没有改成引用“我改变了你!!!”

    【讨论】:

      【解决方案5】:

      字符串确实是一种特殊情况。它们是引用类型,但行为类似于值类型。这是 .Net 开发团队为了“简单”而完成的。

      伟大的报价: 它是参考类型 字符串是一种值类型是一个常见的误解。那是因为它的不变性(见下一点)使它的行为有点像值类型。它实际上就像一个普通的引用类型。有关值类型和引用类型之间差异的更多详细信息,请参阅我关于参数传递和内存的文章。

      发件人:http://www.yoda.arachsys.com/csharp/strings.html

      【讨论】:

      • 字符串并不特别:您可以在自己的代码中轻松创建具有值语义的不可变引用类型。发帖人没有看到他原来的变化的原因与不变性无关:如果他通过了控制,他会看到完全相同的东西。
      • 是的,“字符串确实是一种特殊情况”的说法在这种情况下可能会产生误导。字符串是普通的不可变引用类型,在所有用途(object.Equals 行为、可空性、构造、不可复制等)中都表现为引用类型而不是值类型。它们是不可变的这一事实与它们是引用类型还是值类型是正交的:引用类型或值类型都可能是不可变的。
      • 但我应该以另一种方式提到 字符串确实是一种特殊情况:NET Framework 运行时(CLR)优化了在 RAM 中存储字符串的内部字符数组一种独特的方式。如果您使用低级调试器查看 RAM,您会发现字符串的存储方式确实不同。但是,除非您在某些特定情况下仔细测量 RAM 消耗,否则无法从您的用户级代码中检测到这种差异,对于所有正常目的而言,字符串并没有什么特别之处。
      • 虽然只有 in-place-stored 类型可以提供可变值语义(并且通常提供这种语义的唯一类型是结构体),并且带有暴露字段的结构体只能提供这种语义,但没有暴露字段的结构体和堆引用类型可以提供不可变引用语义、不可变值语义或可变引用语义。尽管字符串是一种堆引用类型(通常称为引用类型),但它们提供了不可变的值语义
      【解决方案6】:

      是的,这看起来很奇怪。发生的事情是您在参数中传递了对字符串的引用,但您是按值 传递它,即您正在参数中创建引用的副本。因此,当您在您的功能范围内重新分配它时,您只是更改了 changeMe() 方法的字符串引用的本地副本。

      这里有更多关于此的信息: http://www.yoda.arachsys.com/csharp/parameters.html

      【讨论】:

        【解决方案7】:

        MSDN page 可以帮助解释为什么它是引用类型。

        因为Stringchars 的集合,并且继承自各种Collection 基类,所以它是一个类,因此是一个引用类型。

        【讨论】:

        • 这是不正确的。您可以轻松定义一个包含字符集合的结构。
        • 是的,String 是一种特殊的引用类型,不可变;利用实习生池。您可能想阅读以下内容:en.wikipedia.org/wiki/String_interning
        • String 不继承自集合基类。它直接来自对象。它实现各种集合接口,但结构也可以实现接口(查看 Int32 grin)。
        【解决方案8】:

        您按值传递并复制了引用。通过引用传递它会改变。好吧,实际上将创建一个新字符串,并且您的引用将指向一个新位置。

        【讨论】:

          【解决方案9】:

          如果您打算将参数的值更改为方法,您应该真正使用“ref”或“out”关键字。否则,您可能会做出令人困惑的方法,这些方法可能会或可能不会达到您的预期。正如其他人指出的那样,您正在更改字符串引用的值而不是对象的实例。为了了解它如何与其他类一起使用,我编写了一个快速示例

          static void Main(string[] args)
          {
              var str = "String";
              var obj = new MyObject() { Value = "Object" };
          
              Console.WriteLine(str); //String
              Console.WriteLine(obj); //Object
          
              ChangeMe(str);
              ChangeMe(obj);
          
              Console.WriteLine(str); //String
              Console.WriteLine(obj); //Object
          
              ChangeMeInner(obj);
              // this is where it can get confusing!!!
              Console.WriteLine(obj); //Inner 
          
              ChangeMe(ref str);
              ChangeMe(ref obj); 
          
              Console.WriteLine(str); // Changed
              Console.WriteLine(obj); // Changed
          
              Console.Read();
          }
          class MyObject
          {
              public string Value { get; set; }
              public override string ToString()
              {
                  return Value;
              }
          }
          static void ChangeMe(MyObject input)
          {
              input = new MyObject() { Value = "Changed" };
          }
          static void ChangeMeInner(MyObject input)
          {
              input.Value = "Inner";
          }
          static void ChangeMe(string input)
          {
              input = "Changed";
          }
          static void ChangeMe(ref MyObject input)
          {
              input = new MyObject() { Value = "Changed" };
          }
          static void ChangeMe(ref string input)
          {
              input = "Changed";
          }
          

          【讨论】:

            【解决方案10】:

            对于所有其他引用类型,使用 byval 语义(没有 ref 或 out )传递它,意味着您不能更改调用者复制指向的堆上的 object ,这并不意味着您可以' t 更改变量指向的堆上对象的内容。因为字符串是不可变的,所以它们确实是一个例外,它们在这方面表现类似于值类型,即使它们是引用类型,但这是通过 CLR 中的特殊附加编码来完成的。 p>

            【讨论】:

            • 不正确。 1.没有“CLR中的特殊附加编码”使字符串成为值类型。 2.第一句中的“其他”一词使其具有欺骗性(“对于所有其他引用类型......”)。事实上,对于 all 引用类型,包括 字符串,这句话的其余部分都适用。实际上,您不能(轻松)更改指向的字符串,但是如果您想以这种方式解释语句,它也适用于所有不可变类型,因此“对于所有其他引用类型”需要为“对于可变引用类型”。我只是担心人们会因此而被误导。
            • 上一条评论中的错字:第一个编号的项目应该是: 1. 没有“CLR 中的特殊附加编码”使字符串表现得像 一个值类型。跨度>
            • 谜题:NET Framework 中其实有一种修改字符串的方法。这是什么?
            • 在 CLR(或 CTS)中有额外的代码使字符串表现得像值类型。对于字符串 thjat 没有被保留,它是在堆上创建一个新对象的代码,它使用您“分配”给字符串的新值,并将变量中的指针值替换为这个新对象的地址而不是旧的一。对于实习字符串,负责管理实习表的所有代码,以及字符串“变量”和实习表中的条目之间的间接级别是 CTS 中任何其他类型未使用的附加代码。
            • 但是@Ray,关于能够更改内容与哪个对象的第二点,再次,由于字符串的工作方式,当您通过引用传递字符串时,它似乎只能像您一样更改字符串的内容。很多事情都发生在表面之下。没有其他 CTS 类型可以创建此行为。所以我不确定怎么会有人被这个误导......
            猜你喜欢
            • 2017-08-16
            • 2011-02-12
            • 1970-01-01
            • 1970-01-01
            • 2016-01-04
            • 2019-04-28
            • 2022-01-02
            • 2012-01-28
            相关资源
            最近更新 更多