在C#中传递对象是按引用还是按值传递?

329

在C#中,我一直认为非基元变量是通过引用传递的,而基元值是按值传递的。

因此,当将任何非基元对象传递给方法时,在方法中对该对象进行任何操作都会影响被传递的对象。(C# 101 的基础知识)

然而,我注意到当我传递一个System.Drawing.Image对象时,似乎并不是这种情况?如果我将一个system.drawing.image对象传递给另一个方法,并在该对象上加载图像,然后让该方法超出范围并返回到调用方法,则原始对象上没有加载该图像?

为什么会这样呢?


28
在C#中,默认情况下所有变量都按值传递。对于引用类型,你传递的是引用的值 - Andrew Barber
由于没有给出代码,所以不太清楚正在询问什么。也许OP的意思是image.Load(filename),或者他们的意思是image = Image.Load(filename),其中image是函数参数。 - StayOnTarget
9个回答

670

对象根本没有被传递。默认情况下,参数被计算并且它的作为传递的初始值传递(按值传递)给你调用方法的参数。现在重要的一点是,对于引用类型,该值是一个引用 - 一种访问对象(或null)的方式。对该对象所做的更改将从调用方可见。但是,在使用按值传递时(这是所有类型的默认值),将参数的值更改为引用不同的对象将不会可见。

如果您想使用按引用传递,无论参数类型是值类型还是引用类型,都必须使用outref。在这种情况下,实际上变量本身通过引用传递,因此参数使用与参数相同的存储位置 - 并且调用者可以看到对参数本身所做的更改。

所以:

public void Foo(Image image)
{
    // This change won't be seen by the caller: it's changing the value
    // of the parameter.
    image = Image.FromStream(...);
}

public void Foo(ref Image image)
{
    // This change *will* be seen by the caller: it's changing the value
    // of the parameter, but we're using pass by reference
    image = Image.FromStream(...);
}

public void Foo(Image image)
{
    // This change *will* be seen by the caller: it's changing the data
    // within the object that the parameter value refers to.
    image.RotateFlip(...);
}

我有一篇文章详细解释了这个问题。基本上,“按引用传递”并不是你想象中的意思。


2
你说得对,我没有看到那个!我使用"Image.FromFile(..)"在替换变量image而不是改变对象! :) 当然。 - Michael
2
如果我们从C#中删除关键字refout,那么说C#传递参数的方式与Java相同,即始终按值传递,这样说是否可以?与Java有什么区别吗? - broadband
2
@DJMethaneMan:是的,完全正确。这不会改变参数(假设myobj是某种引用类型...) - Jon Skeet
5
不,完全不是。这是对参考资料的复制。我建议您阅读链接的文章。 - Jon Skeet
3
例子1和3将指针的副本(地址)传递给源引用类型,因此最初源和本地变量都在同一位置查看同一对象。但是,例子1在方法内更改了本地副本的指针,因此现在它正在查看其他内容,原始指针未受影响,因为无法触及它。例子2(ref)对两者使用相同的指针,因此如果您对其进行更改,它会同时更改两个指针。 - CAD bloke
显示剩余9条评论

108

已经添加了许多好的答案,我还想做出一些贡献,也许可以更加清晰地说明问题。

当您将实例作为参数传递给方法时,它会传递该实例的副本。如果您传递的实例是值类型(驻留在堆栈中),则传递该值的副本,因此,如果您修改它,则不会在调用者中反映出来。如果实例是引用类型,则将引用的副本(同样驻留在堆栈中)传递给对象。因此,您有两个引用指向相同的对象。两者都可以修改该对象。但是,如果在方法体内实例化新对象,则您的引用副本将不再引用原始对象,而是引用您刚创建的新对象。因此,您最终会拥有2个引用和2个对象。


6
这应该被选为答案! - JAN
1
我完全同意! :) - JOSEFtw
非常有道理。非常感谢您,先生。 - Vivek Kaushik
希望对来自Java世界的人有所帮助:在这方面,C#与Java基本相同。然而,不同之处在于通过ref关键字传递引用。请查看此链接-https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/ref - bp4D

23
这里有一个代码示例来展示这个问题:
void Main()
{


    int k = 0;
    TestPlain(k);
    Console.WriteLine("TestPlain:" + k);

    TestRef(ref k);
    Console.WriteLine("TestRef:" + k);

    string t = "test";

    TestObjPlain(t);
    Console.WriteLine("TestObjPlain:" +t);

    TestObjRef(ref t);
    Console.WriteLine("TestObjRef:" + t);
}

public static void TestPlain(int i)
{
    i = 5;
}

public static void TestRef(ref int i)
{
    i = 5;
}

public static void TestObjPlain(string s)
{
    s = "TestObjPlain";
}

public static void TestObjRef(ref string s)
{
    s = "TestObjRef";
}

并且输出:

测试普通值:0

测试引用值:5

测试普通对象:测试

测试引用对象:测试引用对象


