Pages

C# Features Through the History - Version 8.0

 




Version 8.0 of the C# Language came along with the .NET Core 3.1, in the year 2019.


How to use the examples 

The examples are available here. You can open the solution and play around. Just uncomment the code and have fun! 



Readonly Members

Now, it's possible to apply the readonly modifier to any member of a struct. It's more granular than applying it just in the declaration of the struct because the member doesn't modify state:
    public struct Item
    {
        private int Id { get; set; }

        public readonly string Name;

        public override string ToString()
        {
            Id += 1; //ok
            return Id.ToString();
        }
    }
			
    public struct Item
    {
        private int Id { get; set; }

        public readonly override string ToString()
        {
            Id += 1; //Error: cannot assign to Id because it's readonly
            return Id.ToString();
        }
    }
			  
Default Interface Methods

This is a feature used in Java, where you can add members to interfaces and provide an implementation for those members. The idea is to ensure backward compatibility with code written in old versions of this interface:

Imagine that you have your interface Account and the class that implements this interface, AccountImplementation:
    public interface Account
    {
        void Deposit(decimal amount);
    }

    public class AccountImplementation : Account
    {
        public void Deposit(decimal amount)
        {
            throw new NotImplementedException();
        }
    }
Everything is good but, if for some reason this interface is modified to receive a new method, you should go to all the concrete classes, in order to implement this new method right? This new feature allows you to use a different approach:
    public interface Account
    {
        void Deposit(decimal amount);
        void SpecificDeposit(decimal amount)
        {
            //default routine here
        }
    }
                
Pattern Matching Enhancements

With pattern matching, you can deconstruct objects, with the possibility of access parts of these structures. 

With switch matching, we can construct the code in a more concise way:
        public enum Color
        {
            Red,
            Green,
            Blue
        }

        public static string getColor(Color color) =>
            color switch
            {
                Color.Red => "Red",
                Color.Green => "Green",
                Color.Blue => "Blue",
                _ => throw new NotImplementedException()
            };  
                              
With property pattern, the idea is to match the properties of the object: 
        internal class User
        {
            public int Age { get; set; }
            public string Name { get; set; }
        }

        internal static decimal ComputeDiscount(decimal price, User user) =>
            user switch
            {
                { Age: 10 } => price * 0.6M,
                { Age: 18 } => price * 0.5M,
                { Age: 80 } => price * 0.7M,
                _ => price
            };

Using tuple patterns, you can switch based on multiple values of a tuple:
         public static string getCongratsPhrase(string firstName, string surName)
            => (firstName, surName) switch
            {
                ("Jane", "Austen") => "Jane Austen is awesome",
                ("Clarice", "Linspector") => "Jane Austen is awesome",
                _ => "I don't know who are you, but welcome."
            };

Nullable Reference Type

Now, the compiler uses flow analysis to identify that any variable of nullable reference type is checked against null before it's accessed or assigned to a non-nullable reference type:
            string? name = null;  // compile will show a warning here
            Console.WriteLine(name.Length);

            string surName = null; // No warning
            Console.WriteLine(surName);

            string? nickName = null; // warning again
            Console.WriteLine(nickName);

            #nullable enable    
            string? address = null; // no warning
            Console.Write(address);

Using Declarations

The using declarations will allow you to declare and use disposable objects in an easier way, without the necessity of using curly brackets. Based on the context the code is inserted, it will automatically disposed, in the reverse order (stream2 first, in this following example):
            using var stream1 = new StreamReader(Path.GetTempFileName());
            using var stream2 = new StreamReader(Path.GetTempFileName());		  

This using statement is useful for disposable types that implements the IAsyncDisposable interface:
    public static class AsynchronousDisposable
    {
        public static async void ExecuteExample()
        {
            var userAsyncDisposable = new UserAsyncDisposable();
            await using (userAsyncDisposable.ConfigureAwait(false)) // old way
            {
            }

            await using var userAsyncDisposable2 = new UserAsyncDisposable();

        }
    }

    internal class UserAsyncDisposable : IAsyncDisposable
    {
        public ValueTask DisposeAsync()
        {
            throw new NotImplementedException();
        }
    }

Asynchronous Streams

It's possible to create and consume streams in an asynchronous way. It allows working in an easily way with IoT or cloud solutions, with a better suppor of the language to process the information necessary. In this next example you can see that we make use of the interface IAsyncEnumerable<T>, instead of using Task<IEnumerable<T>>. With this interface, it's possible to make asynchronous calls in between yielding results: 

public static async Task ExecuteExample()
{
   await foreach (var item in GenerateOrder())
      Console.WriteLine(item);
}

internal static async IAsyncEnumerable<int> GenerateOrder()
{
   for(int i=0; i<10; i++) 
   { 
      await Task.Delay(100);
      yield return i;
   }
}

Indices and Ranges
Now the C# language provide some interesting ways for accessing elements or ranges from an array, similar of what we have in Python:
            var numbers = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

            Console.WriteLine(numbers[^1]); // latest item 9

            var rangeBetween2And5 = numbers[1..6]; // returns 1,2,3,4,5

            Range rangeBetween2And5New = 1..6;
            var result = numbers[rangeBetween2And5New]; // returns 1,2,3,4,5 using Range struct

            var numbersFrom5to7 = numbers[^5..^2]; // returns numbers 5, 6, 7
           	  
Null-Coalescing Assignment

With the operator ??=, you can assign te value of its right-hand operator to its left-hand operand if the left-hand operand evaluates to null:
List<int> numbers = null;
int? i = null;

numbers ??= new List<int>();
numbers.Add(i ??= 17);
numbers.Add(i ??= 20);

Console.WriteLine(string.Join(" ", numbers)); //output: 17 17
Console.WriteLine(i); // output: 17
---------------------------------------

I hope you have enjoyed this journey now. Have a nice week!

Here is the entire list of this series:
---------------------------------------------------------------------------------------------------------------------

References:

I'd like to leave with you some references that can be helpful for your development as a software engineer, part of what we talked about above:


Links for books to buy in the USA: 

                 



Links for books to buy in Europe:

                 


Help us to maintain the MyLifeInDev running:


Fabio Ono

No comments:

Post a Comment