【问题标题】:Reference Type passed by ref and without ref由 ref 传递且不带 ref 的引用类型
【发布时间】:2019-04-21 11:51:26
【问题描述】:

我在以不同方式调用 4 种方法时得到不同的结果:

static void Main(string[] args)
{
   var emp = new Employee { Name = "ABC" };

   ChangeName1(emp);
   Console.WriteLine(emp.Name); //XYZ

   ChangeName2(ref emp);
   Console.WriteLine(emp.Name); //XYZ

   ChangeToNull1(emp);
   Console.WriteLine(emp.Name); // XYZ

   ChangeToNull2(ref emp);
   Console.WriteLine(emp.Name); // Null Reference Exception

   Console.ReadLine();
}

static void ChangeName1(Employee e)
{
   e.Name = "XYZ";
}
static void ChangeName2(ref Employee e)
{
   e.Name = "XYZ";
}
static void ChangeToNull1(Employee e)
{
   e = null;
}
static void ChangeToNull2(ref Employee e)
{
   e = null;
}

正如你看到的前 2 个方法,我正在更改 Name 属性的值,从该方法返回后,原始对象属性被更改。

但是当设置对象为null时,ChangeToNull1方法不会改变原来的对象但是ChangeToNull2方法会。

所以我的问题是:

1. 为什么 C# 会这样?

2. C# 在传递给方法时是否会复制原始对象?

3. 如果是,那么它如何改变原始对象属性,如Name,为什么不改变原始对象?

4. c# 通过ref 传递时是否复制原始对象?

【问题讨论】:

  • 问题是您正在通过按值调用和按引用调用来传递对内存中对象的引用。东西变得很奇怪。另请注意:不要尝试使用 String 来学习类行为!它的行为与正常的引用类型不同。它的设计更像是像 lint 这样的原始值类型。
  • stackoverflow.com/questions/30585483/…也许这个解释对你有帮助。
  • @Christopher 你能解释一下string 的行为何时不同于其他类型吗?我还没有遇到过这种情况

标签: c#


【解决方案1】:

之所以如此,是因为 C# 将指针值的副本传递给了引用类型。这有点拗口,所以这可能更能说明问题:

当你写的时候:

var emp = new Employee { Name = "ABC" }; 

您正在创建Employee 的实例,并将指向该对象的指针 存储在变量emp 中。 假设emp 的内存位置是0x000001。而那个(对象的位置)的0x0000AA

当你打电话时:

ChangeName1(emp);

您正在传递值0x0000AA。在方法ChangeName1 中,e 的值是0x0000AA,但它的 位置不是0x000001。它存储在内存中的不同位置。

但是,当您调用时:

ChangeName2(ref emp);

您正在传递emp 的内存位置,即0x000001。所以在这个方法中,更新e也会更新emp


要解决成员问题 - 如上所述,您不是在复制对象。 ChangeName1ChangeName2 都指向同一个对象。它们都引用存储在0x0000AA 的对象。


如需进一步阅读,请参阅When is a C# value/object copied and when is its reference copied?

