Chapter 11
IN THIS CHAPTER
Determining when to use structures
Defining structures
Working with structures
Working with records
Structures are an important addition to C# because they provide a means for defining complex data entities, akin to records from a database. Because of the way you use structures to develop applications, a distinct overlap exists between structures and classes. This overlap causes problems for many developers because determining when to use a structure versus a class can be difficult. Consequently, the first order of business in this chapter is to discuss the differences between the two and offer some best practices.
Creating structures requires you to use the struct
keyword. A structure can contain many of the same elements found in classes: constructors, constants, fields, methods, properties, indexers, operators, events, and even nested types. This chapter helps you understand the nuances of creating structures with these elements so that you can fully access all the flexibility that structures have to offer.
Even though structures do have a great deal to offer, the most common way to use them is to represent a kind of data record. The next section of this chapter discusses the structure as a record-holding object. You discover how to use structures in this manner for single records and for multiple records as part of a collection.
C# 9.0 introduced the new record
type (with the field
addition in C# 10.0), which is a kind of class with immutable features. Like classes, record
s are a reference type, rather than a value type like structures are. The final section of this chapter discusses the new record
type and helps you understand how it differs from the structure and class.
For many developers, the differences between structures and classes are confusing, to say the least. In fact, many developers use classes alone and forget about structures. However, not using structures is a mistake because they fulfill a definite purpose in your programming strategy. Using structures can make the difference between an application that performs well and one that does the job, but does so more slowly than it could.
Structures are a value type, which means that C# allocates memory for them differently than classes. Most of the struct
limits come from this difference. Here are some things to consider when you think about using a struct
in place of a class. A structure
abstract
, virtual
, or protected
.When working with structures, you must remember that they're a value type, not a reference type like classes. This means that structures have certain inherent advantages over classes. For example, they’re much less resource intensive. In addition, because structures aren’t garbage-collected, they tend to require less time to allocate and deallocate.
Value types are also deterministic. You know that C# deallocates them the moment they go out of scope. Waiting for C# to garbage-collect reference types means that you can’t quite be sure how memory is used in your application.
Opinions abound as to when to use a struct
versus a class
. For the most part, it all ends up being a matter of what you're trying to achieve and what you’re willing to pay in terms of both resource usage and application speed to achieve it. In most cases, you use class
far more often than you use struct
simply because class
is more flexible and tends to incur fewer penalties in some situations.
As with all value types, structures must be boxed and unboxed when cast to a reference type or when required by an interface they implement. Too much boxing and unboxing will actually make your application run slower. This means that you should avoid using structures when you need to perform tasks with reference types. In this case, using a class is the best idea.
It’s true that structures do have a definite advantage when working with arrays. However, you must exercise care when working with structures in collection types because the structure may require boxing and unboxing. If a collection works with objects, you need to consider using a class instead.
Avoid using structures when working with objects. Yes, you can place object types within a structure, but then the structure will contain a reference to the object, rather than the object itself. References reduce the impact of any resource and time savings that a structure can provide. Keep structures limited to other value types such as int
and double
when possible. Of course, many structures still use reference types such as String
.
Creating a structure is similar to creating a class in many respects. Of course, you use the struct
keyword instead of the class
keyword, and a structure has the limitations described in the “Considering struct limits” section, earlier in this chapter. However, even with these differences, if you know how to create a class, you can also create a structure. The following sections describe how to work with structures in greater detail.
A basic struct
doesn't contain much more than the fields you want to use to store data. For example, consider a struct
used to store messages from people requesting the price of certain products given a particular quantity. It might look like this (also found in the BasicStruct
example in the downloadable source):
public struct Message
{
public int MsgID;
public int ProductID;
public int Qty;
public double Price;
}
To use this basic structure, you might follow a process like this:
static void Main(string[] args)
{
// Create the struct without new.
Message myMsg;
// Or, create it with new.
//Message myMsg = new Message();
// Create a message.
myMsg.MsgID = 1;
myMsg.ProductID = 22;
myMsg.Qty = 5;
// Compute the price.
myMsg.Price = 5.99 * myMsg.Qty;
// Display the struct on screen.
Console.WriteLine(
$"In response to Msg {myMsg.MsgID}, you can get {myMsg.Qty} " +
$"of {myMsg.ProductID} for ${myMsg.Price}.");
Console.ReadLine();
}
Note that the process used to create and use a structure is much the same as creating and using a class. In fact, you could possibly look at the two processes as being the same for the most part (keeping in mind that structures do have differences). For example, you can create a struct
without using new
as shown in the code, which is a benefit when struct
s are used in an array from a performance perspective. If you create a struct
without using new
, then you must initialize the fields before you use them. The output from this example looks like this:
In response to Msg 1, you can get 5 of 22 for $29.95.
Structures can include many of the same elements as classes do. The “Defining a basic struct” section of the chapter introduces you to the use of fields. As previously noted, fields cannot be abstract
, virtual
, or protected
. However, their default scope is private
, and you can set them to public
, as shown in the code. Obviously, classes contain far more than fields and so do structures. The following sections use the StructWithElements
example to take you through the common struct
elements so that you can use structures efficiently in your code.
As with a class
, you can create a struct
with a constructor. Here's an example of the Message
struct
with a constructor included:
public struct Message
{
public int MsgID;
public int ProductID;
public int Qty;
public double Price;
public Message(
int msgId, int productId = 22, int qty = 5)
{
// Provided by the user.
MsgID = msgId;
ProductID = productId;
Qty = qty;
// Defined by the application.
if (ProductID == 22)
Price = 5.99 * qty;
else
Price = 6.99 * qty;
}
}
Message myMsg2 = new Message(2);
Console.WriteLine(
$"In response to Msg {myMsg2.MsgID}, you can get {myMsg2.Qty} " +
$"of {myMsg2.ProductID} for ${myMsg2.Price}.");
The output from this part of the example is the same as for the BasicStruct
example. Thanks to the use of default parameters, you can create a new message by simply providing the message number. The default parameters assign the other values. Of course, you can choose to override any of the values to create a unique object.
As with all other areas of C#, you can define constants in structures to serve as human readable forms of values that don’t change. For example, you might choose to create a generic product constant like this:
public const int genericProduct = 22;
Creating a new message now might look like this:
Message myMsg3 = new Message(3, Message.genericProduct);
Console.WriteLine(
$"In response to Msg {myMsg3.MsgID}, you can get {myMsg3.Qty} " +
$"of {myMsg3.ProductID} for ${myMsg3.Price}.");
The new form is easier to read. However, it doesn’t produce different results.
Structures can often benefit from the addition of methods that help you perform specific tasks with them. For example, you might want to provide a method for calculating the Message
Price
field, rather than perform the task manually every time. Using a method would ensure that a change in calculation method appears only once in your code, rather than each time the application requires the calculation. The CalculatePrice()
method looks like this:
public static double CalculatePrice(double SinglePrice, int Qty)
{
return SinglePrice * Qty;
}
Obviously, most calculations aren't this simple, but you get the idea. Moving the code to a method means that you can change the other parts of the code to make its meaning clearer. For example, the Message()
constructor if
statement now looks like this:
// Defined by the application.
if (ProductID == 22)
Price = CalculatePrice(5.99, qty);
else
Price = CalculatePrice(6.99, qty);
You can also use properties with structures. In fact, using properties is the recommended approach in many cases because using properties lets you ensure that input values are correct. Fortunately, if you are using C# 7.0 or above and originally created public fields, you can turn them into properties quite easily using these steps:
Place the cursor (insertion point) anywhere on the line of code you want to turn into a property.
In this case, place it anywhere on the line of code that reads: public int MsgID;
. A screwdriver icon appears in the left margin of the editing area.
Hover your mouse cursor over the top of the screwdriver to show the down arrow next to the icon, and click the down arrow.
You see options associated with the field. The highlighted option, Encapsulate Field: 'MsgID’ (And Use Property), lets you turn MsgID into a property and use it appropriately in your code.
Click the Encapsulate Field: ‘MsgID’ (And Use Property) option.
Visual Studio turns the field into a property by making the changes shown in bold in the following code:
private int msgID;
public int ProductID;
public int Qty;
public double Price;
public const int genericProduct = 22;
public int MsgID { get => msgID; set => msgID = value; }
The “Including common struct elements” section, earlier in this chapter, discusses elements that you commonly use with both classes and structures to perform essential tasks. The ColorList
example found in the following sections describes some supplemental elements that will enhance your use of structures.
An indexer allows you to treat a structure like an array. In fact, you must use many of the same techniques with it, but you must also create a lot of the features from scratch because your structure indexer has flexibility that an array doesn't provide. Here’s the code for a structure, ColorList
, that has the basic features required for an indexer (note that you must add the using System.Linq;
directive to the beginning of your code for this example):
public struct ColorList
{
private string[] names;
public string this[int i]
{ get => names[i]; set => names[i] = value; }
public void Add(string ColorName)
{
if (names == null)
{
names = new string[1];
names[0] = ColorName;
}
else
{
names = names.Concat<string>(
new string[] { ColorName }).ToArray();
}
}
public int Length
{ get => names.Length; }
}
When working with an indexer associated with a class, you assign a starting value to the array. However, you can’t do that in this case because this is a structure, so names
remains uninitialized. However, you can override the default constructor, so you can initialize names
there. The Add()
method provides the solution. To add a new member to names
, a caller must provide a string
that adds the value to names
as shown.
Note that when names is null
, Add()
first initializes the array and then adds the color to the first element (given that there are no other elements). However, when names
already has values, the code concatenates a new single element string
array to names
. You must call ToArray()
to convert the enumerable type used with Concat()
to an array for storage in names
.
To use ColorList
in a real application, you must also provide a means of obtaining the array length. The read-only Length
property accomplishes this task by exposing the names.Length
property value. Here is an example of ColorList
in action:
static void Main(string[] args)
{
// Create a color list.
ColorList myList = new ColorList();
// Fill it with values.
myList.Add("Yellow");
myList.Add("Blue");
// Display each of the elements in turn.
for (int i = 0; i < myList.Length; i++)
Console.WriteLine("Color = " + myList[i]);
Console.ReadLine();
}
The code works much as you might expect for a custom array. You create a new ColorList
, rely on Add()
to add values to it, and then use Length
within a for
loop to display the values. Here's the output from this code:
Color = Yellow
Color = Blue
Structures can also contain operators. For example, you might choose to create a method for adding two ColorList
structures together. You do that by creating a +
operator. Note that you're creating, not overriding, the +
operator, as shown here:
public static ColorList operator + (ColorList First, ColorList Second)
{
ColorList Output = new ColorList();
for (int i = 0; i < First.Length; i++)
Output.Add(First[i]);
for (int i = 0; i < Second.Length; i++)
Output.Add(Second[i]);
return Output;
}
You can’t create an instance operator. It must appear as part of the struct
, as shown. The process follows the same technique you use to create a ColorList
in the first place. The difference is that you iterate through both ColorList
variables to perform the task using a for
loop. Here's some code that uses the +
operator to add two ColorList
variables.
// Create and fill a second color list.
ColorList myList2 = new ColorList();
myList2.Add("Red");
myList2.Add("Purple");
// Add the first list to the second.
ColorList myList3 = myList + myList2;
// Display each of the elements in turn.
Console.WriteLine("
Combined Color Lists
");
for (int i = 0; i < myList3.Length; i++)
Console.WriteLine("myList3 Color = " + myList3[i]);
As you can see, myList3
is the result of adding two other ColorList
variables, not of creating a new one. The output is as you'd expect:
myList3 Color = Yellow
myList3 Color = Blue
myList3 Color = Red
myList3 Color = Purple
Starting with C# 7.2, you can create read-only structures. The main reason to use a read-only structure is to improve application performance. The article at https://devblogs.microsoft.com/premier-developer/the-in-modifier-and-the-readonly-structs-in-c/
demonstrates one way in which this performance improvement occurs. The point is that you can use such a structure to model complex data that the application can’t change after it creates the structure.
public readonly struct ReadOnlyData
{
// Create properties to hold values.
public readonly int Value { get; }
// Define a constructor to assign values
// to the properties.
public ReadOnlyData(int n)
{
Value = n;
}
}
To create a read-only structure, you add the readonly
keyword before struct
, as shown. The auto-constructed Value
property is read-only by default. However, adding readonly
to it reminds users that it isn't possible to assign a value to Value
outside of the constructor. The following code shows how you might use a read-only structure:
static void Main(string[] args)
{
// Define some data.
int[] Data = Enumerable.Range(1, 5).ToArray();
// Create the read-only structure.
ReadOnlyData MyReadOnlyData = new ReadOnlyData(10);
// Perform a task with the structure.
int Result = 0;
foreach (int n in Data)
{
Result += n + MyReadOnlyData.Value;
Console.WriteLine(
$"n = {n} Value = {MyReadOnlyData.Value} " +
$"Result = {Result}");
}
Console.ReadLine();
}
To compile this code, you need to add using System.Linq;
to the top of the file to make Enumerable.Range()
accessible. When you run this code, you see the following output:
n = 1 Value = 10 Result = 11
n = 2 Value = 10 Result = 23
n = 3 Value = 10 Result = 36
n = 4 Value = 10 Result = 50
n = 5 Value = 10 Result = 65
// Use a C# 9.0 construction.
ReadOnlyData MyReadOnlyData2 = new ReadOnlyData
{
Value = 12
};
This approach is clearer than using the constructor to assign values to the structure properties. However, the end result is the same — you can't reassign values after the initial construction.
The previous section talks about a performance enhancement to using structures in the form of reduced access. The reference structure that first appears in C# 7.2 is another way to make structures more efficient and faster. In fact, you can combine this form of structure with a read-only structure to greatly improve structure performance, but at the cost of a huge loss of flexibility. The reference structure always remains on the stack, which means that you can’t box it and put it on the heap, even accidentally. Consequently, a reference structure eliminates the potential for performance losses resulting from boxing and unboxing. However, this approach also comes with these limitations:
struct
System.ValueType
or System.Object
All these limitations come as a result of not being able to move the structure from the stack to the heap. However, you can use a reference structure variable in a synchronous method such as those that return Task
or Task<TResult>
. The following code, found in the RefStruct
example, shows how to create a reference structure:
public ref struct FullName
{
public string First { get; set; }
public string Middle { get; set; }
public string Last { get; set; }
public override string ToString()
{
return $"Name: {First} {Middle} {Last}";
}
}
To use this structure, you work with it in essentially the same way as you do for any other structure, like this:
static void Main(string[] args)
{
FullName ThisName = new FullName
{
First = "Sam",
Middle = "L",
Last = "Johnson"
};
Console.WriteLine(ThisName.ToString());
Console.ReadLine();
}
The main reason to work with structures in most code is to create records that contain custom data. You use these custom data records to hold complex information and pass it around as needed within your application. It's easier and faster to pass a single record than it is to pass a collection of data values, especially when your application performs the task regularly. The following sections show how to use structures as a kind of data record.
Passing structures to methods is cleaner and easier than passing individual data items. Of course, the values in the structure must be related in order for this strategy to work well. However, consider the following method:
static void DisplayMessage(Message msg)
{
Console.WriteLine(
$"In response to Msg {myMsg.MsgID}, you can get {myMsg.Qty} " +
$"of {myMsg.ProductID} for ${myMsg.Price}.");
}
Applications rarely use a single data record for every purpose. In most cases, applications also include database-like collections of records. For example, an application is unlikely to receive just one Message
. Instead, the application will likely receive a group of Message
records, each of which it must process.
Working with an array of structures is much like working with an array of anything else. You could use code like this to create an array of
structures: Message
// Display all the messages on screen.
Message[] Msgs = { myMsg, myMsg2 };
DisplayMessages(Msgs);
In this case, Msgs
contains two records, myMsg
and myMsg2
. The code then processes the messages by passing the array to DisplayMessages()
, which is shown here:
static void DisplayMessages(Message[] msgs)
{
foreach (Message item in msgs)
{
Console.WriteLine(
$"In response to Msg {myMsg.MsgID}, you can get {myMsg.Qty} " +
$"of {myMsg.ProductID} for ${myMsg.Price}.");
}
}
The DisplayMessages()
method uses a foreach
loop to separate the individual Message
records. It then processes them using the same approach as DisplayMessage()
in the previous section of the chapter.
Structures.Program+Messages
The output isn't useful because it doesn’t tell you anything. To garner anything useful, you must override the ToString()
method by using code like this:
public override string ToString()
{
// Create a useful output string.
return "Message ID: " + MsgID +
"
Product ID: " + ProductID +
"
Quantity: " + Qty +
"
Total Price: " + Price;
}
Now when you call ToString()
, you obtain useful information. In this case, you see the following output when calling myMsg.ToString()
:
Message ID: 1
Product ID: 22
Quantity: 5
Total Price: 29.95
Using structures as records proved so helpful that in C# 9.0 you find a new record
type. Rather than force you to write a bunch of code to obtain the same effect, the record type combines the best features of structures and classes to provide you with the means of defining records along the same lines as those described in the “Using Structures as Records” section of the chapter. However, there are some significant differences as described in the sections that follow and shown in the BasicRecord
example. Note that you must use the .NET Core template with .NET 5.0 for this example to obtain the proper support.
As previously mentioned, a record is a combination of a structure and a class with a little secret sauce added so that you experience the delicious taste of records without the coding. A record has these properties:
with
expression.ToString()
method that displays:
A record can look remarkably similar to a structure, except for the use of the record
keyword, as shown here:
public record Person
{
public string First { get; set; }
public string? Middle { get; set; }
public string Last { get; set; }
public int? Age { get; set; }
}
static void Main(string[] args)
{
Person ThisPerson = new Person()
{
First = "Amanda",
Middle = null,
Last = "Langley",
Age = null
};
Console.WriteLine(ThisPerson.ToString());
Console.ReadLine();
}
Unlike classes and structures, a record provides the means to print itself out in a meaningful way through the ToString()
method. Here’s the default output you see:
Person { First = Amanda, Middle = , Last = Langley, Age = }
However, you likely want to add a ToString()
method, especially if you have nullable properties. The following ToString()
method makes use of the nullable Age
value to determine what to print:
public override string ToString()
{
if (Age.HasValue)
return $"{Last}, {First} {Middle}
Age: {Age}";
else
return $"{Last}, {First} {Middle}
Age Withheld";
}
Notice the use of the HasValue
property to determine whether Age
is null
. The output now looks like this:
Langley, Amanda
Age Withheld
Records provide a level of flexibility in declaration that you don't find with classes or structures. For example, you can create a positional declaration with relative ease, as shown here:
public record Person2(string First, string Middle, string Last, int? Age)
{
public override string ToString()
{
if (Age.HasValue)
return $"{Last}, {First} {Middle}
Age: {Age}";
else
return $"{Last}, {First} {Middle}
Age Withheld";
}
}
This code is significantly shorter than the declaration in the previous section, yet it does the same thing with a little twist. You can now create the record using a shorter syntax as well:
Person2 NextPerson = new("Andy", "X", "Rustic", 42);
The record is instantiated on a single line without writing any code for a special constructor. The point is that you waste a lot less time writing code, yet get flexible records for data processing.
Like a structure, a record compares the kind of record and the values it contains when making an equality decision. This means that you can compare two different records quickly and easily. Here is an example of how this comparison works:
Person2 ThirdPerson = new("Andy", "X", "Rustic", 42);
Console.WriteLine($"NextPerson == ThirdPerson: " +
$"{NextPerson == ThirdPerson}");
Console.WriteLine($"ReferenceEquals(NextPerson, ThirdPerson): " +
$"{ReferenceEquals(NextPerson, ThirdPerson)}");
When you run this code, you see the following output:
NextPerson == ThirdPerson: True
ReferenceEquals(NextPerson, ThirdPerson): False
NextPerson
and ThirdPerson
are two completely different objects. However, you can compare them to verify that they contain the same record using the ==
operator. If this were a class, you'd need to compare the value of each property individually, which is time consuming and error prone.
There are times when two records might be almost the same, but just a little different. In such a case, you can mutate a current record into a new record using this technique:
Person2 FourthPerson = ThirdPerson with { Age = null };
Console.WriteLine(FourthPerson);
In this case, FourthPerson
would be just like ThirdPerson
, but with a different Age
property value. When you run this code, you see the following output:
Rustic, Andy X
Age Withheld
The get; set;
syntax used by auto-implemented properties is fine as long as you don't need to do anything other than get or set a value. Otherwise, you need to create a backing field and add a lot more code to your properties. C# 10.0 introduces the field
keyword to reduce or eliminate the use of backing fields. For example, in the following record, it’s possible to ensure that Department
is stored in uppercase without resorting to the use of a backing field:
public record Person
{
public string First { get; set; }
public string? Middle { get; set; }
public string Last { get; set; }
public int? Age { get; set; }
public string Department { get; set => field = value.ToUpper(); }
}
This feature only works if you configure your project to use C# 10.0 in the .csproj
file like this:
<PropertyGroup>
<LangVersion>10.0</LangVersion>
</PropertyGroup>
The field keyword makes it easy to keep things short and simple. You can use it with get;
, set;
, and init;
as needed. Don’t worry if you can’t fit what you need on a single line. You can use an extended version like this as well:
public record Person
{
public string First { get; set; }
public string? Middle { get; set; }
public string Last { get; set; }
public int? Age { get; set; }
public string Department { get;
set
{
if (value.Trim() == "")
throw new ArgumentException("No blank strings");
field = value.ToUpper();
}
}
}
This second form still doesn’t require the use of a backing field, and it tends to be shorter than the older version of the code. The point is that you shouldn’t have to use backing fields very often anymore with C# 10.0 and the .NET 6.0 framework (it doesn’t work with .NET 5.0).