0

我有一个有趣的例子,你能解释一下吗?据我了解,字符串是一个参考类,但不能修改。因此,当我尝试修改字符串时,框架会创建对新堆地址的新引用。我不明白为什么 Bear 方法中的这个新地址没有传递到主调用堆栈。

class DoWork
{
    public static void Beer(string s1)  {
        s1 = s1 + "beer";
    }
    public static void Vodka(ref string s2) {
        s2 = s2 + "vodka";
    }
}

string sss = "I Like ";
DoWork.Beer(sss);
DoWork.Vodka(ref sss);

所以 sss 将具有“我喜欢伏特加”的价值,但为什么呢?

PS还是这样

DoWork.Vodka(ref sss);
DoWork.Beer(sss);

PPS 示例中的一些 IL 代码

DoWork.Beer:
IL_0000:  ldarg.0     
IL_0001:  ldstr       "beer"
IL_0006:  call        System.String.Concat
IL_000B:  starg.s     00 
IL_000D:  ret

DoWork.Vodka:
IL_0000:  ldarg.0     
IL_0001:  ldarg.0     
IL_0002:  ldind.ref   
IL_0003:  ldstr       "vodka"
IL_0008:  call        System.String.Concat
IL_000D:  stind.ref   
IL_000E:  ret   
4

3 回答 3

4

简而言之,ref关键字使被调用方法能够在调用站点操作引用。这是 的全部目的ref

发生的情况是,当Vodka返回时,sss已替换为新的字符串实例。它不会更新原始实例。没有ref修饰符,这是不可能的。

我经常发现人们在谈论它时感到困惑的一件事是,当您处理引用类型时,当您考虑按值按引用传递参数的概念时。当你通过value传递一个字符串(一个引用类型)时,你传递的到底是什么?您传递变量的值。在这种情况下,该值是对字符串的引用。因此,您传递该值,调用方法可以使用该值来访问字符串。但是,由于参数是按值传递的,因此传递的是值本身(本质上是引用的副本),而不是指向存储该值的内存位置的指针。这就是为什么调用方法不能替换调用站点的实例的原因。

当通过引用而不是值传递参数时,被调用的方法可以访问存储值的内存位置(现在请记住,值是对字符串的引用)。这意味着被调用的方法现在可以更新此值,以使调用站点的变量包含对另一个字符串的引用。

更新
让我们从您的评论中剖析示例(名称已更改以保护无辜者):

class SomeClass
{
    public int Value { get; set; }
}

class DoWork
{
    public static void DoOne(SomeClass c) { c.Value = c.Value + 1; }
    public static void DoTwo(ref SomeClass c) { c.Value = c.Value + 2; }
}

SomeClass是引用类型。这意味着您向其传递此类实例的任何代码段(并且,为了清楚起见,我们从不传递引用类型的实际实例,我们传递对实例的引用),可以操作该实例中的数据*。这意味着它可以调用方法、设置属性值等等,并且由此导致的任何状态变化在调用站点上也是可见的。关键字对此ref没有影响。因此,在您的示例中,ref关键字没有区别。但是,请考虑以下更改:

class DoWork
{
    public static void DoOne(SomeClass c) 
    { 
        c = new SomeClass(); 
        c.Value = 1; 
    }
    public static void DoTwo(ref SomeClass c) 
    {
        c = new SomeClass(); 
        c.Value = 2; 
    }
}

现在,让我们称之为:

var temp = new SomeClass() { Value = 42 };
DoWork.DoOne(temp);
Console.WriteLine(temp.Value); // prints 42

DoWork.DoTwo(ref temp);
Console.WriteLine(temp.Value); // prints 2

现在发生的情况是这两种方法都创建了一个新的SomeClass实例来操作。由于DoOne获取的是按值传递的引用,它会更新自己的私有引用副本以指向新的实例,在调用处自然不会看到这种变化。

另一方面,由于DoTwo获取通过引用传递的引用,当它创建一个新实例并将其分配给 时c,它会更新与调用站点使用的内存位置相同的内存位置,因此temp现在正在引用在内部创建的新实例DoTwo

总结一下:对于引用类型,你唯一需要使用ref的时候是你想启用被调用的方法来替换传递的实例本身。如果您只想操纵该实例的状态,则引用类型将始终允许这样做。

Cody Gray 发布了 Jon Skeet在 C# 中传递参数的链接,如果您还没有,请阅读它。它在解释这些事情方面做得很好(还有更多;它还处理值类型)。

* (除非类型是不可变的,比如字符串,但那是完全不同的故事......)

于 2011-02-01T12:28:54.420 回答
2

Beer指向字符串的指针中是按值传递的,所以当你这样做时s1 = s1 + "beer";,指针的副本被更改并且它在方法之外是不可见的。
然而,指向字符串的Vodka指针是通过引用传递的,所以当你这样做时s1 = s1 + "vodka";,相同的指针正在被更改并且它在方法之外是可见的。

于 2011-02-01T12:29:07.947 回答
2

看看不可变类型

http://codebetter.com/patricksmacchia/2008/01/13/immutable-types-understand-them-and-use-them

于 2011-02-01T12:30:48.910 回答