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.
The Sample Project
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:
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:
- Our enum must be decorated with the
[Flags]
attribute, AND - 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.
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.
Happy Coding!