Click here to Skip to main content
15,867,686 members
Articles / DevOps

Experimenting with C# 7’s New Features

Rate me:
Please Sign up or sign in to vote.
4.00/5 (2 votes)
21 May 2017CPOL16 min read 9.6K   5   2
This post talks about the most important improvements to the C# language in its 7th version, provides some IL overview, as well as my own personal opinions on the features and improvements.

Last year, I worked out and actually wrote about C# 6’s features, in a way that it would be easy to understand how they were actually created so that we can easily get a good understanding of the C# language, as it is evolving. Since quite a while, what happens in the language is that, where everyone else is trying to optimize the background code, the code that gets generated, what happens in C# context is that the language gets optimized.

  1. I was studying about async/await these days, what I figured out was the feature itself was a syntactical sugar around the older ways of asynchronous programming. Only thing improved was that you were never allowed to play around in the sand of memory.
  2. Last year, when I was writing that post for C# 6, I figured that most of the features were also improvements over syntax, to ensure language evolution, yet keeping things on ground — string interpolation for example.
  3. While these changes can improve the way programs are executed, it still does not prevent any idiot from writing bad programs, which I am going to do in this post. 🙂

So, to keep things short: In this post, I will be talking about the new improvements in C# 7, and what they are, how they get implemented and what you should understand before you actually start using them in your programs. I will also sum up the post by including my suggestions, and thoughts as to which of these features are helpful and which of them are not.

I will be going through a bit of IL code, and LINQPad will be used for this, so if you have an understanding of what the .NET IL code is, it would be a good plus point for you, if not, then it doesn’t matter as I will explain the concept myself.

Important Features to Cover

C# 7 introduces several improvements to the language, and ignoring the .NET 4,7, there is still a lot of stuff to cover, but I will not be covering any of that stuff. I want to cover only a few of the topics because the stuff is already way too much for one post. Anyways, the following list shows the important features of C# 7 that I want to experiment with:

  1. Literals
  2. Local functions
  3. Tuples improvements in C# 7
  4. Pattern matching cases
  5. Async improvement — ValueType Task<T> object
  6. Deconstruction

There are other some basic differences there as well, but most of them are improvement or additions to the stuff we already looked at in C# 6, such as the expression bodied methods and members, so I will not cover them here at all. However, I might be covering the entire C# 7 improvement course in a separate ebook for developers, keep a watch out for it. 🙂

How to Use C# 7?

Some might say, just download Visual Studio 2017 Community edition and you will get everything of C# 7 by default. Fine, but there are some who cannot access these tools, updated IDEs, SDKs because of several reasons. Thus to sum up the post for them, I will give an overview of the ways in which you can actually use C# 7 in older versions of .NET Framework, with older versions of Visual Studio.

  1. Most of the features are packages; Tuples feature is an extension package from NuGet, that you can install.
  2. Most of the features are syntactic sugars — just like the string interpolation — so you will still be able to use most of the features of C# 7 in older .NET Framework.

But, Visual Studio 2017 Community edition doesn’t charge anything, and has some benefits packed; so do consider it. 🙂

Understanding the Improvements

Ok, so now let us consider understanding the improvements one by one and see how Microsoft (or the community) actually implemented them in the language. Review the list above, I will be covering only the topics provided there, so you know which part contains which of the sections here, and will be providing some more tips, how-to and why-to in the sections themselves.

Literals

The first and foremost thing in C# 7, that most of the people are still trying out are the literals. Fair enough, most of the times, the improvement this has, has nothing to do with more readability. I would not talk much more on this topic, instead would only provide my suggestions on the topic and then close it.

C#
// At the moment C# supports
var dead = 0xDEAD;
var beef = 0xBEEF;

Now, the current literals, support the “_” character to separate the value itself, but of what use would the following be?

C#
var dead = 0xD_EAD; // ? Did I mean to write DREAD? Nope.
// So, not much useful for me.

Secondly, let’s try it out on a numeric value:

C#
var kms = 10_000;

The way I see these values, what I feel about them is, they are like a blank field, or a template. Instead of this, what I would have personally loved to see in C# would have been the “locale” based separators for the digits.

C#
var kms = 10,000; // Looks more natural.

Look, the way C# is providing this feature is that this is just syntactic sugar. So, since this is provided by the IDE, why not use the current locale, and map the digits from there to the native types.

  1. The “comma” as a separator is more natural in many ways. Even 0xAD,BC, would be more readable than 0xAD_BC. That seems as if I actually skipped a value there.
    • I agree that to some 0xAD,BC, might not be as much visible; but given that the “comma” is used as a separator in most contexts, it can be used as a digit separator as well — provided the context of its usage.
  2. Visual Studio should be intelligent enough to actually integrate this feature with the current locale settings of user.

However, the problem with my idea is that it is based on the locale of the user, and not the standard, so in the cases where there might be teams working on the same project, there might be a mismatch breaking the entire build just because of a literal.

Local Functions

The thing about local functions is, somewhat interesting to me. They are just the way any other function is defined, they generate the same IL code and take the same amount of time, the only thing is that they belong to the function itself, unlike an object instance or a class type (as in static functions). So, let’s have a look at what local functions are in real.

Let us assume that I have a function that processes the list of integers, and then returns something,

C#
int Process() {
    var list = new List<int>();
    var sum = Sum(list);
 
     int Sum(List<int> items) {
         return items.Sum();
     }
     return sum;
}

At first glance, this looks just like any other ordinary C# program, you provide the variables, set some functions and then return it; print it. However, this is just a sample function, which has state, a function and something to return to the caller. The function, in no way is different than the following code:

C#
int Process() {
     var list = new List<int>();
     var sum = Sum(list);
 
     return sum;
}

// Notice, that both are instance functions.
int Sum(List<int> items) {
     return items.Sum();
}

Thus, what happens at the background is also similar, the IL generated by these methods is the same.

MSIL
IL_0000:  nop         
IL_0001:  ret         

Process:
IL_0000:  nop         
IL_0001:  newobj      System.Collections.Generic.List<System.Int32>..ctor
IL_0006:  stloc.0     // list
IL_0007:  ldloc.0     // list
IL_0008:  call        UserQuery.<Process>g__Sum1_0
IL_000D:  stloc.1     // sum
IL_000E:  nop         
IL_000F:  ldloc.1     // sum
IL_0010:  stloc.2     
IL_0011:  br.s        IL_0013
IL_0013:  ldloc.2     
IL_0014:  ret         

<Process>g__Sum1_0:
IL_0000:  nop         
IL_0001:  ldarg.0     
IL_0002:  call        System.Linq.Enumerable.Sum
IL_0007:  stloc.0     
IL_0008:  br.s        IL_000A
IL_000A:  ldloc.0     
IL_000B:  ret

The only difference in the second case is the label used for the function block, and the call operation has the function name, instead of the g__Sum1_0. So, the difference in only the naming of these functions, otherwise they have common things:

  1. They are instance functions
  2. They perform same operations, generate the same IL code as well
  3. They can be used to wrap any task that requires to be executed as needed

While this is shown, there are a few differences to note here as well:

  1. The local functions have access to the function local variables as well
  2. For external functions, you have to manually pass the parameters; or at least use ref, out parameters
  3. Local functions have direct connection to the variables of a function.

That said, there is another major point to talk about; the location of the function inside the function doesn’t matter. It is only the matter of taste, and C# would then generate the IL for that, so that the compiler knows where that function is. What I mean to say here is, you can actually return a value before even writing the local function, and C# would compile the code and make it work properly as well.

C#
int Process() {
    var list = new List<int>();
    var sum = Sum(list);
 
    return sum;
   
    // Function here.
    int Sum(List<int> items) {
        return items.Sum();
    }
}

The IL code for this is:

MSIL
Process:
IL_0000:  nop         
IL_0001:  newobj      System.Collections.Generic.List<System.Int32>..ctor
IL_0006:  stloc.0     // list
IL_0007:  ldloc.0     // list
IL_0008:  call        UserQuery.<Process>g__Sum1_0
IL_000D:  stloc.1     // sum
IL_000E:  ldloc.1     // sum
IL_000F:  stloc.2     
IL_0010:  br.s        IL_0012
IL_0012:  ldloc.2     
IL_0013:  ret         

<Process>g__Sum1_0:
IL_0000:  nop         
IL_0001:  ldarg.0     
IL_0002:  call        System.Linq.Enumerable.Sum
IL_0007:  stloc.0     
IL_0008:  br.s        IL_000A
IL_000A:  ldloc.0     
IL_000B:  ret

Typically, the only difference is, if you write the “return sum;” at the end, there will be a nop bytecode operation added to the IL, which as you may know is a no operation command, but might consume a cycle and so.

Local Functions are Instance Functions

One other difference is that you can easily create static functions, but however, the local functions cannot be static functions. Even if that parent function is a static function, the local function cannot be a static one. I don’t actually understand this one thing, why… But perhaps, the context itself is static, and the function would ultimately end up being static… Or what, but, let’s just say it works. 🙂

C#
static int Process() {
    var list = new List<int>();
    var sum = Sum(list);
 
    return sum;

    // Function here.
    static int Sum(List<int> items) { // CS0106: The modifier "static" is not valid.
        return items.Sum();
    }
}

In most cases, the local functions seem to be helpful, but in major cases, this might be an extra region to cover.

Local Functions as Lambdas

One final thing about local functions, is that they can be easily created as lambdas. So the code that we have:

C#
int Process() {
    var list = new List<int>();
    var sum = Sum(list);

    return sum;

    // Function here.
    int Sum(List<int> items) {
        return items.Sum();
    }
}

Can be easily rewritten to the following one:

C#
int Process() {
    var list = new List<int>();
    var sum = Sum(list);
 
    return sum;

    // Function here.
    int Sum(List<int> items) => items.Sum();
}

Believe it or not, the change has a huge performance improvement over the older (function) way. The lambdas are definitely stronger. As we move onwards in the article, you will see how much of the concepts are being taken from the function programming… Tuples is one of such.

Tuple Improvements in C# 7

As you may know, tuple is a concept of functional programming, and are really very powerful and useful types; C# guys don’t know much of it, but function world such as Haskell have been using them for a while. Previously, System.Tuple type was used to create the tuples. Before I actually go down, let me tell you that a tuple doesn’t only mean that you can “return more than 1 value from a function”. That is just one of the uses where a tuple proves to be useful, not all of it. Remember that I said it comes from the functional world, in that world, there are no objects and thus no classes and no instances. Thus, when you have to store something, you would typically be using a tuple that would show a record, or an entity — not an object.

Tuples have their own benefits, and objects have their own. In the older versions, they were created as a type of tuple, a structure. However, improvements have come and they are not a part of the C# language syntax, so you do not have to write anything extra, and you still get the benefit of that. Also, the tuple is now a System.ValueTuple instead of the System.Tuple.

So, now let us see what they are and how they are useful, or not… 🙂

C#
void Main()
{
    var person = GetPerson();
 
    Console.WriteLine($"{person.Item1} is {person.Item2} years old.");
}

(string, int) GetPerson() {
    return ("Afzaal Ahmad Zeeshan", 21);
}

// Output
// Afzaal Ahmad Zeeshan is 21 years old.

In the code above, what I am doing is, I am creating a separate function. A function that returns a tuple type; a tuple is wrapped inside the parenthesis, and has a type for each element, the name of the elements is conditional, which defaults to ItemN.

Similarly, what the code does is that it gets the value from the function and prints it on the console. Studying the IL code for this would give a more in-depth overview of the tuple type:

MSIL
IL_0000:  nop         
IL_0001:  ldarg.0     
IL_0002:  call        UserQuery.GetPerson
IL_0007:  stloc.0     // person
IL_0008:  ldstr       "{0} is {1} years old."
IL_000D:  ldloc.0     // person
IL_000E:  ldfld       System.ValueTuple<System.String,System.Int32>.Item1
IL_0013:  ldloc.0     // person
IL_0014:  ldfld       System.ValueTuple<System.String,System.Int32>.Item2
IL_0019:  box         System.Int32
IL_001E:  call        System.String.Format
IL_0023:  call        System.Console.WriteLine
IL_0028:  nop         
IL_0029:  ret         

GetPerson:
IL_0000:  nop         
IL_0001:  ldstr       "Afzaal Ahmad Zeeshan"
IL_0006:  ldc.i4.s    15 
IL_0008:  newobj      System.ValueTuple<System.String,System.Int32>..ctor
IL_000D:  stloc.0     
IL_000E:  br.s        IL_0010
IL_0010:  ldloc.0     
IL_0011:  ret 

If you try to pay attention, you will see that all it does is, it gets the value and prints it as soon as the value is loaded onto the execution stack. However, I always thought perhaps, tuples were never needed and that the current types were enough to be worked around with, but if you try to unwrap the values and do something on them, then you will have a drastic change in the performance.

C#
var (name, age) = GetPerson();
 
Console.WriteLine($"{name} is {age} years old.");

This code is a bit more readable for the people, but has an extra overhead for the program because now the program also has to push the variables on the stack.

  1. It loads the tuple type from the function.
  2. Maps the types to the named variable.
    1. Notice that the name and age variables are of different type, but they are both sharing the var type.
  3. Then after this, it continues doing the same work that it was.

So, the thing is, the default tuple ItemN type does have a less readability but it gives you extra performance if you’re needing.

As for the IL, here is the IL for the code sample above:

MSIL
IL_0000:  nop         
IL_0001:  ldarg.0     
IL_0002:  call        UserQuery.GetPerson
IL_0007:  dup         
IL_0008:  ldfld       System.ValueTuple<System.String,System.Int32>.Item1
IL_000D:  stloc.2     
IL_000E:  ldfld       System.ValueTuple<System.String,System.Int32>.Item2
IL_0013:  stloc.3     
IL_0014:  ldloc.2     
IL_0015:  stloc.0     // name
IL_0016:  ldloc.3     
IL_0017:  stloc.1     // age
IL_0018:  ldstr       "{0} is {1} years old."
IL_001D:  ldloc.0     // name
IL_001E:  ldloc.1     // age
IL_001F:  box         System.Int32
IL_0024:  call        System.String.Format
IL_0029:  call        System.Console.WriteLine
IL_002E:  nop         
IL_002F:  ret

IL shows that there is no need for duplication of the data, no need for extra stack pushing, plain processing is going on down the road.

No Limit on the Length

One more thing to realize here is that the older types, have a limit on the types that you can use. Such as, a 7-tuple type is Tuple<T1, T2, T3, T4, T5, T6, T7>, but however there is no limit on the length of the tuple typing in this case, you can have any number of elements in the tuple. Interesting improvement! In the older ones, you would also be able to add a local tuple to access 8th to 14th tuple elements. However, they are not the cases here and you can directly access the element as container.Item8.

C#
(string, string, string, string, string, string, string, string, string, string, string) GetPerson() {
    return (null, null, null, null, null, null, null, null, null, null, null);
}

So, if your code requires a type that has like around 11 elements, you can use this without any problem at all.

Finally, there is no limit on the type of parameter to be used, you can use the local variables in the tuple. However, once again, the naming of the tuple elements is just the personal sort of taste that you can, or cannot prefer so I won’t talk about it any further.

Also, using a custom value typed structure also has some overheads, so if you want to create a separate object of that type, that is also not a good option. In most cases, I myself am going to enjoy using the tuple types in C#.

Pattern Matching Cases

Pattern matching is the use of current structures in C#, to initialize the variables, and check the conditions in one place. For example, have a look at the following code:

C#
void Main()
{
    var types = new object[] { "Afzaal", 21, 4.5d };
    Process(types);
}

void Process(object[] list) {
    foreach (var item in list) {
        if(item is String) {
            Console.WriteLine($"{(string) item} is String type.");
        } else if(item is int) {
            Console.WriteLine($"{(int) item} is int type.");
        } else if(item is double) {
            Console.WriteLine($"{(double) item} is double type.");
        }
    }
}

// Output
// Afzaal is String type.
// 21 is int type.
// 4.5 is double type.

This code has a condition that checks if the type is matching, and then after that, casts it to the proper type to show the results. Whereas, if we use the C# 7 way of doing this, we would get the following results:

C#
void Main()
{
    var types = new object[] { "Afzaal", 21, 4.5d };
    Process(types);
}

void Process(object[] list) {
    foreach (var item in list) {
        if(item is String str) {
            Console.WriteLine($"{str} is String type.");
        } else if(item is int i) {
            Console.WriteLine($"{i} is int type.");
        } else if(item is double d) {
           Console.WriteLine($"{d} is double type.");
        }
    }
}

This code has somewhat better readability, as compared to the old one, and also saves us from an extra cast, because that is taken care of by the IL in the background. Making it easier for us to write the code, the same is the case for the switch statements, as an example, let’s see how the integer check works in this case:

MSIL
IL_0036:  ldloc.2     // item
IL_0037:  isinst      System.Nullable<System.Int32>
IL_003C:  unbox.any   System.Nullable<System.Int32>
IL_0041:  stloc.s     07 
IL_0043:  ldloca.s    07 
IL_0045:  call        System.Nullable<System.Int32>.GetValueOrDefault
IL_004A:  stloc.s     05 // i
IL_004C:  ldloca.s    07 
IL_004E:  call        System.Nullable<System.Int32>.get_HasValue
IL_0053:  stloc.s     06 
IL_0055:  ldloc.s     06 
IL_0057:  brfalse.s   IL_0074
IL_0059:  nop         
IL_005A:  ldstr       "{0} is int type."
IL_005F:  ldloc.s     05 // i
IL_0061:  box         System.Int32
IL_0066:  call        System.String.Format
IL_006B:  call        System.Console.WriteLine
IL_0070:  nop 

Here, it captures the value from the list, checks if it is the instance of the “Nullable” type of the object, then gets the value by unboxing it and then rest of the stuff is the same process, or formatting the string and writing it. The thing is, we get saved in many ways, and the readability has improved.

As for the switch statements, the following is the syntax:

C#
switch (obj) {
    case Person p:
         break;
    case Shape s:
         break;
    default:
         break;
}

The improvement is, instead of having a primitive type here, you can now match the object type there — which is the same as I have already shown using the example of ifelse block, also you can use the when block to make sure that a case gets evaluated only if the condition is met, providing you with a good way of range based switch statement.

C#
void Process(object[] list) {
    foreach (var item in list) {
        switch(item) {
            case String str:
                Console.WriteLine($"{str} is of String type.");
                break;
            case int i:
                Console.WriteLine($"{i} is of int type.");
                break;
            case double d:
                Console.WriteLine($"{d} is of double type.");
                break;
        }
    }
}

This gives the same output and works like a charm in the case. Also, if you would like, you can add a condition to evaluate an integer value or any value based on a condition. Personally, I loved this feature as well because there were cases where one of the structures was unable be to be used, and one was not efficient… However, these improvements are bridging the gap actually.

Async Improvement — ValueType, Task<T> object

