C# 11 Structures

Having covered C# classes in the preceding chapters, this lesson will introduce the use of structures in C#. Although at first glance structures and classes look similar, there are some important differences that need to be understood when deciding which to use. This chapter will outline how to declare and use structures, explore the differences between structures and classes and introduce the concepts of value and reference types.

An overview of C# structures

As with classes, structures form the basis of object-oriented programming and provide a way to encapsulate data and functionality into reusable instances. Structure declarations resemble classes with the exception that the struct keyword is used in place of the class keyword. The following code, for example, declares a simple structure consisting of a String variable, initializer, and method:

struct SampleStruct {
    
    private string _name;
    
    public string Name {
        get { return _name; }
        set { _name = value; }
    }

    public SampleStruct(string name) {
        _name = name;
    }
    
    public string BuildHelloMsg() {
        return "Hello " + _name;
    }
}

Consider the above structure declaration in comparison to the equivalent class declaration:

public class SampleClass {
    
    private string _name;
    
    public string Name {
        get { return _name; }
        set { _name = value; }
    }

    public SampleStruct(string name) {
        _name = name;
    }
    
    public string BuildHelloMsg() {
        return "Hello " + _name;
    }
}

Other than the use of the struct keyword instead of class, the two declarations are identical. Instances of each type are also created using the same syntax:

SampleStruct demoStruct = new SampleStruct(name: "Alex");
SampleClass demoClass = new SampleClass(name: "Anna"); 

Given the commonality between classes and structures, it is important to gain an understanding of how the two differ. Before exploring the most significant difference it is first necessary to understand the concepts of value types and reference types.

Value types vs. reference types

While on the surface structures and classes look alike, major differences in behavior occur when structure and class instances are copied or passed as arguments to methods. This occurs because structure instances are value type while class instances are reference type.

When a structure instance is copied or passed to a method, an actual copy of the instance is created, together with any data contained within the instance. This means that the copy has its own version of the data which is unconnected with the original structure instance. In effect, this means that there can be multiple copies of a structure instance within a running app, each with its own local copy of the associated data. A change to one instance has no impact on any other instances.

In contrast, when a class instance is copied or passed as an argument, the only thing duplicated or passed is a reference to the location in memory where that class instance resides. Any changes made to the instance using those references will be performed on the same instance. In other words, there is only one class instance but multiple references pointing to it. A change to the instance data using any one of those references changes the data for all other references.

To demonstrate reference and value types in action, consider the following code:

class Example
{
    public struct SampleStruct {
        
        private string _name;
        
        public string Name {
            get { return _name; }
            set { _name = value; }
        }

        public SampleStruct(string name) {
            _name = name;
        }
        
        public string BuildHelloMsg() {
            return "Hello " + _name;
        }
    }

    static void Main()
    {
        SampleStruct myStruct1 = new SampleStruct(name: "Mark");
        System.Console.WriteLine(myStruct1.BuildHelloMsg());
    }
}

When the code executes, the name “Mark” will be displayed. Now change the code so that a copy of the myStruct1 instance is made, the name property is changed and the names from each instance are displayed in the message:

class Example
{
    public struct SampleStruct {
        
        private string _name;
        
        public string Name {
            get { return _name; }
            set { _name = value; }
        }

        public SampleStruct(string name) {
            _name = name;
        }
        
        public string BuildHelloMsg() {
            return "Hello " + _name;
        }
    }

    static void Main()
    {
        SampleStruct myStruct1 = new SampleStruct(name: "Mark");
        SampleStruct myStruct2 = myStruct1;
        myStruct2.Name = "David";

        System.Console.WriteLine(myStruct1.BuildHelloMsg());
        System.Console.WriteLine(myStruct2.BuildHelloMsg());
    }
}

When executed, the output will read as follows:

Hello Mark
Hello David

Clearly, the change of name only applied to myStruct2 since this is an actual copy of myStruct1 containing its own copy of the data as shown in the following figure:

Figure 1-1

Contrast this with the following class example:

class Example
{
    public class SampleClass {
        
        private string _name;
        
        public string Name {
            get { return _name; }
            set { _name = value; }
        }

        public SampleClass(string name) {
            _name = name;
        }
        
        public string BuildHelloMsg() {
            return "Hello " + _name;
        }
    }

    static void Main()
    {
        SampleClass myClass1 = new SampleClass(name: "Mark");
        SampleClass myClass2 = myClass1;
        myClass2.Name = "David";

        System.Console.WriteLine(myClass1.BuildHelloMsg());
        System.Console.WriteLine(myClass2.BuildHelloMsg());
    }
}

When this code executes, the following output will be generated:

Hello David
Hello David

In this case, the name property change is reflected for both myClass1 and myClass2 because both are references pointing to the same class instance as illustrated in the figure below:

Figure 1-2

In addition to these value and reference type differences, structures do not support inheritance and subclassing in the way that classes do. In other words, it is not possible for one structure to inherit from another structure. Unlike classes, structures also cannot contain a finalizer method. Structure members cannot be specified as abstract, virtual, or protected. Structures can, however, implement interfaces.

Read-only structures

Structures can be declared as being read-only using the readonly modifier. When declared in this way, all data members of the structure must also be declared as being read-only. In the case of field members, this requires the use of the readonly modifier. Properties, on the other hand, must either be declared as readonly or use the init-only accessors (a topic covered in the chapter entitled An Introduction to C# 11 Object-Oriented Programming).

The following structure, for example, includes both a read-only field and an init-only property:

class Example
{
    public readonly struct SampleStruct {
        
        private readonly string _name;
        
        public string Name {
            get { return _name; }
          //  set { _name = value; }
        }

        public SampleStruct(string name) {
            _name = name;
        }
        
        public string BuildHelloMsg() {
            return "Hello " + _name;
        }
    }

    static void Main()
    {
        SampleStruct myStruct1 = new SampleStruct(name: "Mark");
        SampleStruct myStruct2 = myStruct1;
       // myStruct2.Name = "David";

        System.Console.WriteLine(myStruct1.BuildHelloMsg());
        System.Console.WriteLine(myStruct2.BuildHelloMsg());
    }
}

Categories