.NET参数传递 - 按值引用v / s

时间:2010-01-24 02:01:17

标签: c# .net clr value-type reference-type

我正在尝试验证我对C#/ .NET / CLR如何处理值类型和引用类型的理解。我已经阅读了很多相互矛盾的解释

这就是我今天所理解的,如果我的假设是错误的,请纠正我。

诸如int等的值类型存在于堆栈上,引用类型存在于托管堆但是如果引用类型具有例如double类型的实例变量,它将与其一起生存堆上的对象

第二部分是我最困惑的。

让我们考虑一个名为Person的简单类。

Person有一个名为Name的属性。

假设我在另一个类中创建了Person实例,我们称之为UselessUtilityClass。

请考虑以下代码:

class UselessUtilityClass
{
   void AppendWithUnderScore(Person p)
   {
     p.Name = p.Name + "_";
   }
}

然后我们在某处:

Person p = new Person();
p.Name = "Priest";
UselessUtilityClass u = new UselessUtilityClass();
u.AppendWithUnderScore(p);

Person是一个引用类型,当传递给UselessUtilityClass时 - 这就是我去的地方 - 坚固...... VARIABLE p是Person引用的一个实例,由 VALUE传递,这意味着当我写p.Name时,我会看到“牧师_”

然后如果我写了

Person p2 = p;

我做了

p2.Name =“不是牧师”;

写下p的名字就像下面我会得到“不是牧师”

Console.WriteLine(p.Name) // will print "Not a Priest"

这是因为它们是引用类型并指向内存中的相同地址。

我的理解是否正确?

我认为当人们说 .NET中的所有对象都通过参考传递时会出现一些误解,这根据我的想法并不存在。我错了,这就是为什么我来到堆叠器。

6 个答案:

答案 0 :(得分:27)

  

诸如int等的值类型存在于堆栈中。引用类型存在于托管堆,如果引用类型具有double类型的实例变量,它将与其在堆上的对象一起生存

不,这不正确。正确的语句是“在CLI的Microsoft实现中,执行线程的系统堆栈上分配的局部变量和值类型的形式参数既不直接在迭代器块中也不在λ或匿名方法的封闭外部变量中分配和Microsoft的C#实现。“

不要求任何版本的C#或任何版本的CLI都使用系统堆栈。当然我们这样做是因为它是一个方便的数据结构,用于局部变量和值类型的形式参数,它们不直接在迭代器块或lambda或匿名方法的封闭外部变量中。

请参阅我关于此主题的文章,讨论(1)为什么这是一个实现细节,(2)我们从这个实现选择中获得了什么好处,以及(3)做出这个实现选择的愿望有哪些限制推动语言设计。

http://blogs.msdn.com/ericlippert/archive/2009/04/27/the-stack-is-an-implementation-detail.aspx

http://blogs.msdn.com/ericlippert/archive/2009/05/04/the-stack-is-an-implementation-detail-part-two.aspx

  

Person是一个引用类型,当传递给UselessUtilityClass时 - 这就是我去的地方 - 坚果......

深吸一口气。

变量是存储位置。每个存储位置都有一个关联的类型。

其关联类型为引用类型的存储位置可能包含该类型对象的引用,或者可能包含空引用。

关联类型为值类型的存储位置始终包含该类型的对象。

变量的存储位置的内容

  

VARIABLE p是Person引用的一个实例,由VALUE传递,

变量p是存储位置。它包含对Person实例的引用。因此,变量的值是对Person的引用。该值 - 对实例的引用 - 传递给被调用者。现在另一个变量,你可以混淆地称为“p”,它包含相同的值 - 该值是对特定对象的引用。

现在,也可以传递对变量的引用,很多人都觉得这很容易让人感到困惑。更好的思考方式就是当你说

void Foo(ref int x) { x = 10; }
...
int p = 3456;
Foo(ref p);

这意味着“x是变量p的别名”。也就是说,对于相同的变量,x和p是两个名称。因此无论p的值是什么,这也是x的值,因为它们是同一存储位置的两个名称。

现在有意义吗?

答案 1 :(得分:8)

  

诸如int等的值类型存在   堆栈,引用类型存在于   托管堆但是如果引用的话   例如,type有一个实例   double类型的变量,它会存活   以及它在堆上的对象

正确。

您还可以将其描述为实例变量,它是为堆上实例分配的内存区域的一部分。

  

VARIABLE p是一个实例   Person引用传递给   VALUE