4
基本上,如果我们想在调用函数中看到更改,引用类型仍然需要作为引用传递。 - Unbreakable
7
字符串是不可变的引用类型。不可变意味着在创建后无法更改。对字符串的每次更改都会创建一个新字符串。这就是为什么需要将字符串作为'ref'传递以在调用方法中进行更改。其他对象(例如员工)可以在不使用'ref'的情况下传递,以便在调用方法中获取更改。 - Himalaya Garg
4
根据HimalayaGarg的说法,@vmg提供的这个例子并不是很好。你需要包括另一个非不可变的引用类型的例子。 - Daniel

17

我猜这样做更清晰。 我建议下载LinqPad来测试类似的东西。

void Main()
{
    var Person = new Person(){FirstName = "Egli", LastName = "Becerra"};

    //Will update egli
    WontUpdate(Person);
    Console.WriteLine("WontUpdate");
    Console.WriteLine($"First name: {Person.FirstName}, Last name: {Person.LastName}\n");

    UpdateImplicitly(Person);
    Console.WriteLine("UpdateImplicitly");
    Console.WriteLine($"First name: {Person.FirstName}, Last name: {Person.LastName}\n");

    UpdateExplicitly(ref Person);
    Console.WriteLine("UpdateExplicitly");
    Console.WriteLine($"First name: {Person.FirstName}, Last name: {Person.LastName}\n");
}

//Class to test
public class Person{
    public string FirstName {get; set;}
    public string LastName {get; set;}

    public string printName(){
        return $"First name: {FirstName} Last name:{LastName}";
    }
}

public static void WontUpdate(Person p)
{
    //New instance does jack...
    var newP = new Person(){FirstName = p.FirstName, LastName = p.LastName};
    newP.FirstName = "Favio";
    newP.LastName = "Becerra";
}

public static void UpdateImplicitly(Person p)
{
    //Passing by reference implicitly
    p.FirstName = "Favio";
    p.LastName = "Becerra";
}

public static void UpdateExplicitly(ref Person p)
{
    //Again passing by reference explicitly (reduntant)
    p.FirstName = "Favio";
    p.LastName = "Becerra";
}

而且应该输出

不会更新

名字:Egli,姓氏:Becerra

隐式更新

名字:Favio,姓氏:Becerra

显式更新

名字:Favio,姓氏:Becerra


那么,对于公共静态无返回值方法 WhatAbout(Person p) 呢? { p = new Person(){FirstName = "First", LastName = "Last"}; } :) - Marin Popov
1
感谢Lingpad的东西。 - James Ashwood
Linux4Life531,你可以试试这个,它是免费的,而且不需要下载:https://dotnetfiddle.net/。和linqpad一样的功能。 - Egli Becerra

7
当您将 System.Drawing.Image 类型的对象传递给方法时,实际上是传递了该对象的引用副本。因此,如果在该方法内部加载新图像,则使用新复制的引用进行加载。您并没有对原始对象进行更改。
YourMethod(System.Drawing.Image image)
{
    //now this image is a new reference
    //if you load a new image 
    image = new Image()..
    //you are not changing the original reference you are just changing the copy of original reference
}

这是正确的解释方式。在C#中用于二叉树和许多非线性数据结构。 - Mohamed Fathallah

3

2
Employee e = new Employee();
e.Name = "Mayur";

//Passes the reference as value. Parameters passed by value(default).
e.ReferenceParameter(e);

Console.WriteLine(e.Name); // It will print "Shiv"

 class Employee {

   public string Name { get; set; }

   public void ReferenceParameter(Employee emp) {

     //Original reference value updated.
    emp.Name = "Shiv";

    // New reference created so emp object at calling method will not be updated for below changes.
    emp = new Employee();
    emp.Name = "Max";
  }
}

-1
在编写本文时的最新版本C# 9中,默认情况下通过ref传递对象。因此,在调用函数中对对象所做的任何更改都将保留在被调用函数中的对象中。

1
对我来说似乎并不是这样... - XRaycat
1
你的信息来源是什么?本月发布的文档没有提到这一点。传递引用类型参数的文档也没有提到。 - ProgrammingLlama
1
@ProgrammingLlama 的确如此,如果在之前的8个版本之后就这样改变它,似乎会破坏很多现有的代码。 - Michael

-1
在按引用传递中,您只需在函数参数中添加"ref",还有一件事情需要注意,就是声明函数为"static",因为main是静态的(#public void main(String[] args))!
namespace preparation
{
  public  class Program
    {
      public static void swap(ref int lhs,ref int rhs)
      {
          int temp = lhs;
          lhs = rhs;
          rhs = temp;
      }
          static void Main(string[] args)
        {
            int a = 10;
            int b = 80;

  Console.WriteLine("a is before sort " + a);
            Console.WriteLine("b is before sort " + b);
            swap(ref a, ref b);
            Console.WriteLine("");
            Console.WriteLine("a is after sort " + a);
            Console.WriteLine("b is after sort " + b);  
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接