【讨论】:

    【解决方案2】:
    static void ChangeName1(Employee e)
    {
       e.Name = "XYZ";
    }
    

    这里,新的指针“e”指向同一个对象“emp”——所以变化反映了。

    static void ChangeName2(ref Employee e)
    {
       e.Name = "XYZ";
    }
    

    这里“emp”作为引用传递——只是它的名字是 e。 (这样容易理解)

    static void ChangeToNull1(Employee e)
    {
       e = null;
    }
    

    这里,新指针“e”指向同一个对象“emp” - 当您设置 e= Null 时。新指针为空。不是原始对象。

    static void ChangeToNull2(ref Employee e)
    {
       e = null;
    }
    

    我想你现在已经明白发生了什么了。

    【讨论】:

      【解决方案3】:

      什么是“emp”,是一个引用变量。指向引用类型实例的变量。

      引用变量的工作方式类似于指针的工作方式。裸指针是一种非常基本的编程工具,但处理它们非常危险。因此 .NET 团队选择不强制您默认处理它们。但由于它们如此重要,因此必须发明很多东西来替代它们(引用和委托只是两个常见的)。

      内存中的对象和你对它的引用(数量)是完全不真实的。内存中的对象可以有 0、1 或多个引用指向它。没有任何引用的对象将被 GC 收集,只要它选择下一个运行。

      设置对 null 的引用不会强制收集,它只是使收集成为可能。一个高概率,给定更多的运行时间。但绝不是保证。

      【讨论】:

        【解决方案4】:

        在 C# 中,有两类对象:Value-Types 和 Reference-Types。

        值类型为structs 和enums,例如int (System.Int32)。这些类型在传递时总是被复制。如果在方法中更改int,调用者内部的变量不会改变。

        您说的是引用类型——基本上是类、数组和接口。

        在引用类型中,例如string (System.String),有两个部分:对象和指针。例如,让我们看看您的Employee。假设我声明了一个名为e1 类型为Employee 的变量,并将其分配给它的名称"abc"

        Employee e1 = new Employee { Name = "abc" };
        

        现在,内存中有一个员工对象(堆,因为引用类型几乎总是在堆中分配,除了stackalloc),其中包含Name="abc"。 (在堆栈中)还有一个指向该对象的指针。假设我们有这个记忆图像:

        0x123 - 员工对象 0x45F - 变量 `e1` - 指向员工的指针 |--------------------------| |-----------------| |名称 = "abc" | | 0x123 | |--------------------------| |-----------------|

        当您在没有ref 的情况下传递它时,e1 - 0x123 的值会被复制,但不会复制员工!因此,如果您更改其名称,原来的员工被更改!但是,如果您更改指针,则不会发生任何事情,因为指针 e1 只是复制了

        当您使用ref 传递时,指针的地址 被复制 - 0x45F。因此,如果您更改参数,它更改e1,因为它不是复制的,而是它的地址。

        编辑:

        如果我将一个引用类型变量分配给另一个变量,例如:

        var e1 = new Employee { Name = "abc" };
        Employee e2 = employee;
        

        那么,e2e1 相同——它也是一个指针,指向同一个地址。如果我们使用之前的内存映像,现在在地址0x4AC 中有一个名为e2 的变量,其中还包含对象的地址0x123。所以,如果我们改变e2.Name

        e2.Name = "new";
        

        那么,e1.Name 现在也是 "new"。 关于引用类型的最后一个重要事实是,比较 (==) 引用类型(我说的是没有重载运算符 ==),不会检查它们是否包含相同的值,但如果它们指向同一个对象。我们来看一个例子:

        var e1 = new Employee { Name = "abc" };
        Employee e2 = e1;
        var e3 = new Employee { Name = "abc" };
        var e4 = new Employee { Name = "123" };
        
        Console.WriteLine(e1 == e4); // false
        Console.WriteLine(e1 == e3); // false, since they don't point to the same object, they just contain the same values
        Console.WriteLine(e1 == e2); // true, since they point to the same object
        

        一些关于字符串的方法:

        1. 虽然System.String 类是引用类型,但它重载了operator ==(),所以比较两个字符串会得到正确的结果。
        2. 编译器通常会优化字符串,所以如果string s1 = "abc";string s2 = "abc";s1s2可以指向同一个地址(为了节省内存)。这是可能的,因为字符串是不可变的 - 例如,如果您在字符串上调用 Replace(),它将创建一个新字符串。所以你不应该知道这一点(但如果你编写了不安全的代码,这很重要)。

        【讨论】:

        • 我可以将 object 设置为 null 吗?因为如果我将引用设置为null,它会变为null,但它不会将实例更改为null。
        • @BimalDas 是的,当你设置一个对 null 的引用时,你并没有从内存中删除对象,但是现在,没有更多的引用了,所以,在下一次GC会运行,它会发现这个对象没有引用,它会删除它(这是一个非常复杂的过程,我简单解释过,但是直到对象被销毁,这是一些站。)
        猜你喜欢
        • 1970-01-01
        • 2010-11-25
        • 1970-01-01
        • 2011-11-02
        • 1970-01-01
        • 2011-03-25
        • 1970-01-01
        • 1970-01-01
        • 2018-06-26
        相关资源
        最近更新 更多