简体   繁体   中英

Passing a class as a ref parameter in C# does not always work as expected. Can anyone explain?

I always thought that a method parameter with a class type is passed as a reference parameter by default. Apparently that is not always the case. Consider these unit tests in C# (using MSTest).

[TestClass]
public class Sandbox
{
    private class TestRefClass
    {
        public int TestInt { get; set; }
    }

    private void TestDefaultMethod(TestRefClass testClass)
    {
        testClass.TestInt = 1;
    }

    private void TestAssignmentMethod(TestRefClass testClass)
    {
        testClass = new TestRefClass() { TestInt = 1 };
    }

    private void TestAssignmentRefMethod(ref TestRefClass testClass)
    {
        testClass = new TestRefClass() { TestInt = 1 };
    }

    [TestMethod]
    public void DefaultTest()
    {
        var testObj = new TestRefClass() { TestInt = 0 };
        TestDefaultMethod(testObj);
        Assert.IsTrue(testObj.TestInt == 1);
    }

    [TestMethod]
    public void AssignmentTest()
    {
        var testObj = new TestRefClass() { TestInt = 0 };
        TestAssignmentMethod(testObj);
        Assert.IsTrue(testObj.TestInt == 1);
    }

    [TestMethod]
    public void AssignmentRefTest()
    {
        var testObj = new TestRefClass() { TestInt = 0 };
        TestAssignmentRefMethod(ref testObj);
        Assert.IsTrue(testObj.TestInt == 1);
    }
}

The results are that AssignmentTest() fails and the other two test methods pass. I assume the issue is that assigning a new instance to the testClass parameter breaks the parameter reference, but somehow explicitly adding the ref keyword fixes this.

Can anyone give a good, detailed explanation of whats going on here? I'm mainly just trying to expand my knowledge of C#; I don't have any specific scenario I'm trying to solve...

The thing that is nearly always forgotten is that a class isn't passed by reference, the reference to the class is passed by value.

This is important. Instead of copying the entire class (pass by value in the stereotypical sense), the reference to that class (I'm trying to avoid saying "pointer") is copied. This is 4 or 8 bytes; much more palatable than copying the whole class and in effect means the class is passed "by reference".

At this point, the method has it's own copy of the reference to the class . Assignment to that reference is scoped within the method (the method re-assigned only its own copy of the reference).

Dereferencing that reference (as in, talking to class members) would work as you'd expect: you'd see the underlying class unless you change it to look at a new instance (which is what you do in your failing test).

Using the ref keyword is effectively passing the reference itself by reference (pointer to a pointer sort of thing).

As always, Jon Skeet has provided a very well written overview:

http://www.yoda.arachsys.com/csharp/parameters.html

Pay attention to the "Reference parameters" part:

Reference parameters don't pass the values of the variables used in the function member invocation - they use the variables themselves.

If the method assigns something to a ref reference, then the caller's copy is also affected (as you have observed) because they are looking at the same reference to an instance in memory (as opposed to each having their own copy).

The default convention for parameters in C# is pass by value. This is true whether the parameter is a class or struct . In the class case just the reference is passed by value while in the struct case a shallow copy of the entire object is passed.

When you enter the TestAssignmentMethod there are 2 references to a single object: testObj which lives in AssignmentTest and testClass which lives in TestAssignmentMethod . If you were to mutate the actual object via testClass or testObj it would be visible to both references since they both point to the same object. In the first line though you execute

testClass = new TestRefClass() { TestInt = 1 }

This creates a new object and points testClass to it. This doesn't alter where the testObj reference points in any way because testClass is an independent copy. There are now 2 objects and 2 references which each reference pointing to a different object instance.

If you want pass by reference semantics you need to use a ref parameter.

The AssignmentTest uses TestAssignmentMethod which only changes the object reference passed by value .

So the object itself is passed by reference but the reference to the object is passed by value. so when you do:

testClass = new TestRefClass() { TestInt = 1 };

You are changing the local copied reference passed to the method not the reference you have in the test.

So here:

[TestMethod]
public void AssignmentTest()
{
    var testObj = new TestRefClass() { TestInt = 0 };
    TestAssignmentMethod(testObj);
    Assert.IsTrue(testObj.TestInt == 1);
}

testObj is a reference variable. When you pass it to TestAssignmentMethod(testObj); , the refernce is passed by value. so when you change it in the method, original reference still points to the same object .

My 2 cents

When class is passed to a method a copy of it's memory space address is being sent (a direction to you house is being sent). So any operation on that address with effect the house but will not change the address it self. (this is default)

Passing class(object) by reference has an effect of passing it's actual address instead of copy of an address. That means if you assign a new object to argument passed by reference it will change the actual address (similar to relocation). :D

This is how I see it.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM