Author avatar

Dániel Szabó

Value and Reference Type Assignment in C#

Dániel Szabó

  • Mar 5, 2020
  • 6 Min read
  • 880 Views
  • Mar 5, 2020
  • 6 Min read
  • 880 Views
Languages Frameworks and Tools
C#

Introduction

When it comes to the type system of a programming language, there are two main categories: strongly and weakly typed programming languages. These categories define how typing rules apply during compilation and runtime of the source code. In 1974, Barbara Liskov and Stephen Zilles defined the strongly typed programming language this way.

Whenever an object is passed from a calling function to a called function, its type must be compatible with the type declared in the called function.

In this guide we will explore why C# falls into the category of strongly typed languages and how this affects type assignment, particularly when it comes to value and reference types.

Types and Context

In the .NET framework, and thus the C# programming language, there are two main types: value type and reference type. There are also two contexts in C#, safe and unsafe. It should be mentioned that in an unsafe context, we have a third type called pointer. For now it's enough to know that we call a context safe when the code is running under the supervision of the CLR, and unsafe code is quite the opposite.

Value and Reference

There are four types that fall to the reference type category:

  1. String
  2. Arrays
  3. Class
  4. Delegate

The rest of the types fall to the value type category:

  1. Bool
  2. Byte
  3. Char
  4. Decimal
  5. etc.

Value Type

Let's look at a demonstration of the value type. In this code, we have a function that takes a value of double type and squares it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;

namespace Pluralsight
{
    public class ValueType
    {
        public static void SquareIt(double x)
        {
            Console.WriteLine($"The value of x: {x} squared is: {x * x}");
        }
        public static void Main()
        {
            double d = 22.22;
            SquareIt(d);
            Console.ReadKey();            
        }
    }
}
csharp

The output looks like this if we pass 22.22 as input for the function.

1
The value of x: 22.22 squared is: 493.7284
bash

What happens behind the scenes? The value of d's content is allocated on the stack. A single space in memory based on the type is reserved in memory, and this variable directly holds the value. If we were to copy this value to another variable via an assignment, the value would be copied so you would have the value of d reserved twice on the stack, one for each variable. Every predefined datatype, including enums and even structs, works this way. The value types are created at compile time and because of this, they are protected from the garbage collector—it cannot access them.

Reference Type

Let's look at a demonstration for this, too, and then dissect it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
using System;

namespace Pluralsight
{
    public class ReferenceType
    {
        public class Server
        {
            public string type;
            public int vCPU;
            public double RAM;
        }
        public static void initializeServer(string category, Server s)
        {
            if(category == "small")
            {
                s.type = "smallServer";
                s.vCPU = 1;
                s.RAM = 2.0;
            }
            else if(category == "medium")
            {
                s.type = "mediumServer";
                s.vCPU = 2;
                s.RAM = 4.0;
            }
            else if(category == "big")
            {
                s.type = "bigServer";
                s.vCPU = 8;
                s.RAM = 16.0;
            } else { throw new System.ArgumentException("Category cannot be other than: small, medium or big"); }
        }
        public static void Main()
        {
            Server a = new Server();
            Server b = new Server();
            Server c = new Server();
            initializeServer("small", a);
            initializeServer("medium", b);
            initializeServer("big", c);

            Console.WriteLine($"The server:{nameof(a)}, is of type: {a.type}, vCPU: {a.vCPU} and RAM: {a.RAM} GB!");
            Console.WriteLine($"The server:{nameof(b)}, is of type: {b.type}, vCPU: {b.vCPU} and RAM: {b.RAM} GB!");
            Console.WriteLine($"The server:{nameof(c)}, is of type: {c.type}, vCPU: {c.vCPU} and RAM: {c.RAM} GB!");
            Console.ReadKey();            
        }
    }
}
csharp

The output looks like this:

1
2
3
The server:a, is of type: smallServer, vCPU: 1 and RAM: 2 GB!
The server:b, is of type: mediumServer, vCPU: 2 and RAM: 4 GB!
The server:c, is of type: bigServer, vCPU: 8 and RAM: 16 GB!
bash

We have a class which is of reference type. This is important to note because the function called initializeServer, which is initializing it, takes two arguments. The first one is of value type, and the second one is of reference type, which is where the class is passed. The function will change public properties of the class passed by reference based on the category we pass. If we pass a category that is not implemented, an error is thrown with this message:

1
System.ArgumentException: 'Category cannot be other than: small, medium or big'
bash

What happens behind the scenes? The server class instances a, b, and c are of reference type. This means when we pass them to the initializeServer function, their addresses are passed rather than the instance with the data. When we assign a reference variable to another, we do not copy the data. We simply tell that the new variable also references the original address. This means if we were to modify either of them, the change would be reflected on both since they point to the same location in memory. The reference type variables are stored in a designated space in memory called heap. As a result, when such a variable is not used anymore in our application, it is subject to be released by the garbage collector.

A note on stack vs. heap: Stack is used for static memory allocation, while heap is used for dynamic memory allocation. They are both useful, but they are intended for different purposes.

Conclusion

In this guide, we looked at value and reference types. Through several examples, we dissected what these types are for and how they are different programmatically. We also learned that there are two main memory types for C#. I hope this guide has been informative to you and I would like to thank you for reading it.

11