18

我试图验证我对 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 - 这是我要去的地方 - 坚果......作为 Person 引用实例的VARIABLE p 由VALUE传递,这意味着当我写 p.Name 我会看到“牧师_”

然后如果我写

Person p2 = p;

我愿意

p2.Name = "不是牧师";

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

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

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

我的理解正确吗?

我认为当人们说.NET 中的所有对象都由 Reference 传递时存在一些误解,这与我的想法并不相符。我可能是错的,这就是我来到 Stackers 的原因。

4

6 回答 6

29

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

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

不要求任何版本的 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 时——这就是我要去的地方——疯了......

深吸一口气。

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

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

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

变量的值是存储位置的内容

作为 Person 引用实例的 VARIABLE p 由 VALUE 传递,

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

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

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

what this means is "x is an alias for variable p". That is, x and p are two names for the same variable. So whatever the value of p is, that's also the value of x, because they are two names for the same storage location.

Make sense now?

于 2010-01-24T20:28:41.507 回答
8

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

正确的。

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

作为 Person 引用实例的 VARIABLE p 由 VALUE 传递

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

我认为当人们说 .NET 中的所有对象都通过引用传递时存在一些误解

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

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

于 2010-01-24T02:13:43.137 回答
1

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

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

于 2010-01-24T02:04:21.940 回答
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();
    }

}
于 2010-01-24T07:55:43.213 回答
0

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

您正在做两件事:

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

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

让我们看看每个案例。

情况1

您创建了指向内存中某个位置的 Person p,我们称这个位置为 x。当您进入 methodAppendWithUnderScore时,您运行以下代码:

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

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

但是,在此方法内部,如果您设置p = null,则不会将方法外部的 p 清空这种行为称为“按值传递”

案例2

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

于 2010-01-24T06:08:16.913 回答
0

规范没有说明在哪里分配值类型和对象。说在堆上分配所有内容以及在堆上分配值而不是您编写的值的 Atr 情况是正确的 C# 实现。

诠释 i = 4; 函数删除 = ()=> (object)i;

将导致 i 在堆上分配(的副本),因为编译器将使其成为类的成员,即使它没有被声明为这样。除此之外,你几乎是正确的。并且没有所有东西都没有作为参考传递。如果说每个参数都是按值传递但仍不完全正确(例如 ref 或 out),这将更接近真相。

于 2010-01-24T07:54:27.897 回答