Understanding types passed by value or by reference, and what it means to pass a reference type by reference.

Microsoft .NET

Overview. The goal of this article is to explain the mechanisms of passing structures and classes around in code. It will cover areas such as what it means to pass by value, pass by reference, as well as the unique behaviors that structures and classes present. It is recommended that you have at least a basic understanding that a structure is a value type, and a class is a reference type, and what each of those means. A brief definition of each is provided in the next section for terminology, but does not go into much detail.

Terminology. I will try to use these terms precisely and consistantly to prevent as much confusion as possible.

(1) Types. When speaking about structures and references (eg. value types and reference types) both collectively, I will refer to them as types. This will mean that the context applies to both a value type (struct) and a reference type (class).

(2) Value Type. A value type is a struct in C#. Value types directly store their data, and modifying a value type affects only the copy in which you are modifying.

(3) Reference Type. A reference type is a class in C#. Unlike value types, reference types do not directly store their data. Instead, they store a reference to their data, and modifying a reference type modifies the data that it references. Other reference types that reference the same data in memory will also reflect the change.

(4) Pass/Passing by Reference. Refers to passing in a variable as an argument to a method parameter using the ref and out keywords.

Types are passed by value by default. In C# by default, types are passed into method parameters by value. This means that instead of using a pointer that points to the original storage location in memory, a copy of the type is made in a new storage location.

It is important to understand that value types and reference types have different symantics when passed into a method by value.

(1) Value Type. When a value type is passed by value, a copy of the type is created and any changes to the copy only affect the copy itself; the original type passed in is preserved.

namespace CSharpDemo
{
    using System.Diagnostics;

    /// <summary>
    /// Simple mutable xy value type
    /// </summary>
    internal struct Point
    {
        public Point(int x, int y) {
            this.x = x;
            this.y = y;
        }

        private int x;
        private int y;

        public int X { get { return x; } set { x = value; } }
        public int Y { get { return y; } set { y = value; } }
    }

    internal class Program
    {
        public static void Main(string[] args) {
            int x = 0;
            int y = 0;

            Point p = new Point(x, y);

            OffsetByOne(p);

            Debug.WriteLine(p.X);
            Debug.WriteLine(p.Y);
        }

        private static void OffsetByOne(Point point) {
            point.X++;
            point.Y++;
        }
    }
}

In the example above, we are creating an instance of our value type with x and y values of 0 and passing it as an argument to the OffsetByOne(Point) method. Because the default parameter passing mechanism is by value in C#, a copy of our value type is created. The method will increment both X and Y by one, but it is only modifying the copy; the original point we passed in as an argument is preserved. Because of this behavior, printing the contents p.X and p.Y in the Main(string[]) method will both print 0.

(2) Reference Type. A reference type is simply a reference to a underlying type in memory. Unlike a value type, a reference type does not store its own value, rather it is a pointer to the actual value in memory.

namespace CSharpDemo
{
    using System.Diagnostics;

    /// <summary>
    /// Simple mutable xy reference type
    /// </summary>
    internal class Point
    {
        public Point(int x, int y) {
            this.x = x;
            this.y = y;
        }

        private int x;
        private int y;

        public int X { get { return x; } set { x = value; } }
        public int Y { get { return y; } set { y = value; } }
    }

    internal class Program
    {
        public static void Main(string[] args) {
            int x = 0;
            int y = 0;

            Point p = new Point(x, y);

            OffsetByOne(p);

            Debug.WriteLine(p.X);
            Debug.WriteLine(p.Y);
        }

        private static void OffsetByOne(Point point) {
            point.X++;
            point.Y++;
        }
    }
}

In the example above note it is the same as the previous example, except that we changed our Point definition from a struct (value type) to a class (reference type). We are creating an instance of our reference type with x and y values of 0 and passing it as an argument to the OffsetByOne(Point) method.

Because the default parameter passing mechanism is by value in C#, a copy of our reference type is created. The method will increment both X and Y by one, and because the copy references the same underlying data in memory as the original type passed in, both references will reflect the change. Because of this behavior, printing the contents p.X and p.Y in the Main(string[]) method will both print 1.

This is where a lot of confusion comes from if you don't quite understand the symantics of a reference type. A type being a reference type does not mean you are passing by reference, you are still passing by value, and it is important to recognize the difference.

