C#按值传递与按引用传递

马修·莱顿

考虑以下代码(我故意将MyPoint编写为该示例的引用类型)

public class MyPoint
{
    public int x;
    public int y;
}

普遍公认(至少在C#中),当您通过引用传递时,该方法包含对要操作的对象的引用,而当您通过值传递时,该方法将复制要操作的值,因此全局范围内的值是不受影响。

例子:

void Replace<T>(T a, T b)
{
    a = b;
}

int a = 1;
int b = 2;

Replace<int>(a, b);

// a and b remain unaffected in global scope since a and b are value types.

这是我的问题;MyPoint是引用类型,因此我希望在全局范围内将相同的操作Point替换ab

例子:

MyPoint a = new MyPoint { x = 1, y = 2 };
MyPoint b = new MyPoint { x = 3, y = 4 };

Replace<MyPoint>(a, b);

// a and b remain unaffected in global scope since a and b...ummm!?

我期望ab指向内存中的同一参考...有人可以澄清我哪里出了问题吗?

斯图尔特

回复:OP的断言

普遍公认(至少在C#中),当您通过引用传递时,该方法包含对要操作的对象的引用,而当您通过值传递时,该方法将复制要操作的值...

TL; DR

不仅如此。除非您使用refout关键字传递变量,否则C#会按将变量传递给方法,而不管变量是值类型还是引用类型

  • 如果通过引用传递,则被调用函数可以更改变量的地址(即,更改原始调用函数的变量的赋值)。

  • 如果通过传递变量

    • 如果被调用函数重新分配了变量,则此更改仅在被调用函数本地发生,并且不会影响调用函数中的原始变量
    • 但是,如果调用的函数对变量的字段或属性进行了更改,则将取决于该变量是类型还是引用类型,以确定调用函数是否会观察对该变量所做的更改。

由于这一切都相当复杂,因此我建议尽可能避免通过引用传递(相反,使用复合类或结构作为返回类型,或使用元组)

另外,在传递引用类型时,可以通过不更改(更改)传递给方法的对象的字段和属性来避免很多错误(例如,使用C#的不可变属性来防止对属性进行更改,并努力分配属性施工期间仅一次)。

详细地

问题在于有两个不同的概念:

  • 值类型(例如int)与引用类型(例如字符串或自定义类)
  • 按值传递(默认行为)与按引用传递(ref,out)

除非您使用outref关键字通过引用显式传递(任何)变量,否则无论变量是值类型还是引用类型,参数都将按C#中传递

当按传递类型(例如intfloat或结构之类DateTime)(即不带outref)时,被调用的函数将获得整个值类型副本(通过堆栈)。

退出调用的函数时,对值类型的任何更改以及对副本的任何属性/字段的任何更改都将丢失。

但是,当通过传递引用类型(例如,自定义类,如您的MyPoint类)时value,将它reference复制到同一共享对象实例并在堆栈上传递。

这意味着:

  • 如果传递的对象具有可变的(可设置的)字段和属性,则对共享对象的这些字段或属性的任何更改都是永久性的(即,对任何对象的观察xy任何人看到的更改
  • 但是,在方法调用期间,引用本身仍将被复制(传递值),因此,如果重新分配参数变量,则此更改仅对引用的本地副本进行,因此调用者将看不到该更改。这就是为什么您的代码无法按预期工作的原因

这里会发生什么:

void Replace<T>(T a, T b) // Both a and b are passed by value
{
    a = b;  // reassignment is localized to method `Replace`
}

对于引用类型T,意味着将对象的局部变量(堆栈)引用a重新分配给局部堆栈引用b此重新分配仅在此功能本地进行-范围离开此功能后,重新分配将丢失。

如果您确实要替换调用方的引用,则需要像下面这样更改签名:

void Replace<T>(ref T a, T b) // a is passed by reference
{
    a = b;   // a is reassigned, and is also visible to the calling function
}

这将调用改为按引用进行调用-实际上,我们正在将调用者变量的地址传递给函数,该函数随后允许被调用方法更改调用方法的变量。

但是,如今:

  • 通过引用传递通常被认为是一个坏主意-相反,我们应该在返回值中传递返回数据,并且如果有多个要返回的变量,则使用Tupleclassstruct包含所有此类返回变量的自定义
  • 在调用的方法中更改(“变异”)共享值(甚至是引用)变量是令人讨厌的,尤其是在函数编程社区中,因为这可能会导致棘手的错误,尤其是在使用多个线程时。相反,应优先考虑不可变的变量,或者如果需要进行突变,则考虑更改变量的(可能很深的)副本。您可能会发现有关“纯函数”和“常量正确性”的主题,这些有趣的内容可以进一步阅读。

编辑

这两个图可能有助于说明。

按值传递(引用类型):

在您的第一个实例(Replace<T>(T a,T b))中,a并按b值传递。对于引用类型这意味着将引用复制到堆栈上并传递给调用的函数。

在此处输入图片说明

  1. 您的初始代码(我称为this mainMyPoint在托管堆上分配了两个对象(我将它们称为point1point2),然后分配了两个局部变量引用ab分别引用点(浅蓝色箭头):

MyPoint a = new MyPoint { x = 1, y = 2 }; // point1
MyPoint b = new MyPoint { x = 3, y = 4 }; // point2
  1. Replace<Point>(a, b)然后,对的调用将两个引用的副本推入堆栈(红色箭头)。方法Replace将它们视为也称为a的两个参数b,它们仍分别指向point1point2(橙色箭头)。

  2. 然后,赋值a = b;将更改Replace方法的a局部变量,从而a现在指向与b(即point2引用的对象相同的对象但是,请注意,此更改仅适用于Replace的本地(堆栈)变量,并且此更改仅会影响Replace(深蓝色线)中的后续代码它不会以任何方式影响调用函数的变量引用,NOR完全不会更改堆上point1andpoint2对象。

通过参考传递:

但是,如果我们将调用更改为Replace<T>(ref T a, T b),然后更改main为通过a引用传递,即Replace(ref a, b)

在此处输入图片说明

  1. 和以前一样,在堆上分配了两个点对象。

  2. 现在,在Replace(ref a, b)调用when的同时,在调用过程中仍复制mains的引用b(指向point2),a现在通过引用传递,这意味着maina变量的“地址”传递给Replace

  3. 现在完成分配后a = b...

  4. 它是调用函数,它maina变量引用现在已更新为reference point2a现在,main都可以看到重新分配对所做的更改Replace现在没有参考point1

通过引用该对象的所有代码可以看到对(堆分配的)对象实例的更改

在上述两种情况下,实际上都没有对堆对象进行任何更改,point1并且point2,只有局部变量引用被传递和重新分配。

但是,如果实际上对堆对象point1进行了任何更改point2,则对这些对象的所有变量引用都将看到这些更改。

因此,例如:

void main()
{
   MyPoint a = new MyPoint { x = 1, y = 2 }; // point1
   MyPoint b = new MyPoint { x = 3, y = 4 }; // point2

   // Passed by value, but the properties x and y are being changed
   DoSomething(a, b);

   // a and b have been changed!
   Assert.AreEqual(53, a.x);
   Assert.AreEqual(21, b.y);
}

public void DoSomething(MyPoint a, MyPoint b)
{
   a.x = 53;
   b.y = 21;
}

现在,当执行返回到时main,对point1和的所有引用point2(包括main's变量a和)b现在将在下次读取xy的值时“查看”更改您还将注意到变量ab仍然按值传递给DoSomething

值类型的更改仅影响本地副本

值类型(原始类型如System.Int32System.Double)和结构(例如System.DateTime或您自己的结构)是在堆栈上分配的,而不是堆上的,并在传递给调用时逐字复制到堆栈上。这将导致行为上的重大差异,因为被调用函数对值类型字段或属性所做的更改将仅由被调用函数在本地观察到,因为这只会使值类型的本地副本发生变化。

例如,考虑以下代码以及可变结构的实例, System.Drawing.Rectangle

public void SomeFunc(System.Drawing.Rectangle aRectangle)
{
    // Only the local SomeFunc copy of aRectangle is changed:
    aRectangle.X = 99;
    // Passes - the changes last for the scope of the copied variable
    Assert.AreEqual(99, aRectangle.X);
}  // The copy aRectangle will be lost when the stack is popped.

// Which when called:
var myRectangle = new System.Drawing.Rectangle(10, 10, 20, 20);
// A copy of `myRectangle` is passed on the stack
SomeFunc(myRectangle);
// Test passes - the caller's struct has NOT been modified
Assert.AreEqual(10, myRectangle.X);

上面的内容可能会让人很困惑,并强调了为什么将自己的自定义结构创建为不可变是一种好习惯。

ref关键字的工作方式类似于允许值类型变量为通过引用而通过,即,该呼叫方的值的变量类型“地址”被传递到堆栈,和调用者的分配的变量的赋值是现在直接可能的。

本文收集自互联网,转载请注明来源。

如有侵权,请联系[email protected] 删除。

编辑于
0

我来说两句

0条评论
登录后参与评论

相关文章

来自分类Dev

c#对象按引用传递或按值传递

来自分类Dev

c#对象按引用传递或按值传递

来自分类Dev

Objective-C是按值传递还是按引用传递?

来自分类Dev

Python,Java,C:按引用传递或按值传递

来自分类Dev

C按值传递数组vs按引用传递数组

来自分类Dev

哪个更快?按引用传递与按值传递C ++

来自分类Dev

对在C#中按引用传递和按值传递感到困惑

来自分类Dev

在C#中,我应该按值传递参数并返回相同的变量,还是按引用传递?

来自分类Dev

C:何时按值返回或传递引用

来自分类Dev

C ++是否按值或引用传递对象?

来自分类Dev

C# 的 Queue.dequeue() 是按引用还是按值传递?

来自分类Dev

C ++中的函数重载按值或按引用传递参数

来自分类Dev

按值或引用传递容器

来自分类Dev

C#是按值传递对象吗?

来自分类Dev

C#是按值传递对象吗?

来自分类Dev

c#按值传递参数

来自分类Dev

为什么c#对象的行为一次类似于按值传递,而一次按引用传递?

来自分类Dev

在Javascript中按值传递和按引用传递

来自分类常见问题

python pandas dataframe,是按值传递还是按引用传递

来自分类Dev

在Android Adapter中按值传递和按引用传递?

来自分类Dev

我是否正确理解C中的按值传递和按引用传递?

来自分类Dev

C ++中按引用传递和按值传递之间过载歧义的可行解决方案

来自分类Dev

在C中按指针传递/按引用传递

来自分类Dev

const引用何时比C ++ 11中的按值传递更好?

来自分类Dev

通过引用而不是按值传递OpenCV C ++

来自分类Dev

SystemVerilog数组是按值或引用传递的吗?

来自分类Dev

数组元素是否也按值或引用传递?

来自分类Dev

SystemVerilog数组是按值或引用传递的吗?

来自分类Dev

有人可以解释一下C#中的“按值传递”和“按引用传递”是什么意思吗?

Related 相关文章

  1. 1

    c#对象按引用传递或按值传递

  2. 2

    c#对象按引用传递或按值传递

  3. 3

    Objective-C是按值传递还是按引用传递?

  4. 4

    Python,Java,C:按引用传递或按值传递

  5. 5

    C按值传递数组vs按引用传递数组

  6. 6

    哪个更快?按引用传递与按值传递C ++

  7. 7

    对在C#中按引用传递和按值传递感到困惑

  8. 8

    在C#中,我应该按值传递参数并返回相同的变量,还是按引用传递?

  9. 9

    C:何时按值返回或传递引用

  10. 10

    C ++是否按值或引用传递对象?

  11. 11

    C# 的 Queue.dequeue() 是按引用还是按值传递?

  12. 12

    C ++中的函数重载按值或按引用传递参数

  13. 13

    按值或引用传递容器

  14. 14

    C#是按值传递对象吗?

  15. 15

    C#是按值传递对象吗?

  16. 16

    c#按值传递参数

  17. 17

    为什么c#对象的行为一次类似于按值传递,而一次按引用传递?

  18. 18

    在Javascript中按值传递和按引用传递

  19. 19

    python pandas dataframe,是按值传递还是按引用传递

  20. 20

    在Android Adapter中按值传递和按引用传递?

  21. 21

    我是否正确理解C中的按值传递和按引用传递?

  22. 22

    C ++中按引用传递和按值传递之间过载歧义的可行解决方案

  23. 23

    在C中按指针传递/按引用传递

  24. 24

    const引用何时比C ++ 11中的按值传递更好?

  25. 25

    通过引用而不是按值传递OpenCV C ++

  26. 26

    SystemVerilog数组是按值或引用传递的吗?

  27. 27

    数组元素是否也按值或引用传递?

  28. 28

    SystemVerilog数组是按值或引用传递的吗?

  29. 29

    有人可以解释一下C#中的“按值传递”和“按引用传递”是什么意思吗?

热门标签

归档