带有字符串参数的方法和.Net中的ref

时间:2011-02-01 12:24:03

标签: .net il

我有一个有趣的例子,你能解释一下吗? 据我所知,string是一个引用类,但它无法修改。所以当我尝试修改字符串时 - 框架创建对新堆地址的新引用。我不明白为什么来自方法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会有价值“我喜欢伏特加”,但为什么呢?

P.S。 或者这样

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

P.P.S。来自示例的一些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   

3 个答案:

答案 0 :(得分:3)

简单地说,ref关键字使被调用方法能够在调用站点处理引用。这是ref的全部目的。

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

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

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

<强>更新
让我们从你的评论中剖析出这个例子(这些名字已被改变以保护无辜者):

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 Parameter passing in C#的链接,如果您还没有,请阅读。它在解释这些事情方面做得非常好(以及更多;它还涉及价值类型)。

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

答案 1 :(得分:2)

Beer中,指向字符串的指针按值传递,因此当您执行s1 = s1 + "beer";时,指针的副本会更改,并且在方法外部不可见。
但是,在Vodka中,指向字符串的指针是通过引用传递的,因此当您执行s1 = s1 + "vodka";时,相同的指针将被更改,并且在方法外部可见。

答案 2 :(得分:2)