C#按值传递而不是按引用传递

ser*_*0ne 9 c# value-type reference-type pass-by-reference pass-by-value

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

public class MyPoint
{
    public int x;
    public int y;
}
Run Code Online (Sandbox Code Playgroud)

它是普遍公认的(至少在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.
Run Code Online (Sandbox Code Playgroud)

这是我的问题; 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!?
Run Code Online (Sandbox Code Playgroud)

我期待ab在记忆中指出相同的参考...有人可以澄清我哪里出错了吗?

Stu*_*tLC 25

Re:OP的断言

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

TL; DR

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

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

  • 如果变量按传递:

    • 如果被调用函数重新赋值变量,则此更改仅对被调用函数是局部变量,并且不会影响调用函数中的原始变量
    • 但是,如果被调用函数对变量的字段或属性进行了更改,则将取决于变量是类型还是引用类型,以确定调用函数是否"看到"对此变量所做的更改.

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

如果可以的话,可以通过永远不改变(变异)传递给方法的对象的字段和属性来避免许多错误.不可变的属性将阻止变化.

详细地

问题是有两个不同的概念:

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

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

传递类型(例如int,float或类似结构DateTime)时,被调用函数获取整个值类型副本(通过堆栈).

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

但是,对于引用类型(例如类的自定义类MyPoint),它是reference在堆栈上复制和传递的相同的共享对象实例.

这意味着:

  • 对共享对象的字段或属性的任何更改都是永久性的(即对xor的任何更改y)
  • 但是,引用本身仍然被复制(通过值传递),因此对引用副本的任何更改都将丢失.这就是您的代码无法按预期工作的原因

这里发生了什么:

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

对于引用类型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
}
Run Code Online (Sandbox Code Playgroud)

这改变了对引用调用的调用 - 实际上我们将调用者的变量的地址传递给函数,然后允许被调用的方法改变调用方法的变量.

但是,现在:

  • 通过引用传递通常被认为是一个坏主意 - 相反,我们应该在返回值中传递返回数据,如果要返回多个变量,则使用a Tuple或custom classstruct包含所有这些返回变量.
  • 改变('变异')被调用方法中的共享值(甚至引用)变量是不受欢迎的,尤其是功能编程社区,因为这会导致棘手的错误,尤其是在使用多个线程时.相反,优先考虑不可变变量,或者如果需要变异,则考虑更改变量的(可能很深的)副本.您可能会发现有关"纯函数"和"常量正确性"的主题有趣的进一步阅读.

编辑

这两个图可能有助于解释.

按值传递(参考类型):

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

在此输入图像描述

  1. 初始代码(我称为此main)分配两个MyPoint在托管堆上对象(我称这些point1point2),然后分配两个本地变量引用ab,以引用点,分别(淡蓝色箭头):

MyPoint a = new MyPoint { x = 1, y = 2 }; // point1
MyPoint b = new MyPoint { x = 3, y = 4 }; // point2
Run Code Online (Sandbox Code Playgroud)
  1. 然后调用Replace<Point>(a, b)将两个引用的副本推送到堆栈(红色箭头).方法Replace认为这些作为两个参数也被命名ab,这仍然指向point1point2,分别为(橙色箭头).

  2. 赋值,a = b;然后更改Replace方法的a局部变量,使得a现在指向与b(ie point2)引用的同一对象.但请注意,此更改仅适用于Replace的本地(堆栈)变量,此更改仅会影响Replace(深蓝色线条)中的后续代码.它不会以任何方式影响调用函数的变量引用,NOR会改变堆上的point1point2对象.

通过引用传递:

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

在此输入图像描述

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

  2. 现在,当Replace(ref a, b)调用时,在调用期间仍然复制mains引用b(指向point2),a现在通过引用传递,这意味着传递给main的a变量的"地址" Replace.

  3. 现在当a = b作出任务时......

  4. 它是调用函数maina变量引用,现在更新为引用point2.通过重新分配进行了更改,以a双方现在看到的mainReplace.现在没有提及point1

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

在上面的两个场景中,实际上没有对堆对象进行任何更改,point1并且point2只有传递和重新分配的局部变量引用.

但是,如果任何变化实际上对堆中的对象做point1point2,然后对这些对象的所有变量的引用会看到这些变化.

所以,例如:

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;
}
Run Code Online (Sandbox Code Playgroud)

现在,当执行返回到main,所有参考文献point1point2,包括main's变量ab,现在将"看见"的变化时,他们接下来的读取值xy观点.您还会注意到,变量ab仍然按值传递给DoSomething.

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

值类型(原语喜欢System.Int32,System.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);
Run Code Online (Sandbox Code Playgroud)

上面的内容可能非常令人困惑,并强调了为什么将自己的自定义结构创建为不可变的优良做法.

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

  • 作为初学者,我需要多读几遍才能理解。谢谢你的图表。 (3认同)

Kev*_*lia 5

C#实际上是按值传递的.你得到了它通过引用传递的幻觉,因为当你传递一个引用类型时,你得到一个引用的副本(引用是通过值传递的).但是,由于您的替换方法正在用另一个引用替换该引用副本,因此它实际上什么都不做(复制的引用立即超出范围).您实际上可以通过添加ref关键字来引用:

void Replace<T>(ref T a, T b)
{
    a = b;
}
Run Code Online (Sandbox Code Playgroud)

这将获得你想要的结果,但在实践中有点奇怪.


归档时间:

查看次数:

7759 次

最近记录:

6 年,6 月 前