Passing types by reference. Passing types by references can be done two ways. The first method is using the ref keyword, and the second is using the out keyword. It is crucial to know that out is actually identical to ref, but with stronger rules. The out keyword actually gets mapped to the ref keyword in the CLR, but with special metadata for the rules it has.

Both the ref and out keywords do not create new storage locations. Instead, they represent the same storage location as the type given as the argument in the function member. It is important to understand that this is actually a pointer.

Because using ref and out means you are actually using a pointer parameter, it is important to know that when passing a reference type by reference, you are actually creating a double pointer, pointer to pointer, or double indirection. This is because a reference type itself is a pointer to a storage location in memory. It is highly recommended to have some knowledge and understanding of how pointers work before deciding whether passing a type, especially a reference type by reference is actually necessary.

(1) ref. A reference type must be definitely assigned before it can be passed as a reference parameter. Because of this rule, a reference parameter is always considered initially assigned within a function.

namespace CSharpDemo
{
    using System.Diagnostics;

    /// <summary>
    /// Simple mutable xy value type
    /// </summary>
    internal struct Point
    {
        public Point(int x, int y) {
            this.x = x;
            this.y = y;
        }

        private int x;
        private int y;

        public int X { get { return x; } set { x = value; } }
        public int Y { get { return y; } set { y = value; } }
    }

    internal class Program
    {
        public static void Main(string[] args) {
            int x = 0;
            int y = 0;

            Point p = new Point(x, y);

            OffsetByOne(ref p);

            Debug.WriteLine(p.X);
            Debug.WriteLine(p.Y);
        }

        private static void OffsetByOne(ref Point point) {
            point.X++;
            point.Y++;
        }
    }
}

Make note that in the example above we are working with a value type again, and that there are two key changes. The first change is that we added a reference parameter to our OffsetByOne(Point) method, which changes the signature to OffsetByOne(ref Point). The second change is our calling syntax has changed by also adding the ref keyword. This convention is that of passing a type by reference using the ref keyword.

For a value type, passing by reference means that you get reference type symantics. By passing our value type by reference, OffsetByOne(ref Point) will actually modify the value of the original type passed in. The behavior of this is that printing the contents of p.X and p.Y in Main(string[]) will now both print the value of 1.

Now because passing a reference type by reference means using a double pointer, pointer to pointer, or double indirection, I will cover passing a reference type by reference later in the article.

(2) out. As I stated earlier, the rules for out are much stronger. A type does not need to be definitely assigned before it can be passed as an argument.

It's important to note here that if you do assign your type before passing it as an argument using the out keyword, don't expect your value to be retained.

I urge the point above because when using the out keyword, it is required that the type passed in as an argument be definitely assigned before the method returns normally. This means that no matter what, the type that you passed in will be assigned a new value, overwriting any old value that was there.

Parameters passed in via out, are actually considered output parameters. These should be treated as additional return values, so treat it with return type symantics.

namespace CSharpDemo
{
    using System.Diagnostics;

    /// <summary>
    /// Simple mutable xy value type
    /// </summary>
    internal struct Point
    {
        public Point(int x, int y) {
            this.x = x;
            this.y = y;
        }

        private int x;
        private int y;

        public int X { get { return x; } set { x = value; } }
        public int Y { get { return y; } set { y = value; } }
    }

    internal class Program
    {
        public static void Main(string[] args) {
            int x = 0;
            int y = 0;

            Point p = new Point(x, y);

            OffsetByOne(out p);

            Debug.WriteLine(p.X);
            Debug.WriteLine(p.Y);
        }

        private static void OffsetByOne(out Point point) {
            int x = 1;
            int y = 1;

            point = new Point(x, y);

            point.X++;
            point.Y++;
        }
    }
}

The only difference between this example and the ref example, is that we are using the out keyword and I have updated the OffsetByOne(out Point) method body. Note that because we must definitely assign the parameter named point before the method returns, we are assigning it a new Point with a XY values of 1. Because when passing a value type by reference we get reference type symantics, printing the value of our type in Main(string[]) will yield XY values of 2.

Again, out and ref are the same, but out should be treated as an extra return value. With ref you can always expect your value to either be changed, or unchanged, but with out you should expect a completely new value each time just as if you were consuming a normal return value from a method.

Passing a reference type by reference. Hopefully by the time you get to this part you already have an idea of what I'm about to say. It's not complicated. The importance here is that you simply understand the symantics of a reference type itself, and that passing a reference type does not automatically mean passing by reference.

At any rate, I hope you enjoyed the article and that it provided some insight.

Leave a Comment