Now that we understand a little more about classes and previously learned the difference between value types and reference types, it's time to explore some more specialized C# types.

In today's edition of C# in Simple Terms, let's explore two useful value types: structs and enums.

A series of overlapping metal trusses on the side of a building.
Don't cross the beams! Photo by Ricardo Gomez Angel / Unsplash

The Sample Project

exceptionnotfound/CSharpInSimpleTerms
Contribute to exceptionnotfound/CSharpInSimpleTerms development by creating an account on GitHub.
Project for this post: 8StructsAndEnums

Structs

A structure type (or struct) is a C# type that, similarly to classes, encapsulates data and functionality. We use the struct keyword to define a struct.

Like classes, structs can have methods, constructors, and properties. However, structs are always value types, while classes are always reference types.

public struct Player 
{
    public string Name { get; set; }
    public int YearsPlaying { get; set; }
    
    public Player(string name, int yearsPlaying)
    {
        Name = name;
        YearsPlaying = yearsPlaying;
    }
}

var player = new Player();
player.Name = "Alex Hampton";
player.YearsPlaying = 2;

While similar to classes, structs have some very important differences:

  • Structs cannot have a parameterless constructor, AND
  • Structs do not support inheritance. We will discuss inheritance more thoroughly in the next part of this series.

Typically, we use structs to define small, data-only objects with little or no behavior. To demonstrate this, we need only look at several of the primitive types we are already familiar with. The keywords for many of the primitive types are just shortcuts to defined types that are created as structs.

int five = 5; //Actually System.Int32
decimal money = 12.4M; //Actually System.Decimal
bool isTrue = true; //Actually System.Boolean

Instantiating Structs

Because structs are value types, when we create an instance of a struct we must pass to it values for each of its properties. We can do this in three ways.

In the first way, we rely on the default values of the properties in the struct.

public struct Coach
{
    public string Name { get; set; }
    public int YearsCoaching { get; set; }
}

var coach = new Coach();
Console.WriteLine(coach.Name); //Null
Console.WriteLine(coach.YearsCoaching); //0

In the second way, we pass values for each public property during instantiation:

public struct Coach
{
    public string Name { get; set; }
    public int YearsCoaching { get; set; }
}

var coach2 = new Coach() //This is called inline instantiation
{
    Name = "John Smith",
    YearsCoaching = 12
};

Console.WriteLine(coach2.Name);
Console.WriteLine(coach2.YearsCoaching);

In the third way, we use a constructor method on the struct to give default values.

public struct Coach
{
    public string Name { get; set; }
    public int YearsCoaching { get; set; }
    
    public Coach (string name, int years)
    {
        Name = name;
        YearsCoaching = years;
    }
}

var coach3 = new Coach("Elaine Harkness", 6);

Console.WriteLine(coach3.Name); //Elaine Harkness
Console.WriteLine(coach3.YearsCoaching); //6

Readonly Struct

Due to restrictions on struct usage, Microsoft recommends we implement immutable structs in most cases. An immutable struct is one whose values can only be set at instantiation. We make a struct immutable by declaring it with the readonly keyword.

public readonly struct ReadonlyPlayer
{
    public string Name { get; } //No setter methods!
    public int TurnOrder { get; }

    public ReadonlyPlayer(string name, int turnOrder)
    {
        Name = name;
        TurnOrder = turnOrder;
    }
}

//elsewhere
var readonlyPlayer = new ReadonlyPlayer("Matt", 1);

Note that because this struct is immutable, its properties can no longer have setter methods. Further, we cannot change the value of the struct's properties at runtime:

var readonlyPlayer = new ReadonlyPlayer("Matt", 1);
readonlyPlayer.Name = "Different Name"; //Compilation error!
Error: Property or indexer 'Name' cannot be assigned to – it is read only.

Readonly Instance Members

We can also declare the members of a struct as readonly; this is commonly done with methods where the method does not change the state of the struct.

public readonly struct Player 
{
    public string Name { get; }
    public int TurnOrder { get; }
    
    public Player(string name, int turnOrder)
    {
        Name = name;
        TurnOrder = turnOrder;
    }
    
    public readonly string GetCustomDisplay()
    {
        return Name + " will play in position #" + TurnOrder.ToString();
    }
}

//elsewhere
var player = new Player()
{
    Name = "Matt",
    TurnOrder = 1
};
Console.WriteLine(player.GetCustomDisplay());

Enumerations

The other kind of special object we'll discuss is an enumeration.

Enumerations (or enums) are a set of integer values which have been assigned names. Their major purpose is to make reading code easier and to eliminate the use of magic numbers, or numbers whose meaning is not obvious from their value.

int color = 1; //What does 1 mean?

//Instead, do this
public enum Color
{
    Red = 1,
    Yellow = 2,
    Blue = 3,
    Green = 4
}

var myColor = Color.Red;
Console.WriteLine(myColor); //Red

Casting to Value

We can cast enum values to their value, and back:

var myColor = Color.Blue;
var myColorValue = (int)myColor;
Console.WriteLine(myColorValue); //3

var myColor2 = 4;
var myColorEnum = (Color)myColor2;
Console.WriteLine(myColorEnum); //Green

Default Values

Enumerations can be assigned values like in the above example, or use the default values (which start at zero and go up by one):

public enum CardType
{
    Resource, //0
    Science, //1
    Economic, //2
    Military //3
}

By default, enums are of type int; we can optionally specify another integer type instead. We can also assign any value of that type we like to represent each enum value:

public enum HairColor : short
{
    Brown = 5,
    Blonde = 38,
    Red = 145,
    Black = 2,
    Grey = 42
}

Because enumerations are little more than integers with names, they cannot have methods, constructors, or other features of structs and classes.

Enumerations as Bit Flags

A neat trick of enumerations in C# is their ability to represent combinations of values; to do this, we implement a specialized kind of enumeration called a bit flag.

For this to work, we need two things:

  1. Our enum must be decorated with the [Flags] attribute, AND
  2. Every value in the enum must be a power of 2.

By setting this up, we enable our enum to represent several selected values, rather than just one. For example, here's a Months enum set up as a bit flag.

[Flags]
public enum Months
{
    January = 1, //2^0
    February = 2, //2^1
    March = 4, //2^2
    April = 8, //2^3
    May = 16, //2^4
    June = 32, //2^5
    July = 64, //2^6
    August = 128, //2^7
    September = 256, //2^8
    October = 512, //2^9
    November = 1024, //2^10
    December = 2048 //2^11
}

If we want to represent multiple values, we can use the logical OR operator (|) to do so.

Months birthdayMonths = Months.January 
                        | Months.March 
                        | Months.September 
                        | Months.November;
                        
Console.WriteLine($"Your family has birthdays in {birthdayMonths}");
//Output: Your family has birthdays in January, March, September, November

We can also use the logical AND operator (&) to get the intersection of two groups (that is, values which appear in both sets).

Months birthdayMonths = Months.January 
                        | Months.March 
                        | Months.September 
                        | Months.November;
                        
Months otherBirthdays = Months.January
                        | Months.April
                        | Months.September;
                        
Console.WriteLine($"The months in both groups are {birthdayMonths & otherBirthdays}");
//Output: The months in both groups are January, September

Precisely why this works is beyond the scope of this series; you can find out more in the official documentation.

Enumeration types - C# reference
Learn about C# enumeration types that represent a choice or a combination of choices

Glossary

  • Structure type (struct) - A value type in C# with a set of properties.
  • Immutable - A value that cannot be changed after it is instantiated.
  • Magic numbers - Numbers which have a meaning, but said meaning is not obvious to the person reading the code.
  • Enumeration - A value type. Is an integer value with a given name. Used to replace magic numbers and for other purposes.
  • Bit flag - A specialized usage of enumerations where we can represent a combination of values.

New Keywords

  • struct - Used to define a structure type.
  • enum - Used to define an enumeration.
  • readonly - Used to make an object immutable.

Summary

Structs and enums are both specialized value types in C#.

Structs allow us to define small, encapsulated values and pass them around as a group. They can have constructors, methods, and properties. Generally we use structs for objects that have little to no behavior. It is recommended that we create immutable structs, which means we must assign values to a struct instance when it is instantiated.

Enums are integer values that have been given a name; their primary purpose is to make reading our code a bit easier. We can use any integer type to represent the value of an enum, and int is the default. When creating an enum, we can either specify the integer value for each name, or let C#'s compiler assign values. Finally, we can use a specialized kind of enumeration called a bit flag to represent combinations of values.

In the next part of this series, we zoom out a bit from the specifics of C# and take a look at two of the most fundamental concepts in all of object-oriented programming: inheritance and polymorphism.

C# in Simple Terms - Inheritance and Polymorphism
Two of the fundamental object-oriented programming concepts explained! Plus: virtual methods and properties.

Happy Coding!