To most, this might not come as a surprise, but to those who do some hardcore multithreaded programming and rely on this, for their job, the new improvement is just amazing. What this means, is that you can now easily use the ValueTask. This type actually exists in the System.Threading.Tasks.ValueTask, but comes from an extension library from NuGet; System.Threading.Tasks.Extensions. You can easily download it for your own project, and get started using the new type.

However, there are a few things to note:

  1. This is a value type, instead of a reference type.
  2. The major reason was that, a reference type, as the name suggests, required to be instantiated and an object was created.
  3. However, the value type, if known, would only require a stack push… no other stuff required.

Even the documentation suggests that you should consider using the Task<T> type instead of the ValueTask<T> type. Only if you are aware that there is a performance improvement, then do that, otherwise keep using the Task type.

However, deep somewhere, I do believe Microsoft is working on bringing some improvements to the package and will make this even better. Until then, let’s talk about the next topic in C# 7.

Deconstruction

This is a very interesting topic in C# 7, but I left this one for the last, so that I can easily explain the core concepts that are required, such as the Tuple types, and some other extra features in C# 7, such as ref and out parameters.

The deconstruction, and destructors are two different things, and must never be intertwined under any circumstances. The deconstruction is a language feature, which, provided a function converts the runtime object to a tuple. See how important the concept of tuples was before this section.

The function itself is:

C#
void Deconstruct(out string param1, out int param2) {
    // Set the out parameters here.
}

Without this function, the language won’t be able to provide you with the deconstruct feature, and you would typically have to write a personal function that does this. If we create the function and then execute the following, it works:

C#
void Main()
{
    var person = new Person() { Name = "Afzaal Ahmad Zeeshan", Age = 21 };
 
    var (n, a) = person;
    Console.WriteLine($"{n} is {a} years old.");
}

public class Person {
    public string Name { get; set; }
    public int Age { get; set; }
 
    public void Deconstruct(out string n, out int a) {
        n = this.Name;
        a = this.Age;
    }
}

Removing the function causes an error, that tells the programmer that only a type that has the “Deconstruct” function is able to undergo a deconstruction, otherwise add some helper functions, etc. If none, then you cannot use this in your own code.

The properties of the objects that get returned are depending on what you want to create with the object. Also, you would require to use the tuple to store the values; thus understanding how they works, is a good start for you.

Final Words

C# 7 has some great improvements, be it a syntax improvement, or the language improvement. I am impressed by some major changes, but I was annoyed by a few changes that were never required, or should have been left to the packages that were supplied to built it.

Most of the features of C# 7 are package based, and thus they require a package to be installed from the NuGet libraries. Whereas, a few of the improvements were installed natively — that literal one is such, and I hated these changes… Whereas, the changes that I might have enjoyed were skipped — tuples, value task, etc. They would have been added to the language.

Also, to the reader, I did not cover most of the improvements, because most of them I already did cover in the previous post of mine and were similar thus I left them. However, this post was meant to give you a brief overview of the improvements in C# 7. I hope you enjoyed reading the improvements and which are just magic tricks. 🙂

See you next time!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Pakistan Pakistan
Afzaal Ahmad Zeeshan is a computer programmer from Rabwah, Pakistan, currently living in The Netherlands, likes .NET Core and Node.js for regular everyday development. Afzaal Ahmad works at Adyen as a Developer Advocate.

He is an expert with Cloud, Mobile, and API development. Afzaal has experience with the Azure platform and likes to build cross-platform libraries/software with .NET Core. Afzaal is an Alibaba Cloud MVP, twice he has been awarded Microsoft MVP status for his community leadership in software development, four times CodeProject MVP status for technical writing and mentoring, and 4 times C# Corner MVP status in the same field.

Comments and Discussions

 
SuggestionLiterals and Local functions Pin
Daniele Rota Nodari21-May-17 12:13
Daniele Rota Nodari21-May-17 12:13 
GeneralRe: Literals and Local functions Pin
Afzaal Ahmad Zeeshan21-May-17 13:31
professionalAfzaal Ahmad Zeeshan21-May-17 13:31 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.