In the previous chapter, we looked at serialization, which allows us to store the state of an object, where an object is an instance of a specific class. We looked at different ways to serialize and store the data, and two such formats were XML and JSON. We also learned how to deserialize the serialized data, which is usually stored in a file, and therefore convert it to the same class structure it was in originally. The reason we serialize an object or objects is to allow it to be transferred across a network or shared with other people. Serialization also works across programming languages
. In Chapter 13 we gained knowledge of classes and objects, which formed the foundations for the serialization and deserialization processes. Classes are a structure, and in this chapter we will extend our knowledge of structures by looking at the C# struct.
In C#
a struct is a lightweight alternative to a class, but it is not a class. The struct can have members and methods, or put another way, we say it can have data and functionality. A struct in C# is defined using the keyword struct. Interestingly, we have, without thinking, used simple structs already when coding
our applications, since all the value types we have used – for example, int, float, double, bool, char, etc. – are structs. Before we begin, let us take a look at the difference between value types and reference types in relation to structs and classes.
Value and Reference Types
Structs are value types
, whereas a class is a reference type, and they are dealt with by the runtime in different ways:
When a value type instance is made, there is one single space allocated in memory to store the value. We have discussed primitive data types such as int and float and these are value types like a struct. When the runtime deals with the value type, it is dealing directly with the data.
On the other hand, with reference types, an object is created in memory and dealt with through a pointer.
Example for a Struct
CustomerStruct myCustomerStruct = new CustomerStruct();
Here one single space is allocated in memory to store the myCustomerStruct, and if we were to copy the struct object to a new variable as
then myCustomer2Struct would be completely independent of myCustomerStruct and have its own fields.
Example for a Class
CustomerClass myCustomerClass= new CustomerClass();
Here two spaces are allocated in memory. One stores the CustomerClass object, and the other stores its reference myCustomerClass. So we could show the example code like this:
CustomerClass myCustomerClass;
myCustomerClass = new CustomerClass();
Now if we were to copy the class object to a new variable as
CustomerClass myCustomer2Class = myCustomerClass;
then myCustomer2Class would be simply a copy of the reference CustomerClass, and therefore myCustomerClass and myCustomer2Class point to the same object.
Difference Between Struct and Class
Some ways in which structs differ from classes
are as follows:
Structs are value types, and we use the actual struct. On the other hand, a class is a reference type, and we point to the actual class.
Structs cannot be coded by us to have a constructor that has no parameters, a default constructor
, since the default constructor is automatically defined and not available to change.
Structs cannot inherit from other structs or classes, whereas a class can inherit from other classes.
Structs can implement one or more interfaces
.
Structs cannot declare a finalizer
, which is a destructor used to garbage collect.
If we use the new keyword to create an object of the struct, the default constructor
is called and the object is created. A struct can be instantiated without using the new keyword, whereas a class cannot be instantiated without using the new keyword.
If the struct is used without the new keyword, the members of the struct will remain unassigned and we cannot use the struct object until we have initialized all the members.
Members, fields, cannot be initialized, for example, public int policy_number = 0; causes an error in C# 10 or lower.
In writing our code, there may be times when we wish to access an object directly, in the same way that value types are accessed. To address these concerns, C# offers the structure as described previously and, in our programming, we would use structs to represent more simple data structures. The format of the struct declaration
is
struct name
{
member declarations
constructor if required
}
Let's code some C# and build our programming muscle.
We will create a Customer struct in a similar manner to the Customer class we created when we completed Chapter 13 on classes and objects.
Add a new project to hold the code for this chapter.
1.
Right-click the solution CoreCSharp.
2.
Choose Add.
3.
Choose New Project.
4.
Choose Console App from the listed templates that appear.
5.
Click the Next button.
6.
Name the project Chapter19 and leave it in the same location.
7.
Click the Next button.
8.
Choose the framework to be used, which in our projects will be .NET 6.0 or higher.
9.
Click the Create button.
Now we should see the Chapter19 project within the solution called CoreCSharp.
10.
Right-click the Chapter19 project in the Solution Explorer panel.
11.
Click the Set as Startup Project option.
Notice how the Chapter19 project name has been made to have bold text, indicating that it is the new startup project and that it is the Program.cs file within it that will be executed when we run the debugging.
12.
Right-click the Program.cs file in the Solution Explorer window.
13.
Choose Rename.
14.
Change the name to CustomerExample.cs.
15.
Press the Enter key
16.
Double-click the CustomerExample.cs file to open it in the editor window.
Struct with a Default Constructor Only
We will now add a Main() method. Then we will add a struct called Customer
, which is outside the class but inside the namespace and will hold the variables we require. In the code in Listing 19-1, the Customer struct
has been created above the Main() method
, but it could also have been created below the Main() method. The Customer struct will not be given a constructor in our code, but the default constructor still exists.
We will now add code inside the Main() method to create an instance of the Customer struct, then assign values to the Customer struct fields, and then display the Customer struct values in a formatted display. This should all look familiar to us as we have coded applications using classes and objects.
Assign values to the Customer struct fields and display the data
19.
Click the File menu.
20.
Choose Save All.
21.
Click the Debug menu.
22.
Choose Start Without Debugging.
The console window will appear, as shown in Figure 19-1, and show details of this Customer instance.
23.
Press the Enter key to close the console window.
Struct with a User Constructor
As we have seen, a C# struct is a value type. This means that when we create an instance
of the struct, we must pass to it values for each of its members. This can be achieved by
Using the default constructor
method to give default values of the members
Having a user-defined constructor method to assign values to the members
We will code an example of an insurance policy that we might have for our home building insurance, home contents insurance, car insurance, travel insurance, etc. All these policy types will have common data that needs to be stored for the specific customer policy, the policy object. We will create a sample C# policy struct and include a constructor
, which we did not use in the last example.
24.
Right-click the Chapter19 project in the Solution Explorer window.
25.
Choose Add.
26.
Choose Class.
27.
Name the class PolicyExample.cs.
28.
Open the PolicyExample.cs file in the editor window.
29.
Amend the code, as in Listing 19-3, to create a Policy struct outside the class and inside the namespace.
namespace Chapter19
{
struct Policy
{
public int policyNumber;
public string policyType;
public double monthlyPremium;
public string policyEndDate;
} // End of Policy struct
internal class PolicyExample
{
} // End of PolicyExample class
} // End of Chapter19 namespace
Listing 19-3
Create a struct called Policy
Struct Instantiation Without the New Keyword
We will now create a Main() method
inside the class and instantiate the struct WITHOUT using the new keyword
, and then we will display the four struct values in the console.
if the struct is used without the new keyword the members
of the struct will remain unassigned, no values, and
we cannot use the struct object until we have
initialised all the members
Using the code below we will see that
- myPolicy is an instance of Policy
- myPolicy.PolicyNumber is unassigned but
- doing myPolicy.PolicyNumber = 123456 means it exists
- same applies for all members
*/
Policy myPolicy;
Console.WriteLine(myPolicy.policyNumber);
Console.WriteLine(myPolicy.policyType);
Console.WriteLine(myPolicy.monthlyPremium);
Console.WriteLine(myPolicy.policyEndDate);
} // End of Main() method
} // End of PolicyExample class
Listing 19-4
Instantiate the Policy struct without using the new keyword
Now if we look at the code, we will see that under the four struct member names, we will have a red underline indicating there are errors
.
31.
Hover over the policyNumber code that has the red underline and look at the error message, as shown in Figure 19-2.
The message tells us what we already know from reading the earlier sections of this chapter:
If the struct is used without the new keyword, the members of the struct will remain unassigned and we cannot use the struct object until we have initialized all the members.
Struct Instantiation with the New Keyword
We will now instantiate the struct WITH the new keyword and then display the four struct
values to the console.
32.
Amend the code, as in Listing 19-5. Only one line of code needs amended.
Policy myPolicy = new Policy();
Console.WriteLine(myPolicy.policyNumber);
Console.WriteLine(myPolicy.policyType);
Console.WriteLine(myPolicy.monthlyPremium);
Console.WriteLine(myPolicy.policyEndDate);
} // End of Main() method
Listing 19-5
Instantiate the Policy struct using the new keyword
As we will see, the four struct member names are error-free. We read earlier that if we use the new keyword to create an object of the struct, the default constructor is called and the object is created, and the members will be assigned the default value for their specific data type. The default values for our struct members will be 0 for an int and null for a string, which will therefore display nothing. Let us see if this is the case by running the code.
33.
Right-click the Chapter19 project in the Solution Explorer panel.
34.
Choose Properties from the pop-up menu.
35.
Choose the Chapter19.PolicyExample class in the Startup object drop-down list.
36.
Close the Properties window.
37.
Click the File menu.
38.
Choose Save All.
39.
Click the Debug menu.
40.
Choose Start Without Debugging.
The console window will appear, as shown in Figure 19-3, and show details of the policy. The display shows the default values of 0 for the int and double and null for the two strings.
41.
Press the Enter key to close the console window.
Creating a Constructor
The default constructor
is automatically defined and not available for change. Remember, a default constructor is parameterless; it accepts no values. However, a struct allows a constructor to be added that contains parameters.
We will now add a custom constructor to the struct and this constructor will accept values for all the struct members. The constructor will therefore have four parameters, which will accept the values passed to it when a new instance of the struct is created.
public Policy(int policyNumber, string policyType,
double monthlyPremium, string policyEndDate)
{
this.policyNumber = policyNumber;
this.policyType = policyType;
this.monthlyPremium = monthlyPremium;
this.policyEndDate = policyEndDate;
} // End of user constructor
} // End of Policy struct
Listing 19-6
Add a constructor to the struct
We should note that the existing code we have, where we are creating an instance of the Policy, Policy myPolicyNew = new Policy();, still works because the default constructor still exists. However, we will amend this code line to pass the new constructor four values for this new policy.
43.
Amend the code to add the four values to be passed to the constructor of the struct. Only the one line of code needs amended, as Listing 19-7:
Policy myPolicy = new Policy(123456, "Computer Hardware", 9.99, "31/12/2021");
Console.WriteLine(myPolicy.policyNumber);
Console.WriteLine(myPolicy.policyType);
Console.WriteLine(myPolicy.monthlyPremium);
Console.WriteLine(myPolicy.policyEndDate);
} // End of Main() method
} // End of PolicyExample class
Listing 19-7
Instantiate using the custom constructor
44.
Click the File menu.
45.
Choose Save All.
46.
Click the Debug menu.
47.
Choose Start Without Debugging.
The console window will appear, as shown in Figure 19-4, and show details of the policy with the new initialized values passed to the constructor from the instance.
48.
Press the Enter key to close the console window.
Creating Member Properties (Get and Set Accessors)
Members of a struct
can, like members of a class, have a property, which is used to get and set the value of the member. We saw this being used in Chapter 13 on classes, so now we will see it in action with structs. We also saw in the last chapter that there can be several flavors of properties.
49.
Amend the code, as in Listing 19-8, to make the members of the struct have access modifiers of private.
struct Policy
{
private int policyNumber;
private string policyType;
private double monthlyPremium;
private string policyEndDate;
Listing 19-8
Make members private
Look at the display lines of the code where we have used the dot notation to access the struct members, and we will see that we have errors
. Hovering over one of the errors, as shown in Figure 19-5, tells us what we should realize the member is not accessible as it is private to the struct.
We will now create the property for each member, and because we are more experienced in our understanding of C# code, we will use the Visual Studio 2020 help, in the form of Quick Actions and Refactorings, which we discussed in Chapter 6.
50.
Highlight the four members of the struct.
51.
Right-click in the highlighted code.
52.
Choose Quick Actions and Refactorings as shown in Figure 19-6.
53.
Choose Encapsulate field (but still use field).
The property for each member will appear either above or below the constructor. Either is fine, as shown in Listing 19-9. The struct will now have the members, the constructor, and the properties, just like a class can have these.
struct Policy
{
private int policyNumber;
private string policyType;
private double monthlyPremium;
private string policyEndDate;
public Policy(int policyNumber, string policyType,
double monthlyPremium, string policyEndDate)
{
this.policyNumber = policyNumber;
this.policyType = policyType;
this.monthlyPremium = monthlyPremium;
this.policyEndDate = policyEndDate;
} // End of user constructor
public int PolicyNumber
{
get => policyNumber;
set => policyNumber = value;
}
public string PolicyType
{
get => policyType;
set => policyType = value;
}
public double MonthlyPremium
{
get => monthlyPremium;
set => monthlyPremium = value;
}
public string PolicyEndDate
{
get => policyEndDate;
set => policyEndDate = value;
}
} // End of Policy struct
Listing 19-9
Member properties created, the get and the set
Now we need to change the dot notation in the WriteLine() code
lines that refer to the members directly and refer them instead to the property of the member. The property uses the capitalized form of the member.
54.
Amend the code, as in Listing 19-10, to refer to the property.
Console.WriteLine(myPolicy.PolicyNumber);
Console.WriteLine(myPolicy.PolicyType);
Console.WriteLine(myPolicy.MonthlyPremium);
Console.WriteLine(myPolicy.PolicyEndDate);
} // End of Main() method
} // End of PolicyExample class
} // End of Chapter19 namespace
Listing 19-10
Call the property, not the member, with a capital letter
55.
Click the File menu.
56.
Choose Save All.
57.
Click the Debug menu.
58.
Choose Start Without Debugging.
The console window will appear, as shown in Figure 19-7, and details of the policy will be displayed.
59.
Press the Enter key to close the console window.
Encapsulation
The code we have just run produced the same result
as the previous example, but we have made use of the properties, get and set, for the private variables. This has therefore given us a little exposure to encapsulation, where we refer to encapsulation as hiding the variables of a class or struct so that they can only be accessed from outside the class or struct by special methods called properties. We achieve encapsulation, as we have just seen, by making the access modifier of the fields private and then generating a property for each member. We do not need to have a property for every member if we do not want to, and we do not have to have a get and a set in each property. This will depend on what we wish for the given situation.
A struct can include concrete methods just like classes can include concrete methods. We have not included methods in our examples, but it would be easy to do so, in the same way we did when we coded the applications in Chapter 13 on classes and objects.
Readonly Struct
When we want to limit access to the struct data, we can make the struct readonly. The keyword readonly is therefore a modifier indicating that something cannot be changed. With structs we can use the readonly modifier with the
struct – In which case the whole struct is readonly. We could therefore say that the structure is immutable; it is “final.” In this case all the members are readonly, and consequently every member property does not require a set accessor, a setter – remember YAGNI. The only way to change the member values is at the time of creating the instance object, which calls the constructor whose purpose is to set the initial values of the members. The members of the readonly struct are readonly, and therefore no method in the struct can change its value, which essentially means that we will not see a statement like myPolicy.PolicyType = "Jewellery".
Member of the struct – From C# 8, a member can be set to readonly, in which case the value of the member cannot be changed. If the member is readonly, then its property does not require a set accessor, a setter. Allowing readonly members means we do not need to make the whole struct readonly.
Creating a Readonly Struct
We will use the PolicyExample
as our starting point for this exercise on readonly structs.
60.
Open the PolicyExample.cs file in the Chapter19 project.
61.
Amend the code, as in Listing 19-11, to make the Policy struct readonly.
readonly struct Policy
{
private int policyNumber;
private String policyType;
private double monthlyPremium;
private String policyEndDate;
Listing 19-11
Readonly struct
Now we will see that there are errors indicated under the struct members because they will need to be made readonly as well. Hovering over the error
will display the error message, as shown in Figure 19-8.
62.
Amend the code, as in Listing 19-12, to make the member fields readonly.
readonly struct Policy
{
readonly int policyNumber;
readonly String policyType;
readonly double monthlyPremium;
readonly String policyEndDate;
Listing 19-12
Readonly fields
Now we will see that the errors for the members are removed. However, we will also see errors in the set accessor of each member property. We will now correct the set errors by commenting the code. This is not a normal programming practice (YAGNI), but we will keep the code for reference.
63.
Amend the code, as in Listing 19-13, to comment the setters.
public int PolicyNumber
{
get => policyNumber;
//set => policyNumber = value;
}
public string PolicyType
{
get => policyType;
//set => policyType = value;
}
public double MonthlyPremium
{
get => monthlyPremium;
//set => monthlyPremium = value;
}
public string PolicyEndDate
{
get => policyEndDate;
//set => policyEndDate = value;
}
} // End of Policy struct
Listing 19-13
Setters are not required; comment them
Within the Main() method
, the code that uses the members is accessing them through the property of the member, really the get accessor of the property, so this will still work because we do have the getters but not the setters as the struct is readonly. Perfect! Our code should work well.
64.
Click the File menu.
65.
Choose Save All.
66.
Click the Debug menu.
67.
Choose Start Without Debugging.
The console window will appear displaying details of the policy, as shown in Figure 19-9.
68.
Press the Enter key to close the console window.
We have achieved the same result, but we have seen the use of a readonly struct, with readonly members and member properties that only require a get accessor
since the member values cannot be changed, unless it is done through the constructor. Now we will try and assign a new value to a readonly struct member.
69.
Amend the code in the Main() method to attempt to assign a new value to the policyNumber as shown in Listing 19-14.
Console.WriteLine(myPolicy.PolicyEndDate);
myPolicy.PolicyNumber =
} // End of Main() method
} // End of PolicyExample class
} // End of Chapter19 namespace
Listing 19-14
Attempt to assign a value to a readonly struct
Even before we have completed the line of code, we will see that the compiler
is complaining, and this is indicated in Visual Studio 2022 by the red underline. On hovering over the red underline, a pop-up window with a message appears, and we should note that we are being told it is not possible to assign a value, because we are dealing with a readonly struct member.
70.
Hover over the red underline of PolicyNumber as shown in Figure 19-10.
71.
Remove the partial line of code.
72.
Click the File menu.
73.
Choose Save All.
C# 8 readonly Members
Prior to C# 8 we could have a readonly struct and all the fields had to be readonly, as we have just seen in the example we coded. From C# 8 we were introduced to a new feature for the struct
, which allows us to declare members of the struct as readonly without the struct being readonly. This has the advantage of making the code more specific and more granular, because we target those fields that need to be readonly rather than having to make all the fields readonly.
We will now use the same Policy struct code we used earlier, to illustrate the use of the readonly field without the struct being readonly. As we already have a Policy struct in the namespace, we will have to rename the struct from Policy, so we will call it PolicyReadOnlyMembers.
74.
Right-click the Chapter19 project in the Solution Explorer window.
75.
Choose Add.
76.
Choose Class.
77.
Name the class PolicyExampleReadOnlyMember.cs.
We will now amend the code to add a struct, which will have two readonly fields, two private fields, and a property for each field with a get for all fields and a set for the non-readonly fields.
78.
Amend the PolicyExampleReadOnlyMember class code, as shown in Listing 19-15.
namespace Chapter19
{
struct PolicyReadOnlyMembers
{
private readonly int policy_number;
private string policyType;
private double monthlyPremium;
private readonly string policyEndDate;
public PolicyReadOnlyMembers(int policy_number,
string policyType, double monthlyPremium,
string policyEndDate)
{
this.policy_number = policy_number;
this.policyType = policyType;
this.monthlyPremium = monthlyPremium;
this.policyEndDate = policyEndDate;
} // End of user constructor
// Properties used to get and set the members
public int Policy_number
{
get => policy_number;
} // End of Policy_number property
public string PolicyType
{
get => policyType;
set => policyType = value;
} // End of PolicyType property
public double MonthlyPremium
{
get => monthlyPremium;
set => monthlyPremium = value;
} // End of MonthlyPremium property
public string PolicyEndDate
{
get => policyEndDate;
} // End of PolicyEndDate property
} // End of PolicyReadOnlyMembers struct
internal class PolicyExampleReadOnlyMember
{
} // End of PolicyExampleReadOnlyMember class
} // End of Chapter19 namespace
Listing 19-15
Creating the struct with readonly fields
We will now add a Main() method that will contain the code to instantiate the struct and have two lines of code to set the values of the two readonly fields.
Instantiate the struct and try to assign values to readonly fields
Now look at the assignment lines that are trying to set the values of the readonly fields; they have a red underline indicating a problem. If we hover over the red underline of any of the assignments, we will see a message telling us we cannot assign a value to a readonly field.
80.
Hover of the red underline of any of the assignment lines, and note the message, as shown in Figure 19-11.
This is the C# 8 readonly field of a struct in action. The struct is not readonly, but the fields marked with the readonly keyword are, and as we know, we cannot change the value of a readonly member.
81.
Comment the two lines that are causing the error
, as in Listing 19-17.
Comment the lines causing the error (or delete them)
82.
Right-click the Chapter19 project in the Solution Explorer panel.
83.
Choose Properties from the pop-up menu.
84.
Choose the PolicyExampleReadOnlyMember class in the Startup object drop-down list.
85.
Close the Properties window.
86.
Click the File menu.
87.
Choose Save All.
88.
Click the Debug menu.
89.
Choose Start Without Debugging.
Figure 19-12 shows the console window displaying the details of the policy.
So we have shown that readonly members cannot be assigned new values. The values are set through the custom constructor, if there is one, or the parameterless default constructor.
C
# 8 Nullable Reference Types
In Chapter 6 on data types, we looked at conversion and parsing, and we read about the C# 8 nullable reference type
. We read how we could use the ? to ensure that a null value was acceptable, given that from C# 8, reference types are non-nullable
by default. In our reading we used two code examples using strings, one using the ? and the other not using it:
Example 1 was
/*
This will cause a warning as all reference types are
non-nullable by default. The warning will be similar to:
Converting null literal or possible null value to
non-nullable reference type.
*/
string policyId = null;
Example 2 was
/*
This will not cause a warning as null is acceptable
*/
string? policyId = null;
For some reinforcement, we will apply the nullable reference type to the struct example we have just completed, but this will require the project to have Nullable enabled, which is done within the project’s .csproj file.
The Microsoft site
says
Null-state analysis and variable annotations are disabled by default for existing projects–meaning that all reference types continue to be nullable. Starting in .NET 6, they're enabled by default for new projects.
The line of code, <Nullable>enable</Nullable>, would need to be added to the .csproj file, but as we have been using .NET 6, we will not need to make this change, as Nullable is enabled by default. Listing 19-18 shows the contents of our Chapter19.csproj file, and we can see that Nullable is indeed enabled.
We can also enable or disable the nullable reference type within the code of a .cs file. This is achieved by using the code line #nullable enable above any line where the action is required or #nullable disable above any line where the action is not required. This can be seen in this code snippet where the nullable reference type has been disabled for the policyId field because it was enabled at the project level:
#nullable disable
string policyId = null;
The Microsoft site also says
The nullable annotation context and nullable warning context can be set for a project using the<Nullable> elementin your .csproj file. This element configures how the compiler interprets the nullability of types and what warnings are emitted.
Note:
Deference warnings occur when the application deferences (tries to obtain the address of something held in a location from a pointer) and expects it to be valid but is returned a null.
An assignment warning
is issued when we try to assign incorrectly, for example, in an if construct, we might try to do if(amount = amountLimit) but we would get an assignment warning as it should be if(amount == amountLimit).
The Microsoft site
also gives us details about the contexts we can use:
If we use disable, then
All reference types are nullable.
We do not need to use the ? suffix.
Deference warnings are disabled.
Assignment warnings are disabled.
If we use enable, then
All reference types are non-nullable unless declared using the ?.
We can use the ? suffix.
Deference warnings are enabled.
Assignment warnings are enabled.
If we use warnings, then
All reference types are nullable but members
are considered not null at the opening brace of methods.
We can use the ? suffix but it produces a warning.
Deference warnings are enabled.
Assignment warnings are not applicable.
If we use annotations, then
All reference types
are non-nullable unless declared using the ?.
We can use the ? suffix and it declares the nullable type.
Deference warnings are disabled.
Assignment warnings are disabled.
90.
Amend the PolicyReadOnlyMembers struct so that the policyType is assigned a null value within the constructor, as in Listing 19-19.
public PolicyReadOnlyMembers(int policy_number,
string policyType, double monthlyPremium,
string policyEndDate)
{
this.policy_number = policy_number;
//this.policyType = policyType;
this.policyType = null;
this.monthlyPremium = monthlyPremium;
this.policyEndDate = policyEndDate;
} // End of user constructor
Listing 19-19
Assign a null value to the policyType field
Hovering over the word null will show a pop-up window with a message, and reading this message tells us that we cannot have a nullable – the string field policyType
cannot be assigned a null value.
91.
Hover over the word null, as in Figure 19-13, and read the warning message.
We will now amend the declaration of the policyType within the struct, so that the type string is marked with the ?:
private string?policyType;
This will enable the policyType to accept a null value.
Notice the warning under the null
in the code has disappeared.
93.
Click the File menu.
94.
Choose Save All.
95.
Click the Debug menu.
96.
Choose Start Without Debugging.
The console window will appear displaying details of the policy, as shown in Figure 19-14. We can see that the nullable value has been accepted, and this is where the second line is displaying as “empty.”
Chapter Summary
So, finishing this chapter on structs, we can see the similarity with classes
and objects. A struct is a value type, which must have a default parameterless constructor, but they can also have custom constructors. The fields can be set to private and we can use properties, getters and setters, to access them. The whole struct can be set to readonly and this means all fields must also be readonly, but from C# 8 we can have readonly fields without the struct being readonly. Finally, we looked at nullable references and the use of the ? to permit null values.
Wow, what an achievement. This is not basic coding. We are doing some wonderful things with our C# code. We should be immensely proud of the learning to date. In finishing this chapter, we have increased our knowledge further and we are advancing to our target.