【问题标题】:How are strings passed in .NET?.NET 中如何传递字符串?
【发布时间】:2012-06-03 06:58:03
【问题描述】:

当我将string 传递给函数时,是指向传递的字符串内容的指针,还是像struct 那样传递给堆栈上的函数的整个字符串?

【问题讨论】:

    标签: .net string function parameter-passing


    【解决方案1】:

    一个引用被传递;但是,从技术上讲,它并不是通过引用传递的。这是一个微妙但非常重要的区别。考虑以下代码:

    void DoSomething(string strLocal)
    {
        strLocal = "local";
    }
    void Main()
    {
        string strMain = "main";
        DoSomething(strMain);
        Console.WriteLine(strMain); // What gets printed?
    }
    

    要了解这里发生的事情,您需要知道三件事:

    1. 字符串是 C# 中的引用类型。
    2. 它们也是不可变的,所以任何时候你做一些看起来你正在改变字符串的事情,你不是。一个全新的字符串被创建,引用指向它,旧的被丢弃。
    3. 即使字符串是引用类型,strMain 也不会通过引用传递。它是一个引用类型,但引用本身是按值传递。任何时候你传递一个没有ref 关键字的参数(不包括out 参数),你就传递了值。

    所以这一定意味着您正在...按值传递引用。由于它是引用类型,因此只有引用被复制到堆栈中。但这意味着什么?

    按值传递引用类型:您已经在这样做了

    C# 变量是引用类型值类型。 C# 参数要么按引用传递,要么按值传递。术语在这里是个问题。这些听起来像是同一件事,但事实并非如此。

    如果你传递了一个 ANY 类型的参数,并且你没有使用 ref 关键字,那么你已经通过值传递了它。如果您按值传递它,那么您真正传递的是一个副本。但如果参数是引用类型,那么您复制的内容就是引用,而不是它指向的任何内容。

    这是Main 方法的第一行:

    string strMain = "main";
    

    我们在这一行创建了两件事:一个字符串,其值 main 存储在内存中某处,以及一个名为 strMain 的引用变量指向它。

    DoSomething(strMain);
    

    现在我们将该引用传递给DoSomething。我们已经按值传递了它,这意味着我们制作了一个副本。它是一个引用类型,这意味着我们复制了引用,而不是字符串本身。现在我们有两个引用,每个引用都指向内存中的相同值。

    被调用者内部

    这是DoSomething方法的顶部:

    void DoSomething(string strLocal)
    

    没有ref 关键字,所以strLocalstrMain 是指向相同值的两个不同引用。如果我们重新分配strLocal...

    strLocal = "local";   
    

    ...我们没有更改存储的值;我们采用了名为strLocal 的引用并将其瞄准了一个全新的字符串。当我们这样做时,strMain 会发生什么? Nothing. It's still pointing at the old string.

    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,并且复制的引用和原始引用仍然指向它。 The properties of the MutableThing itself have changed:

    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。值是不可变的,引用是副本。

    通过引用传递引用

    为了证明有区别,下面是when you pass a reference by reference:

    void DoSomethingByReference(ref string strLocal)
    {
        strLocal = "local";
    }
    void Main()
    {
        string strMain = "main";
        DoSomethingByReference(ref strMain);
        Console.WriteLine(strMain);          // Prints "local"
    }
    

    这一次,Main 中的字符串确实发生了变化,因为您传递了引用而没有将其复制到堆栈中。

    所以即使字符串是引用类型,通过值传递它们意味着被调用者中发生的任何事情都不会影响调用者中的字符串。但是由于它们引用类型,当你想要传递它时,你不必将整个字符串复制到内存中。

    更多资源:

    【讨论】:

    • @TheLight - 对不起,但当您说“引用类型默认通过引用传递”时,您在这里不正确。默认情况下,所有参数都是按值传递的,但是对于引用类型,这意味着 引用是按值传递的。 您将引用类型与引用参数混为一谈,这是可以理解的,因为它非常令人困惑区别。看Passing Reference Types by Value section here.你的链接文章很正确,但它实际上支持我的观点。
    • @JustinMorgan 不要提出一个死评论线程,但如果你用 C 语言思考,我认为 TheLight 的评论是有道理的。在 C 语言中,数据只是一块内存。引用是指向该内存块的指针。如果将整个内存块传递给函数,则称为“按值传递”。如果您传递指针,则称为“通过引用传递”。在 C# 中,没有传入整个内存块的概念,因此他们将“按值传递”重新定义为将指针传入。这似乎是错误的,但指针也只是一块内存!对我来说,术语很随意
    • @roliu - 问题是我们不是在 C 中工作,尽管 C# 的名称和语法相似,但它却截然不同。一方面,references are not the same as pointers,以这种方式思考它们可能会导致陷阱。不过,最大的问题是“通过引用传递”在 C# 中具有非常具体的含义,需要 ref 关键字。要证明通过引用传递会有所作为,请参阅此演示:rextester.com/WKBG5978
    • @JustinMorgan 我同意将 C 和 C# 术语混合使用是不好的,但是,虽然我喜欢 lippert 的帖子,但我不同意将引用视为指针在这里特别模糊。博客文章描述了将引用视为指针如何赋予它过多的权力。我知道ref 关键字具有实用性,我只是想解释为什么一个可能认为在 C# 中按值传递引用类型似乎是“传统”(即 C)概念通过引用传递(在 C# 中通过引用传递引用类型似乎更像是通过值传递对引用的引用)。
    • 你是对的,但我认为@roliu 指的是Foo(string bar) 之类的函数如何被认为是Foo(char* bar)Foo(ref string bar) 将是Foo(char** bar)(或Foo(char*& bar)Foo(string& bar) 在 C++ 中)。当然,这不是你应该每天思考的方式,但它实际上帮助我最终了解了幕后发生的事情。
    【解决方案2】:

    C# 中的字符串是不可变的引用对象。这意味着对它们的引用是(按值)传递的,一旦创建了字符串,就不能修改它。生成字符串修改版本(子字符串、修剪版本等)的方法会创建原始字符串的修改副本

    【讨论】:

      【解决方案3】:

      字符串是特殊情况。每个实例都是不可变的。当您更改字符串的值时,您将在内存中分配一个新字符串。

      所以只有引用被传递给你的函数,但是当字符串被编辑时,它会变成一个新实例,不会修改旧实例。

      【讨论】:

      • 字符串不是在这方面的特例。创建具有相同语义的不可变对象非常容易。 (也就是说,一个类型的实例不暴露一个方法来改变它......)
      • 字符串是特殊情况——它们实际上是不可变的引用类型,看起来是可变的,因为它们的行为类似于值类型。
      • @Enigmativity 按照这个逻辑,Uri(类)和Guid(结构)也是特例。我看不出System.String 与其他不可变类型相比如何表现得更像“值类型”......无论是类还是结构起源。
      • @pst - 字符串具有特殊的创建语义 - 与 UriGuid 不同 - 您可以将字符串文字值分配给字符串变量。该字符串似乎是可变的,就像重新分配 int 一样,但它隐式创建了一个对象 - 没有 new 关键字。
      • String 是一个特例,但这与这个问题无关。值类型、引用类型,无论哪种类型,在这个问题中的作用都是一样的。
      猜你喜欢
      • 1970-01-01
      • 2023-02-22
      • 2016-03-20
      • 1970-01-01
      • 1970-01-01
      • 2021-05-05
      • 1970-01-01
      • 2018-09-02
      • 1970-01-01
      相关资源
      最近更新 更多