Chapter 11

Interacting with Structures

IN THIS CHAPTER

Bullet Determining when to use structures

Bullet Defining structures

Bullet Working with structures

Bullet 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, records 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.

Remember You don't have to type the source code for this chapter manually. In fact, using the downloadable source is a lot easier. You can find the source for this chapter in the CSAIO4D2EBK02CH11 folder of the downloadable source. See the Introduction for details on how to find these source files.

Comparing Structures to Classes

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.

Remember You find many schools of thought on the use of structures. This book doesn’t even attempt to cover them all. It does give you a good overview of how structures can help you create better applications. After you have this information, you can begin using structures and discover for yourself precisely how you want to interact with them.

Considering struct limits

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

  • Can have constructors, but not destructors. This means that you can perform all the usual tasks required to create a specific data type, but you don't have control over cleaning up through a destructor.
  • Cannot inherit from other structures or classes (meaning they’re stand-alone).
  • Can implement one or more interfaces, but with the limits imposed by the elements they support (see “Including common struct elements,” later in this chapter, for details).
  • Cannot be defined as abstract, virtual, or protected.

Understanding the value type difference

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.

Tip The differences in resource use and in both allocation and deallocation time is compounded when working with arrays. An array of reference types incurs a huge penalty because the array contains just pointers to the individual objects. To access the object, the application must then look for it on the heap.

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.

Determining when to use struct versus class

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.

Remember Using a value type also changes the way in which C# interacts with the variable. A reference type is passed by reference during a call so that any changes made to the reference type appear in all instances that point to that reference. A value type is copied when you pass it because it's passed by value. This means that changes you make to the value type in another method don’t appear in the original variable. This is possibly the most confusing aspect of using structures for developers because passing an object created by a class is inherently different from passing a variable created by a structure. This difference makes classes generally more efficient to pass than structures.

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 Structures

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.

Defining a basic struct

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 structs 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.

Remember Obviously, this is a simplified example, and you'd never create code like this for a real application, but it does get the process you use across. When working with structures, think about the processes you use with classes, but with a few differences that can make structures far more efficient to use.

Including common struct elements

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.

Constructors

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;
}
}

Remember Note that the constructor accepts default values, so you can use a single constructor in more than one way. When you use the new version of Message, IntelliSense shows both the default constructor (which, in contrast to a class, doesn't go away) and the new constructor that you created, as shown here:

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.

Constants

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.

Methods

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);

Remember Note that you must declare the CalculatePrice() method static or you receive an error message. A structure, like a class, can have both static and instance methods. The instance methods become available only after you create an instance of the structure.

Properties

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:

  1. 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.

  2. 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.

  3. 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; }

Tip At this point, you can work with the property as needed to safeguard your data. However, one other issue is still there. If you try to compile the code now, you see a CS0188 error code in the constructor telling you that you’re trying to use the property before the fields are assigned. To correct this problem, change the assignment MsgID = msgId; in the constructor to msgID = msgId;. The difference is that you assign a value to the private field now, rather than use the public property.

Using supplemental struct elements

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.

Indexers

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; }
}

Remember Starting from the top of the listing, an indexer implies that you have an array, list, or some other data structure somewhere in the struct, which is names in this case. To access names using an indexer, you must also create a this property of the type shown in the example. The this property enables you to access specific names array elements. Note that this example is using a really simple this property; a production version would add all sorts of checks, including verifying that names isn't null and that the requested value actually exists.

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

Operators

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

Working with Read-only Structures

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.

Tip A second, less obvious reason to use read-only structures is to provide thread safety. When working with huge amounts of data, the ability to move data between processors without concern for state changes is critical. You can also use read-only structures to provide a constant hash value for cryptographic needs. The ReadOnlyStruct example shows how to create a read-only structure like the one shown here (note that if you use the standard .NET Framework, you must add the IsExternalInit code shown in the “Working with init-only setters” section of Chapter 4 of this minibook, as well as set your language version in the ReadOnlyStruct.csproj file to a minimum of 7.2):

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

Tip As of C# 9.0, you can add an init accessor to your read-only structure, public readonly int Value { get; init; }, so that it allows an alternative method of assigning values to the properties, like this:

// 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.

Working with Reference Structures

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:

  • No array element support
  • Unable to declare it as a type of a field of a class or a non-ref struct
  • No interface implementation
  • No boxing to System.ValueType or System.Object
  • Unable to use it as a type argument
  • Ineligible for capture by a lambda expression or a local function
  • Inaccessible in an async method
  • No iterator support

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();
}

Using Structures as Records

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.

Managing a single 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}.");
}

Tip In this case, the DisplayMessage() method receives a single input of type Message instead of the four variables that the method would normally require. Using the Message structure produces these positive results in the code:

  • The receiving method can assume that all the required data values are present.
  • The receiving method can assume that all the variables are initialized.
  • The caller is less likely to create erroneous code.
  • Other developers can read the code with greater ease.
  • Code changes are easier to make.

Adding structures to arrays

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.

Technicalstuff You can add structures to any collection. However, most collections work with objects, so adding a structure to them would incur a performance penalty because C# must box and unbox each structure individually. As the size of the collection increases, the penalty becomes quite noticeable. Consequently, it's always a better idea to restrict collections of data records that rely on structures to arrays in your application when speed is the most important concern.

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 Message structures:

// 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.

Overriding methods

Remember Structures provide a great deal of flexibility that many developers assign exclusively to classes. For example, you can override methods, often in ways that make the structure output infinitely better. A good example is the ToString() method, which outputs a somewhat unhelpful (or something similar):

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 the New Record Type

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.

Comparing records to structures and classes

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:

  • The ability to define immutable properties so that a record is more secure than either a structure or class.
  • A record uses value equality so that two records with the same structure and the same properties are the same. This differs from a class, which depends on reference equality (looking for pointers to the same memory location).
  • You can use non-destructive mutation to create a new record (with different property values) based on an existing record. To perform this task, you use the with expression.
  • Unlike many other C# objects, the record provides a ToString() method that displays:
    • The record type name
    • The names and values of public properties
  • A record is a kind of class under the covers, so, unlike a structure, it supports inheritance hierarchies.

Working with a record

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; }
}

Tip The question marks after the type declaration for the Middle and Age properties means that these properties are nullable. You'll see how this works to your advantage a little later in this section. However, for now, try this code to work with the structure:

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

Using the positional syntax for property definition

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.

Understanding value equality

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.

Creating safe changes: Nondestructive mutation

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

Using the field keyword

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).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset