Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
High-Performance Programming in C# and .NET

You're reading from   High-Performance Programming in C# and .NET Understand the nuts and bolts of developing robust, faster, and resilient applications in C# 10.0 and .NET 6

Arrow left icon
Product type Paperback
Published in Jul 2022
Publisher Packt
ISBN-13 9781800564718
Length 660 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Jason Alls Jason Alls
Author Profile Icon Jason Alls
Jason Alls
Arrow right icon
View More author details
Toc

Table of Contents (22) Chapters Close

Preface 1. Part 1: High-Performance Code Foundation
2. Chapter 1: Introducing C# 10.0 and .NET 6 FREE CHAPTER 3. Chapter 2: Implementing C# Interoperability 4. Chapter 3: Predefined Data Types and Memory Allocations 5. Chapter 4: Memory Management 6. Chapter 5: Application Profiling and Tracing 7. Part 2: Writing High-Performance Code
8. Chapter 6: The .NET Collections 9. Chapter 7: LINQ Performance 10. Chapter 8: File and Stream I/O 11. Chapter 9: Enhancing the Performance of Networked Applications 12. Chapter 10: Setting Up Our Database Project 13. Chapter 11: Benchmarking Relational Data Access Frameworks 14. Chapter 12: Responsive User Interfaces 15. Chapter 13: Distributed Systems 16. Part 3: Threading and Concurrency
17. Chapter 14: Multi-Threaded Programming 18. Chapter 15: Parallel Programming 19. Chapter 16: Asynchronous Programming 20. Assessments 21. Other Books You May Enjoy

Overview of C# 10.0

You can find the features that will become part of C# 10.0 on the Roslyn GitHub page at https://github.com/dotnet/roslyn/blob/master/docs/Language%20Feature%20Status.md.

Not all these features are available at the time of writing. However, we will look at some of the available features. With that, let's start with top-level programs.

Writing top-level programs

Before C# 9.0, the Hello, World! console application was always the starting point for learning C#. The file that students would update was called Program.cs. In this file, you would have something akin to the following:

using System;
namespace HelloWorld
{
class Program
{
            static void Main(string[] args)
            {
                  Console.WriteLine("Hello, World!");
            }
}
}

As you can see, first, we import our System library. Then, we have a namespace definition followed by our class definition. Then, in the class definition, we have our Main method, in which we output the phrase "Hello, World!" to the console window.

In version 10.0 of the C# programming language, this can be simplified down to a single line:

System.Console.WriteLine("Hello, World");

Here, we have eradicated 10 lines of code. Running the program will output the following:

Figure 1.2 – The console window showing the output "Hello World!"

Figure 1.2 – The console window showing the output "Hello World!"

If we open the generated DLL in IL DASM, we will see the following:

Figure 1.3 – ILDASM showing the internals of the hello world program

Figure 1.3 – ILDASM showing the internals of the hello world program

You will see from the decompilation that the compiler adds the Main method at compile time. The next addition to C# 10.0 that we will look at is init-only properties.

Using init-only properties

Init-only properties allow you to use object initializers with immutable fields. For our little demonstration, we will use a Book class that holds the name of a book and its author:

namespace CH01_Books
{
    internal class Book
    {
        public string Title { get; init; }
        public string Author { get; init; }
    }
}

The properties can be initialized when the book is created. But once created, they can only be read, not updated, making the Book type immutable. Now, let's look at init-only properties. In the Program class, replace its contents with the following:

using System;
using CH01_Books;
var bookName = new Book { Title = "Made up book name", 
    Author = "Made Up Author" };
Console.WriteLine($"{bookName.Title} is written by 
    {bookName.Author}. Well worth reading!");

Here, we imported the System and CH01_Books namespaces. Then, we declared a new immutable variable of the Book type. After that, we output the contents of that Book type using an interpolated string. Run the program; you should see the following output:

Figure 1.4 – The output of our init-only properties example

Figure 1.4 – The output of our init-only properties example

Now that we have been introduced to init-only properties, let's look at records.

Using records

When updating data, you do not want that data to be changed by another thread. So, in multi-threaded applications, you will want to use thread-safe objects when making updates. Records allow complete objects to be immutable and behave as values. The advantage of using records over structs is that they require less memory to be allocated to them. This reduction in memory allocation is accomplished by compiling records to reference types. They are then accessed via references and not as copies. Due to this, other than the original record allocation, no further memory allocation is required.

Let's learn how to use records. Start a new console application.

To demonstrate the use of records, we will use the following Book example:

internal record Book
{
public string Title { get; init; }
     public string Author { get; init; }
}

The only change to the Book class is that class has been replaced with record. Everything else remains the same. Now, let's put the record to work:

  1. Replace the contents of the Program class with the following code:
    using System;
    using CH01_Records;
    var bookOne = new Book { 
        Title = "Made Up Book", 
        Author = "Made Up Author
    };
    var bookTwo = bookOne with { 
        Title = "And Another Made Up Book"
    };
    var bookThree = bookTwo with { 
        Title = "Yet Another Made Up Book"
    };
    var bookFour = bookThree with { 
        Title = "And Yet Another Made Up Book: Part 1",
    };
    var bookFive = bookFour with { 
        Title = "And Yet Another Made Up Book: Part 2"
    };
    var bookSix = bookFive with { 
        Title = "And Yet Another Made Up Book: Part 3"
    };
    Console.WriteLine($"Some of {bookThree.Author}'s 
        books include:\n");
    Console.WriteLine($"- {bookOne.Title}");
    Console.WriteLine($"- {bookTwo.Title}");
    Console.WriteLine($"- {bookThree.Title}");
    Console.WriteLine($"- {bookFour.Title}");
    Console.WriteLine($"- {bookFive.Title}");
    Console.WriteLine($"- {bookSix.Title}");
    Console.WriteLine($"\nMy favourite book by {bookOne.
        Author} is {bookOne.Title}.");
  2. As you can see, we are creating immutable record types. We can create new immutable types from them and change any fields we like using the with expression. The original record is not mutated in any way. Run the code; you will see the following output:

Figure 1.5 – Init-only properties showing their immutability

Figure 1.5 – Init-only properties showing their immutability

Despite changing the title during the assignment, the original record has not been mutated at all.

  1. Records can also use inheritance. Let's add a new record that contains the publisher's name:
        internal record Publisher
        {
            public string PublisherName { get; init; }
        }
  2. Now, let's have our Book inherit this Publisher record:
        internal record Book : Publisher
        {
            public string Title { get; init; }
            public string Author { get; init; }
        }
  3. Book will now include PublisherName. When we initialize a new book, we can now set its PublisherName:
    var bookOne = new Book { 
        Title = "Made Up Book", 
        Author = "Made Up Author",
        PublisherName = "Made Up Publisher Ltd."
    };
  4. Here, we have created a new Book that contains Publisher.PublisherName. Let's print the publisher's name. Add the following line to the end of the Program class:
    Console.WriteLine($"These books were originally published 
        by {bookSix.PublisherName}.");
  5. Run the code; you should see the following output:

Figure 1.6 – Init-only properties using inheritance

Figure 1.6 – Init-only properties using inheritance

  1. As you can see, we never set the publisher's name for bookTwo to bookSix. However, the inheritance has followed through from when we set it for bookOne.
  2. Now, let's perform object equality checking. Add the following code to the end of the Program class:
    var book = bookThree with { Title = "Made Up Book" };
    var booksEqual = Object.Equals(book, bookOne) ? 
        "Yes" : "No";
    Console.WriteLine($"Are {book.Title} and 
        {bookOne.Title} equal? {booksEqual}"); 
  3. Here, we created a new Book from bookThree and set the title to Made Up Book. Then, we performed an equality check and output the result to the console window. Run the code; you will see the following output:

Figure 1.7 – Init-only properties showing the result of an equality check

Figure 1.7 – Init-only properties showing the result of an equality check

It is clear to see that the equality check works with both book instances being equal.

  1. Our final look at records considers positional records. Positional records set data via the constructor and extract data via the deconstructor. The best way to understand this is with code. Add a class called Product and replace the class with the following:
        public record Product
        {
            readonly string Name;
            readonly string Description;
            public Product(string name, string 
                description) 
                => (Name, Description) = (name, 
                description);
            public void Deconstruct(out string name, out 
                string description) 
                => (name, description) = (Name, 
                    Description);
        }
  2. Here, we have an immutable record. The record has two private and readonly fields. They are set in the constructor. The Deconstruct method is used to return the data. Add the following code to the Program class:
    var ide = new Product("Awesome-X", "Advanced Multi-
        Language IDE");
    var (product, description) = ide;
    Console.WriteLine($"The product called {product} is an 
        {description}.");

In this code, we created a new product with parameters for the name and description. Then, we declared two fields called product and description. The fields are set by assigning the product. Then, we output the product and description to the console window, as shown here:

Figure 1.8 – Init-only positional records

Figure 1.8 – Init-only positional records

Now that we have finished looking at records, let's look at the improved pattern matching capabilities of C# 10.0.

Using the new pattern matching features

Now, let's look at what's new for pattern matching in C# 10.0, starting with simple patterns. With simple pattern matching, you no longer need the discard (_) operator to just declare the type. In our example, we will apply discounts to orders:

  1. Add a new record called Product to a new file called Product.cs in a new console application and add the following code:
        internal record Product
        {
            public string Name { get; init; }
            public string Description { get; init; }
            public decimal UnitPrice { get; init; }
        }
  2. Our Product record has three init-only properties for Name, Description, and UnitPrice. Now, add the OrderItem record that inherits from Product:
        internal record OrderItem : Product
        {
            public int QuantityOrdered { get; init; }
        }
  3. Our OrderItem record inherits the Product record and adds the QuantityOrdered init-only property. In the Program class, we will add three variables of the OrderItem type and initialize them. Here is the first OrderItem:
    var orderOne = new OrderItem {
            Name = "50-80mm Scottish Cobbles", 
            Description = "These rounded stones are 
              frequently used for edging paths and to add 
                interest to gardens", 
            QuantityOrdered = 4, 
            UnitPrice = 199 
    };

As you can see, the quantity that's being ordered is 4.

  1. Add orderTwo with the same values but with an OrderQuantity of 7.
  2. Then, add orderThree with the same values, but with an OrderQuantity of 31. We will demonstrate simple pattern matching in the GetDiscount method:
    static int GetDiscount(object order) =>
        order switch
        {
            OrderItem o when o.QuantityOrdered == 0 => 
                throw 
              new ArgumentException("Quantity must be 
                  greater than zero."),
            OrderItem o when o.QuantityOrdered > 20 => 30,
            OrderItem o when o.QuantityOrdered < 5 => 10,
            OrderItem => 20,
            _ => throw new ArgumentException("Not a known 
                OrderItem!", nameof(order))
        };
  3. Our GetDiscount method receives an order. QuantityOrdered is then evaluated. Argument exceptions are thrown if the order quantity is 0 and if the object type that's been passed in is not of the OrderItem type. Otherwise, a discount of the int type is returned for the quantity ordered. Notice that we use the type without using the discard operator on the line for the 20% discount.
  4. Finally, we must add the following lines to the end of the Program class:
    Console.WriteLine($"The discount for Order One is 
        {GetDiscount(orderOne)}%.");
    Console.WriteLine($"The discount for Order Two is 
        {GetDiscount(orderTwo)}%.");
    Console.WriteLine($"The discount for Order Three is 
        {GetDiscount(orderThree)}%.");
  5. These lines print the discount received for each of the orders to the console window. Now, let's modify our code so that it uses relational pattern matching. Add the following method to the Program class:
    static int GetDiscountRelational(OrderItem orderItem) 
        => orderItem.QuantityOrdered switch
        {
            < 1 => throw new ArgumentException("Quantity 
                must be greater than zero."),
            > 20 => 30,
            < 5 => 10,
            _ => 20
        };
  6. Using relational pattern matching, we have received the same outcome as with simple pattern matching, but with less code. It is also very readable, which makes it easy to maintain. Add the following three lines of code to the end of the Program class:
    Console.WriteLine($"The discount for Order One is 
        {GetDiscountRelational(orderOne)}%.");
    Console.WriteLine($"The discount for Order Two is 
        {GetDiscountRelational(orderTwo)}%.");
    Console.WriteLine($"The discount for Order Three is 
        {GetDiscountRelational(orderThree)}%.");
  7. In these three lines, we simply output the discount for each order to the console window. Run the program; you will see the following output:
Figure 1.9 – Simple and relational pattern matching output showing the same results

Figure 1.9 – Simple and relational pattern matching output showing the same results

From the preceding screenshot, you can see that the same outcome has been received for both discount methods.

  1. The logical AND, OR, and NOT methods can be used in logical pattern matching. Let's add the following method:
    static int GetDiscountLogical(OrderItem orderItem) =>
        orderItem.QuantityOrdered switch
        {
            < 1 => throw new ArgumentException("Quantity 
                must be greater than zero."),
            > 0 and < 5 => 10,
            > 4 and < 21 => 20,
            > 20 => 30
        };
  2. In the GetDiscountLogical method, we employ the logical AND operator to check whether a value falls in that range. Add the following three lines to the end of the Program class:
    Console.WriteLine($"The discount for Order One is 
        {GetDiscountLogical(orderOne)}%.");
    Console.WriteLine($"The discount for Order Two is 
        {GetDiscountLogical(orderTwo)}%.");
    Console.WriteLine($"The discount for Order Three is 
        {GetDiscountLogical(orderThree)}%.");
  3. In those three lines of code, we output the discount value for the order to the console window. Run the code; you will see the following output:
Figure 1.10 – Simple, relational, and logical pattern matching showing the same results

Figure 1.10 – Simple, relational, and logical pattern matching showing the same results

The output for the logical pattern matching is the same as for simple and relational pattern matching. Now, let's learn how to use new expressions with targeted types.

Using new expressions with targeted types

You can omit the type of object being instantiated. But to do so, the declared type must be explicit and not use the var keyword. If you attempt to do this with the ternary operator, you will be greeted with an exception:

  1. Create a new console application and add the Student record:
        public record Student
        {
            private readonly string _firstName;
            private readonly string _lastName;
            public Student(string firstName, string 
                lastName)
            {
                _firstName = firstName;
                _lastName = lastName;
            }
            public void Deconstruct(out string firstName, 
                out string lastName)
                => (firstName, lastName) = (_firstName, 
                    _lastName);
        }
  2. Our Student record stores the first and last name values, which have been set via the constructor. These values are obtained via the out parameters of the Deconstruct method. Add the following code to the Program class:
    Student jenniferAlbright = new ("Jennifer", 
        "Albright");
    var studentList = new List<Student>
    {
        new ("Jennifer", "Albright"),
        new ("Kelly", "Charmichael"),
        new ("Lydia", "Braithwait")
    };
    var (firstName, lastName) = jenniferAlbright;
    Console.WriteLine($"Student: {lastName}, {firstName}");
    (firstName, lastName) = studentList.Last();
    Console.WriteLine($"Student: {lastName}, {firstName}");
  3. First, we instantiate a new Student without declaring the type in the new statement. Then, we instantiate a new List and add new students to the list while omitting the Student type. The fields are then defined for firstName and lastName and assigned their values through the assignment of the named student. The student's name is then printed out on the console window. Next, we take those fields and reassign them with the name of the last student on the list. Then, we output the student's name to the console window. Run the program; you will see the following output:
Figure 1.11 – Using targeted types with new expressions

Figure 1.11 – Using targeted types with new expressions

From the preceding screenshot, you can see that we have the correct student names printed. Now, let's look at covariant returns.

Using covariant returns

With covariant returns, base class methods with less specific return types can be overridden with methods that return more specific types. Have a look at the following array declaration:

object[] covariantArray = new string[] { "alpha", "beta", 
    "gamma", "delta" };

Here, we declared an object array. Then, we assigned a string array to it. This is an example of covariance. The object array is the least specific array type, while the string array is the more specific array type.

In this example, we will instantiate covariant types and pass them into a method that accepts less and more specific types. Add the following class and interface declarations to the Program class:

public interface ICovariant<out T> { }
public class Covariant<T> : ICovariant<T> { }
public class Person { }
public class Teacher : Person { }
public class Student : Person { }

Here, we have a covariant class that implements a covariant interface. We declared a general type of Person that is inherited by the specific Teacher and Student types. Add CovarianceClass, as shown here:

public class CovarianceExample
{
public void CovariantMethod(ICovariant<Person> person)
{ 
      Console.WriteLine($"The type of person passed in is 
          of type {person.GetType()}.");
}
}

In the CovarianceExample class, we have a CovariantMethod with a parameter that can accept objects of the ICovariant<Person> type. Now, let's put covariance to work by adding the CovarianceAtWork method to the CovarianceExample class:

public void CovarianceAtWork()
{
ICovariant<Person> person = new Covariant<Person>();
ICovariant<Teacher> teacher = new Covariant<Teacher>();
ICovariant<Student> student = new Covariant<Student>();
CovariantMethod(person);
CovariantMethod(teacher);
CovariantMethod(student);
}

In this method, we have the general Person type and the more specific Teacher and Student types. We must pass each into CovariantMethod. This method can take the less specific Person type and the more specific Teacher and Student types.

To run the CovarianceAtWork method, place the following code after the using statement and before the covariantArray example:

CovarianceExample.CovarianceAtWork();

Now, let's look at native compilation.

You have been reading a chapter from
High-Performance Programming in C# and .NET
Published in: Jul 2022
Publisher: Packt
ISBN-13: 9781800564718
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime