【问题标题】:Reference types as parameters引用类型作为参数
【发布时间】:2014-06-23 06:24:52
【问题描述】:

所以我在深入阅读 Jon Skeet 的 C# 时遇到了一些神话,比如引用类型总是由 ref 传递,所以我决定自己做一个小实验。

正如您在下面的代码中看到的,我有一个简单的 Car 类,它有一个属性,在调用构造函数时初始化为 500。我还有将 null 分配给参数值的 NullIt 函数和仅更改 Speed 属性值的 SpeedUp 方法。

检查 main 方法,您可以看到我实例化了一个 Car 对象,然后我将对象传递给静态 SpeedUp 方法,Speed 值更改为 1000,但是当我将它传递给静态 NullIt 方法时,对象保持不变.由此我唯一可以假设的是对象是按值传递的,而字段/属性是通过引用传递的。是这样吗?

我知道如果我使用 ref 关键字传递它会返回 null。

  class Program
   {
    static void Main(string[] args)
    {
        Car c = new Car();
        Car.SpeedUP(c);
        Car.NullIt(c);
    }

    class Car
    {
        public int Speed { get; set; }

        public Car() { Speed = 500; }


        public static void SpeedUP(Car c)
        {
            c.Speed = 1000;
        }

        public static void NullIt(Car c)
        {
            c = null;
        }
    }
}

【问题讨论】:

  • 在 C# 中,除非明确使用 refout,否则总是按值传递。

标签: c#


【解决方案1】:

从这里我唯一可以假设的是对象被传递了 值和字段/属性通过引用传递。这是 对吧?

不是真的。 对象的地址是按值传递的。

所以当你这样做时:

Car.SpeedUP(c);

现在您的方法SpeedUp 的参数和调用者中的字段c 都指向内存中的相同位置。因此更改属性有效。

但为了您的电话:

Car.NullIt(c);

您的方法参数c 和调用者的c 都指向同一个位置。但是由于您将 null 分配给参数 c ,现在它没有指向任何内存位置,但原始/调用者的 c 仍然指向相同的内存位置。

考虑以下几点:

当您将参数传递给您的方法时,内存中的两个引用将指向相同的地址,例如:

但是当您分配null 时,它不会更改其他引用。

第一个引用(在调用者中)仍然指向同一个位置,只是方法参数现在指向null

【讨论】:

  • 我明白了,但我没有。如果两个 c 都指向同一个位置,所做的更改不应该反映到同一个位置吗?
  • @allocator,不,您将 null 分配给参数引用。假设对象位于内存地址0x100,那么一开始你的调用者的c 持有0x100,后来当你将它作为参数传递时,现在你的方法的参数持有0x100。但是当您执行methodParameter C = null 时,这将更改方法参数,因此现在它将保留null 而不是0x100。来电者的c 不受影响。
  • 对它的理解好一点,我相信随着我对这门语言的更多经验,我会变得更好。标记为答案!
  • 别担心;这些东西一开始是没有意义的,但最终是完全有意义的。当您说c = ... 时,您并没有改变该位置的事物(您的对象)。您只是在更改 c 所指的内存位置。令人困惑的是,这似乎与intlong 等原语的工作方式不同。但是那些不是指向其他位置的地址类型;他们只是价值观本身。
  • 另外:如果您在某个时候开始使用structs,它们会使用更熟悉的概念,即简单地将内存“放入”变量中。因此,如果您将结构传递给方法,对其进行更改并继续,那么这些更改将不会在之后出现,就像它们对于objectclass 实例一样;就像你使用整数一样。
【解决方案2】:

当您调用NullIt() 时,您将 传递给您的Car 实例。然后将此值更改为 null。但是,该值的原始副本保持不变。传递 int 的值的方式完全相同 - 您可以修改本地副本而不影响“原始”。

如果您将其更改为NullIt(ref Car c),您将传递对引用的引用,因此将其设置为 null 会将引用的原始值设置为 null。最后一部分可能有点让人费解,但它很少需要(如果有的话),你不需要太担心。

【讨论】:

  • 是的,我知道 ref 关键字,我的问题来自我无法真正理解的奇怪行为。
  • @allocator 现在有意义吗?
  • 试图更好地理解它!
【解决方案3】:

据此,我唯一可以假设的是对象是按值传递的,而字段/属性是按引用传递的。是这样吗?

虽然观察到的效果可能看起来像这样,但您描述它的方式有一些不正确的含义:字段/属性不是“通过引用传递”的,因为每个字段/属性都有引用。而是对象的引用是按值传递的。

这就是为什么通过访问对象的任何成员或对象本身,您访问的是您传递给方法的同一个实例,而不是其副本。

但是,变量c 本身是一个引用,并且该引用是按值传递的。这就是为什么在您的方法中,您不能通过分配新值来更改该引用并期望变量 c 本身现在具有新值(在您的情况下为 null)。

【讨论】:

    【解决方案4】:

    这有点令人困惑,因为 reference 这个词被重载了(有两个微妙不同的含义)。

    将类型描述为值类型或引用类型时。这意味着一件事, 该类型对象的状态数据是存储在 stack 中(这是方法可以访问的内存部分),还是存储在称为,然后只有该部分堆的地址存储在堆栈上,允许代码仅间接访问对象。

    当描述参数值是通过值还是通过引用传递给方法时,otoh,它意味着不同的东西。表示是否将参数的实际值[复制并]传递给方法,或者是否传递参数值的内存槽的地址。

    所以你实际上可以在这里有四种组合:

    1. 按值传递值类型 - 值被复制并传递给 方法。该方法无法更改源值。
    2. 按值传递引用类型。引用类型的地址 (在堆上)被复制并传递。该方法不能 更改源变量中的地址,但它可以更改 地址指向的 HEAP 上的数据。
    3. 通过引用传递值类型。该方法获取地址 源对象,(在堆栈上)并且可以更改该源值。
    4. 通过引用传递引用类型。该方法获取地址 包含对象地址的变量(在堆栈上) 本身(在堆上)。该方法可以更改源中的数据 对象,并且还可以更改哪个对象(在堆上)源 可变点)

    【讨论】:

    • 栈和堆位不相关且错误:blogs.msdn.com/b/ericlippert/archive/2010/09/30/…
    • 所以为了做到这一点,假设调用者变量指向 X,它的地址是 Y,当我将它传递给方法时,方法的变量 get 是 X 地址,这就是为什么我可以操纵属性和所以。当我使用 ref 关键字时,Y 地址被传递。对吧?
    • Eric 的博客(如果你明白他在说什么)在技术上是正确的,但是通过为单词引用添加另一个重载(表示地址值),这对于新手或初学者来说更加混乱.出于这里讨论的目的,区分是不必要的。除了这些区别之外,从值类型创建的对象的状态存储在堆栈上(ok,或寄存器上),而引用类型的状态存储在堆上。
    • Eric 所说的引用只是一些内存位置的地址。当我们声明任何变量时,我们在堆栈上分配内存(再次,好的,或者在寄存器中)来保存一些东西......对于引用类型,这个内存将保存一个空值,和/或当/如果用 new 运算符初始化它将保存为保存该实例化对象的状态而分配的内存块的堆上的地址。
    • 对象存储在内存中的位置与值类型/引用类型的区别无关。此外,这是错误的,因为值类型可以但不一定存储在堆上。例如,值类型的类成员不是。另外,从字面上看,该帖子中的第一句话是指References are not addresses
    猜你喜欢
    • 1970-01-01
    • 2015-04-24
    • 2018-12-22
    • 2016-09-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-08-23
    • 1970-01-01
    相关资源
    最近更新 更多