在C#中,当一个变量经过一个function/method时,原来的变量会发生变化吗?

In C#, when a variable passes through a function/method, will the original variable change?

我对函数如何更改传递给它的变量感到困惑。例如,如果我创建了一个变量 t = 1,并通过向它添加 2 来传递一个函数,在函数内部 t 是 3,但在 Main 函数中 t 仍然是 1:

static void Main(string[] args)
{
    int t = 1;
    addTwo(t);
    Console.WriteLine(t); 
    ## t=1
    Console.ReadLine();
}
static void addTwo(int t)
{
    t+=2;
    Console.WriteLine("inside function {0}",t); 
    ## t=3

函数中t的值为3,但在Main中该值为1

但是,如果我创建了一个值为 {2,3,4,5,6} 的数组“happiness”并传递了一个函数“SunIsShining”,它将每个值增加 2。之后,我认为数组 happiness应该仍然是 {2,3,4,5,6}。然而它变成了{4,5,6,7,8}。

static void Main(string[] args)
{
    int[] happiness = { 2, 3, 4, 5, 6 };
    SunIsShining(happiness);
    ## happiness = { 4, 5, 6, 7, 8 }

    foreach (int y in happiness)
    {
        Console.WriteLine(y);
    }
    Console.ReadLine();
}
static void SunIsShining(int[] x)
{
    for (int i = 0; i < x.Length; i++)
        x[i] += 2;
}

谁能帮我了解一下原因?谢谢!

因为

  • int[] 是一个引用类型,可能会将对象引用传递给函数,因此您修改同一数组中的值。

  • int是一个值类型,在传给函数之前会克隆值,所以你修改的值不是t 来自 Main 函数。

我们可以通过此示例代码通过ReferenceEquals方法来证明,该方法将比较对象引用是否与下面相同,假设我们可以看到addTwo是returnfalse,但 SunIsShining return 正确。

static int t1;
static int[] happiness;
static void Main(string[] args)
{
    t1 = 1;
    happiness = new int[]{ 2, 3, 4, 5, 6 };
    
    addTwo(t1);
    SunIsShining(happiness);
    
    Console.ReadLine();
}
static void addTwo(int t)
{   
    t+=2;
    Console.WriteLine("Is same object by value?" + object.ReferenceEquals(t1,t)); 
}

static void SunIsShining(int[] x)
{
    Console.WriteLine("Is same object by refer?" + object.ReferenceEquals(happiness,x)); 
}

c# online

更多信息我们可以看到

value-types

reference-types

passing-parameters

变量传递分为值传递和地址传递。
值传递不能改变之前的值,地址传递是传递地址,所以可以改变之前的值。
例如,第一个是通过变量名传递的。 T存储了一个int,所以它传递了一个数据,所以它在函数中传递了一个1,独立于Main中的t。传为值
在后面;我们传的是数组的名字,数组的名字是数组第一个元素的地址,所以我们传的是地址,所以我们是按地址访问的,改变原来的值

你的两个例子在语义上的差异不是由不同的类型来解释的,而是你对两个例子中的参数做了不同的事情。

在第一个示例中,您正在分配给参数(t += 2 相当于 t = t + 2t = something 是分配给 t。)在第二个示例中, 你不分配给 x;相反,您正在使用数组元素语法来修改 x 指向的数组的内部内容。如果您在第二个示例中做了与第一个示例中相同的操作,即直接分配给参数 x,您会看到被调用范围同样不受分配的影响:

static void Main(string[] args) {
    int[] happiness = { 2, 3, 4, 5, 6 };
    SunIsShining(happiness);

    foreach (int y in happiness) {
        Console.WriteLine(y); // prints 2, 3, 4, 5, 6
    }
}
static void SunIsShining(int[] x) {
    x = new int[] {7, 8, 9};
}

所以尽管你的第一个例子传递了int,一个值类型,而这个例子传递了int[],一个引用类型,你可以看到分配给被调用函数中的参数的结果在这两种情况下都是一样的。事实上,对于所有其他类型也是如此——如果方法参数没有标记为 refout,那么无论参数是什么类型,直接赋值给被调用的参数变量函数永远不会对调用函数中传递的变量产生任何影响。

将变量传递给不是 refout 的方法参数与分配给新变量完全一样。因此,与其考虑传递给方法,不如考虑分配给新变量时会发生什么。当您为第一个示例执行此操作时,您将一个 int 分配给另一个。由于 int 是值类型,每个 int 变量都有自己的整数,赋值会复制整数,因此两个变量的整数具有相同的值。然后当你分配给第二个变量时,你改变了那个整数,但它不会影响第一个变量的整数。这很容易理解。

int tInMain = 1;

int tInAddTwo = tInMain;
tInAddTwo += 2;
Console.WriteLine("inside function {0}", tInAddTwo); // prints 3

Console.WriteLine(tInMain); // prints 1

如果您对上面的示例执行此操作,您会看到我们将一个 int[] 变量分配给另一个变量。 int[] 是一个引用类型,所以这样一个变量的值就是一个“引用”(基本上,用 C++ 的说法,就是指向一个对象的指针)。当您将一个引用复制到另一个时,您会得到两个具有相同值的引用,即它们都指向同一个对象。然后,当您将对另一个对象的引用分配给其中一个变量时,您只需将该变量指向另一个对象。它不会对它过去指向的对象做任何事情(第一个变量仍然指向该对象)。

int[] happiness = { 2, 3, 4, 5, 6 };

int[] x = happiness;
x = new int[] {7, 8, 9};

foreach (int y in happiness) {
    Console.WriteLine(y); // prints 2, 3, 4, 5, 6
}

如果您对第二个示例执行此操作,您会看到您再次将一个引用分配给另一个引用,因此您有两个指向同一对象的引用变量。但是随后您使用其中一个引用来更改指向的对象的内容。当您查看另一个引用所指向的对象时,此更改会明显可见,因为它与所指向的对象相同。

int[] happiness = { 2, 3, 4, 5, 6 };

int[] x = happiness;
for (int i = 0; i < x.Length; i++)
    x[i] += 2;

foreach (int y in happiness)
{
    Console.WriteLine(y); // prints 4, 5, 6, 7, 8
}

现在您可能会问为什么不能对第一个示例执行类似的操作。原因是您正在执行的操作,即“修改引用指向的对象”,对于 int 类型根本不存在。最明显的是,这是因为 int 是一个值类型。但即使对于引用类型,您也可能无法执行此操作,因为所指向的对象可能无法为您提供修改其内容的方法。例如,考虑以下示例,我们将 int 装箱到类型为 object 的对象变量中:

static void Main(string[] args) {
    object t = 1;
    addTwo(t);
    Console.WriteLine(t); // prints 1
}
static void addTwo(object t) { // <-- object is a reference type
    // there is nothing you can do to t here
    // that will change what is printed in Main

    // as described above, assignment to t
    // will never affect the calling scope:
    t = (int)t + 2;
}

这与您的第一个示例类似,只是参数类型是object,引用类型(对对象的引用类型)。但是 int 被装箱的这个对象根本不提供修改其内容的方法。因此,即使在这种情况下,您传递了一个引用,并且 addTwo 中的 tMain 中的 t 指向单个对象,这并不重要因为您无法更改可通过其他引用看到的对象。