C#按值传递与按引用传递 [英] C# pass by value vs. pass by reference

查看:109
本文介绍了C#按值传递与按引用传递的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

请考虑以下代码(我已经有目的地将MyPoint编写为该示例的引用类型)

Consider the following code (I have purposefully written MyPoint to be a reference type for this example)

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

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

It is universally acknowledged (in C# at least) that when you pass by reference, the method contains a reference to the object being manipulated, whereas when you pass by value, the method copies the value being manipulated, thus the value in global scope is not affected.

示例:

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进行的相同操作将a替换为b.

Here is my problem; MyPoint is a reference type, thus I would expect the same operation on Point to replace a with b in global scope.

示例:

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指向内存中的同一引用...有人可以澄清我哪里出错了吗?

I expected a and b to point to the same reference in memory...can someone please clarify where I have gone wrong?

推荐答案

回复:OP的断言

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

TL; DR

还有更多的东西.除非您使用 ref out 关键字传递变量,否则C#会通过 value 将变量传递给方法,而不管变量是否为值类型引用类型.

There's more to it than that. Unless you pass variables with the ref or out keywords, C# passes variables to methods by value, irrespective of whether the variable is a value type or a reference type.

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

  • If passed by reference, then the called function may change the variable's address (i.e. change the original calling function's variable's assignment).

如果通过 value 传递变量:

  • 如果被调用函数重新分配了变量,则此更改仅在被调用函数本地发生,并且不会影响调用函数中的原始变量
  • 但是,如果调用的函数对变量的字段或属性进行了更改,则将取决于变量是 value 类型还是 reference 类型.以确定调用函数是否将观察对该变量所做的更改.
  • if the called function re-assigns the variable, this change is local to the called function only, and will not affect the original variable in the calling function
  • however, if changes are made to the variable's fields or properties by the called function, it will depend on whether the variable is a value type or a reference type in order to determine whether the calling function will observe the changes made to this variable.

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

Since this is all rather complicated, I would recommend avoiding passing by reference if possible (instead, use a composite class or struct as a return type, or use Tuples)

此外,当传递引用类型时,可以通过不更改(变异)传递给方法的对象的字段和属性来避免很多错误(例如,使用C#的不变属性来防止更改属性,并努力在构建过程中仅分配一次属性.)

Also, when passing reference types around, a lot of bugs can be avoided by not changing (mutating) fields and properties of an object passed into a method (for example, use C#'s immutable properties to prevent changes to properties, and strive to assign properties only once, during construction).

详细信息

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

The problem is that there are two distinct concepts:

  • 值类型(例如int)与引用类型(例如字符串或自定义类)
  • 按值传递(默认行为)与按引用传递(ref,out)
  • Value Types (e.g. int) vs Reference Types (e.g. string, or custom classes)
  • Passing by Value (default behaviour) vs Passing by Reference(ref, out)

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

Unless you explicitly pass (any) variable by reference, by using the out or ref keywords, parameters are passed by value in C#, irrespective of whether the variable is a value type or reference type.

当按值传递 value 类型(例如intfloatDateTime之类的结构)时(即不使用outref)时,被调用函数将获得一个完整值类型的副本(通过堆栈).

When passing value types (such as int, float or structs like DateTime) by value (i.e. without out or ref), the called function gets a copy of the entire value type (via the stack).

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

Any change to the value type, and any changes to any properties / fields of the copy will be lost when the called function is exited.

但是,当value传递 reference 类型(例如,自定义类,例如MyPoint类)时,是reference到相同的共享对象实例,该实例被复制并传递在堆栈上.

However, when passing reference types (e.g. custom classes like your MyPoint class) by value, it is the reference to the same, shared object instance which is copied and passed on the stack.

这意味着:

  • 如果传递的对象具有可变的(可设置的)字段和属性,则对共享对象的这些字段或属性的任何更改都是永久性的(即,对xy的任何更改都可以由观察该对象的任何人看到)
  • 但是,在方法调用期间,引用本身仍将被复制(按值传递),因此,如果重新分配了参数变量,则此更改仅对引用的本地副本进行,因此更改不会被查看呼叫者,召集者. 这就是为什么您的代码无法按预期运行的原因
  • If the passed object has mutable (settable) fields and properties, any changes to those fields or properties of the shared object are permanent (i.e. any changes to x or y are seen by anyone observing the object)
  • However, during method calls, the reference itself is still copied (passed by value), so if the parameter variable is reassigned, this change is made only to the local copy of the reference, so the change will not be seen by the caller. This is why your code doesn't work as expected

这里会发生什么:

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.此重新分配仅在此功能本地进行-范围离开此功能后,重新分配将丢失.

for reference types T, means that the local variable (stack) reference to the object a is reassigned to the local stack reference b. This reassign is local to this function only - as soon as scope leaves this function, the re-assignment is lost.

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

If you really want to replace the caller's references, you'll need to change the signature like so:

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
}

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

This changes the call to call by reference - in effect we are passing the address of the caller's variable to the function, which then allows the called method to alter the calling method's variable.

但是,如今:

  • 通过引用传递是通常被认为是一个坏主意-相反,我们应该在返回值中传递返回数据,如果要返回多个变量,请使用Tuple或包含所有此类返回变量的自定义classstruct.
  • 在被调用方法中更改(变异")共享值(甚至是引用)变量是不受欢迎的,尤其是在函数编程社区中,因为这可能会导致棘手的错误,尤其是在使用多个线程时.相反,应优先考虑不可变变量,或者如果需要进行突变,则考虑更改变量的(可能较深的)副本.您可能会发现有关纯函数"和常量正确性"的主题,这对进一步阅读很有帮助.
  • Passing by reference is generally regarded as a bad idea - instead, we should either pass return data in the return value, and if there is more than one variable to be returned, then use a Tuple or a custom class or struct which contains all such return variables.
  • Changing ('mutating') a shared value (and even reference) variable in a called method is frowned upon, especially by the Functional Programming community, as this can lead to tricky bugs, especially when using multiple threads. Instead, give preference to immutable variables, or if mutation is required, then consider changing a (potentially deep) copy of the variable. You might find topics around 'pure functions' and 'const correctness' interesting further reading.

修改

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

These two diagrams may help with the explanation.

按值传递(引用类型):

在第一个实例(Replace<T>(T a,T b))中,ab按值传递.对于引用类型

In your first instance (Replace<T>(T a,T b)), a and b are passed by value. For reference types, this means the references are copied onto the stack and passed to the called function.

  1. 您的初始代码(我称为main)在托管堆上分配了两个MyPoint对象(我将这些称为point1point2),然后分配了两个局部变量引用b分别引用点(浅蓝色箭头):
  1. Your initial code (I've called this main) allocates two MyPoint objects on the managed heap (I've called these point1 and point2), and then assigns two local variable references a and b, to reference the points, respectively (the light blue arrows):

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

  1. Replace<Point>(a, b)的调用然后将两个引用的副本推入堆栈(红色箭头).方法Replace将它们视为也分别称为ab的两个参数,它们仍分别指向point1point2(橙色箭头).

  1. The call to Replace<Point>(a, b) then pushes a copy of the two references onto the stack (the red arrows). Method Replace sees these as the two parameters also named a and b, which still point to point1 and point2, respectively (the orange arrows).

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

The assignment, a = b; then changes the Replace methods' a local variable such that a now points to the same object as referenced by b (i.e. point2). However, note that this change is only to Replace's local (stack) variables, and this change will only affect subsequent code in Replace (the dark blue line). It does NOT affect the calling function's variable references in any way, NOR does this change the point1 and point2 objects on the heap at all.

通过引用传递:

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

If however we we change the call to Replace<T>(ref T a, T b) and then change main to pass a by reference, i.e. Replace(ref a, b):

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

  1. As before, two point objects allocated on the heap.

现在,当调用Replace(ref a, b)时,尽管在调用过程中仍复制了main的引用b(指向point2),但是现在被引用传递了,这意味着将main的a变量的地址"传递给Replace.

Now, when Replace(ref a, b) is called, while mains reference b (pointing to point2) is still copied during the call, a is now passed by reference, meaning that the "address" to main's a variable is passed to Replace.

现在进行分配a = b时...

Now when the assignment a = b is made ...

它是调用函数,maina变量引用,现在已更新为引用point2.现在,mainReplace都可以看到重新分配给a所做的更改.现在没有对point1

It is the the calling function, main's a variable reference which is now updated to reference point2. The change made by the re-assignment to a is now seen by both main and Replace. There are now no references to point1

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

在以上两种情况下,实际上都没有对堆对象point1point2进行任何更改,只是传递和重新分配了局部变量引用.

In both scenarios above, no changes were actually made to the heap objects, point1 and point2, it was only local variable references which were passed and re-assigned.

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

However, if any changes were actually made to the heap objects point1 and point2, then all variable references to these objects would see these changes.

例如,

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时,所有对point1point2的引用,包括main's变量ab,当它们下次读取变量时将看到"更改.点的xy的值.您还将注意到,变量ab仍按值传递给DoSomething.

Now, when execution returns to main, all references to point1 and point2, including main's variables a and b, which will now 'see' the changes when they next read the values for x and y of the points. You will also note that the variables a and b were still passed by value to DoSomething.

更改值类型只会影响本地副本

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

Value types (primitives like System.Int32, System.Double) and structs (like System.DateTime, or your own structs) are allocated on the stack, not the heap, and are copied verbatim onto the stack when passed into a call. This leads to a major difference in behaviour, since changes made by the called function to a value type field or property will only be observed locally by the called function, because it only will be mutating the local copy of the value type.

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

e.g. Consider the following code with an instance of the mutable struct, 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);

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

The above can be quite confusing and highlights why it is good practice to create your own custom structs as immutable.

ref关键字的工作原理类似,允许值类型变量通过引用传递,即调用方的值类型变量的地址"被传递到堆栈上,现在可以直接分配调用方的已分配变量

The ref keyword works similarly to allow value type variables to be passed by reference, viz that the 'address' of the caller's value type variable is passed onto the stack, and assignment of the caller's assigned variable is now directly possible.

这篇关于C#按值传递与按引用传递的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