该变量实际上不是该类的实例。该变量是对类实例的引用。引用按值传递,这意味着您传递引用的副本。此副本仍指向与原始引用相同的实例。

  

我认为存在一些误解   当人们说所有对象时继续   在.NET中由Reference

传递

是的,这绝对是一种误解。 所有参数都按值传递(除非您使用refout关键字通过引用传递它们)。传递引用与通过引用传递 不同。

引用是一种值类型,这意味着您作为参数传递的所有内容都是值类型。你永远不会传递一个对象实例,总是它的引用。

答案 2 :(得分:1)

当你传递一个人时,它正在复制引用 - 不要将它与对象的副本混淆。换句话说,它正在为同一个对象创建第二个引用,然后传递它。

当您传递ref(使用ref / out关键字)时,它会将相同的引用传递给您在调用者中使用的对象,而不是创建引用的副本。

答案 3 :(得分:1)

也许这些例子可以显示引用类型和值类型之间以及通过引用传递和传递值之间的差异:

//Reference type
class Foo {
    public int I { get; set; }
}

//Value type
struct Boo {
    //I know, that mutable structures are evil, but it only an example
    public int I { get; set; }
}


class Program
{
    //Passing reference type by value
    //We can change reference object (Foo::I can changed), 
    //but not reference itself (f must be the same reference 
    //to the same object)
    static void ClassByValue1(Foo f) {
        //
        f.I++;
    }

    //Passing reference type by value
    //Here I try to change reference itself,
    //but it doesn't work!
    static void ClassByValue2(Foo f) {
        //But we can't change the reference itself
        f = new Foo { I = f.I + 1 };
    }

    //Passing reference typ by reference
    //Here we can change Foo object
    //and reference itself (f may reference to another object)
    static void ClassByReference(ref Foo f) {
        f = new Foo { I = -1 };
    }

    //Passing value type by value
    //We can't change Boo object
    static void StructByValue(Boo b) {
        b.I++;
    }

    //Passing value tye by reference
    //We can change Boo object
    static void StructByReference(ref Boo b) {
        b.I++;
    }

    static void Main(string[] args)
    {
        Foo f = new Foo { I = 1 };

        //Reference object passed by value.
        //We can change reference object itself, but we can't change reference
        ClassByValue1(f);
        Debug.Assert(f.I == 2);

        ClassByValue2(f);
        //"f" still referenced to the same object!
        Debug.Assert(f.I == 2);

        ClassByReference(ref f);
        //Now "f" referenced to newly created object.
        //Passing by references allow change referenced itself, 
        //not only referenced object
        Debug.Assert(f.I == -1);

        Boo b = new Boo { I = 1 };

        StructByValue(b);
        //Value type passes by value "b" can't changed!
        Debug.Assert(b.I == 1);

        StructByReference(ref b);
        //Value type passed by referenced.
        //We can change value type object!
        Debug.Assert(b.I == 2);

        Console.ReadKey();
    }

}

答案 4 :(得分:0)

“按值传递”一词有点误导。

你正在做两件事:

1)将引用类型(Person p)作为参数传递给方法

2)将refence类型变量(Person p2)设置为已存在的变量(Person p)

让我们看看每个案例。

案例1

您创建的Person p指向内存中的某个位置,让我们调用此位置x。当您进入方法AppendWithUnderScore时,您运行以下代码:

p.Name = p.Name + "_"; 

方法调用创建一个新的局部变量p,它指向内存中的相同位置:x。因此,如果您在方法中修改p,则更改p。

的状态

但是,在此方法中,如果设置p = null,则 将使方法外的p无效。此行为称为“按值传递”

案例2

这种情况与上述情况类似,但略有不同。当您创建一个新变量p2 = p时,您只是说p2引用p位置处的对象。所以现在如果你修改p2,你就是在修改p,因为它们引用了同一个对象。如果你现在说p2 = null,那么p现在也将为null。请注意此行为与方法调用内部行为之间的区别。这种行为差异概述了调用方法时“按值传递”的方式

答案 5 :(得分:0)

规范没有说明在何处分配值类型和对象。这是一个正确的C#实现,可以说在堆上分配所有内容,还有Atr在堆上分配值的情况,而不是你编写的那些。

int i = 4; Func dele =()=> (对象)I;

将导致在堆上分配(副本)因为编译器将使其成为类的成员,尽管它未被声明为此类。除此之外你还有很多地方。并没有一切都没有作为参考传递。它更接近于要说明每个参数都是通过值传递但仍然不完全正确(例如ref或out)。