What's new in C# 7.0

First of all, you have to keep in mind that to work with the new features proposed by version 7.0 of the language, you will need to have Visual Studio 2017 (any version, including the Community Edition) or Visual Studio Code with the OmniSharp Extension (C# plugin), which also allows to use the language in other popular editors like Vim, Emacs, Sublime, Atom, Brackets, and so on.

Once you have that ready, C# 7 features will be available in the IDE and we can start playing with these additions. Also, it's important to note that Microsoft is encouraging the contributors of the coming versions of the language to deploy new features in a faster path, although including a smaller set of new features.

Actually, this version does not include something as foundational to the language as LINQ or async/await. C# 7 adds extra syntactic sugar in some cases, except its most powerful features: the new support for tuples and deconstructions.

Let's start with the "syntactic sugar."

Binary literals and digit separators

You can express binary numbers as literals directly in the definition of the type that holds them, like this, for example:

int[] binNumbers = { 0b1, 0b10, 0b100, 0b1000, 0b100000 };

But when declared in this form, you can easily end up with long expressions difficult to evaluate and assess at first sight. That's why we now have a new language feature called digit separators.

That means you can include any number of underscore symbols located in any position inside a number literal and it will be interpreted correctly by the compiler. In this manner, it becomes easier to read the values.

This is valid for any type of number literal, as it happens in the sixth entry in the next code:

int[] binNumbers = { 0b1, 0b10, 0b100, 0b1_000, 0b100_000, 123_456_ };

In case we want to check the automatic conversion to integers, we can test the result quite easily, adding a couple of lines:

binNumbers.ToList().ForEach((n) => Console.WriteLine($"Item: {n}"));
Console.Read(); 

This produces the following output in the console:

Binary literals and digit separators

Pattern matching and switch statements

In many cases, we need to check the value of a variable marked as out. Remember that in order to use out, the variable has to be initialized first. To illustrate this situation, consider the following code, in which a function has to evaluate whether a string parameter passed to it can be interpreted as an integer or not:

var theValue = "123";
var result = CheckNumber(theValue);
Console.WriteLine($"Result: {result}");
Console.Read();
//…
static object CheckNumber(string s)
{
  // If the string can be converted to int, we double
  // the value. Otherwise, return it with a prefix
  int i = default(int);  // i must be initialized
  if (int.TryParse(s, out i)) {
    return (i * 2);
  }
  else
  {
    return "NotInt_" + s;
  }
}

As you can see, we have to declare and initialize the i variable before we can retrieve the resulting value from the conversion and double it (in case it is an int).

How about avoiding the previous declaration and having i declared and initialized inside the if statement? That's what we can do now with out inline declarations:

static object CheckNumberC7(string s)
{
  // Now i is declared inside the If
  if (int.TryParse(s, out int i))
    return (i * 2);
  else return "NotInt_" + s;
}

We have a more concise, elegant way of expressing the same idea. We're checking whether s matches the int pattern and, if it does, declaring and assigning the resulting value in a single expression.

Another way to use pattern matching is within the switch statements, which have also been extended with more patterns to evaluate the value passed to it. Actually, you can now switch on anything, not just primitive types such as int or string.

Let's see it in some code:

static object CheckObjectSwitch(object o)
{
  var result = default(object);
  switch (o)
  {
    case null:
      result = "null";
      break;
    case int i:
    case string s when int.TryParse(s, out i):
      result = i * 2;
      break;
    case string v:
      result = "NotInt_" + v;
      break;
    default:
      result = "Unknown value";
      break;
  }
  return result;
}

The previous function assumes it is going to receive an object and has to do the following:

  • If the object is null or different from an int or a string, return a string value indicating so
  • If the object is an int or if it is an string convertible to an int, duplicate its value and return it
  • It it is a string not convertible to an int, add a prefix and return it

As per the preceding code, now you can indicate pattern matching to check whatever the value is, and we can even combine similar situations, such as checking for an int or for a string containing an int in sequential case statements.

Observe the use of when in the string pattern matching, which plays the role of an if, really.

Finally, if it is a string but it's not convertible, we use the prefix procedure. These two features are syntactic sugar (as they call it), but they're pretty expressive and help in simplifying type checking and complex checking situations such as the one coded here.

Tuples

In the section named Tuples: implementation in C#, we saw how to declare and use tuples using the Tuple class, and, also, some of the drawbacks linked to that early version or these objects.

Now, in C# 7, tuples reach a new dimension. You no longer have to use the Tuple class to declare tuples, and thanks to pattern matching, the compiler is perfectly comfortable with declarations that include a tuple syntax next to a var definition or use a tuple as the return type of a method (allowing us to return more than a value, without requiring out parameters):

(int n, string s) = ( 4, "data" );

The previous declaration is now understood by the compiler, as the next capture shows:

Tuples

That makes the use of the Tuple class unnecessary and also it makes much natural to work with these types. Besides, we had to use the members Item1, Item2, and so on to access the values of the tuple. Now we can give descriptive names to each member of the tuple to clarify its purpose (such as n and s in this sample).

Another advantage is that you can return a tuple in a function. Let's follow an adapted version of the official demo that PM Mads Torgersen usually presents to explain this feature.

Imagine that we want to know how many items there are inside the initial declaration of binNumbers and also perform a sum of all its members, in the same function. We could write a method like this:

static (int sum, int count) ProcessArray(List<int> numbers)
{
  var result = (sum:0 , count:0);
  numbers.ForEach(n =>
  {
    result.count++;
    result.sum += n;
  });
  return result;
}

Now, invoke the method and present the results in this way:

var res = ProcessArray(binNumbers.ToList());
Console.WriteLine($"Count: {res.count}");
Console.WriteLine($"Sum: {res.sum}");
Console.Read();

We obtain the expected results. But let's go through the code to view the details of the implementation.

First, the return value of the function is a tuple and its members, named accordingly, which makes the calling code more readable. Also, the internal result variable is defined and initialized with the tuple syntax: a list of comma-separated values, optionally prefixed with a name for clarity.

The return value is then assigned to the res variable, which can use the named parameters to output them in the console using string interpolation.

Decomposition

Decomposition is a characteristic that allows us to deconstruct or decompose an object into its parts, or some of its parts.

For instance, in the declaration of the res variable, we could even avoid the use of res by declaring the named members of the tuple, to obtain exactly the same results:

var (count, sum) = ProcessArray(binNumbers.ToList());

As you can see, we can have access to the required returning values, but there's no need to hold them in a named variable; thus, we say that the resulting value has been "decomposed" into its forming parts.

Of course, in this case, we're taking advantage that the type to deconstruct is a tuple already. What about other objects? You can deconstruct any object as long as it has a Deconstruct method defined or you create an extension method with that name.

Let's say we want to be able to decompose a DateTime value. Of course, there's no Deconstruct method defined inside the DateTime object, but we can create one pretty easily:

static void Deconstruct(this DateTime dt, out int hour,
  out int minute, out int second)
{
  hour = dt.Hour;
  minute = dt.Minute;
  second = dt.Second;
}

Once we have that definition accessible, we could "extract" those values from the current time with a sentence like this:

var (hour, minute, second) = DateTime.Now;
Console.WriteLine($"Hour: {hour} - Minute: {minute} - Second: {second}");

And we would get the output shown in the following capture, which also shows the previous calculation on the number or items in the array and its sum:

Decomposition

Local functions

JavaScript programmers are used to passing functions as parameters and returning functions as a return value. That's not available in C#, except for the functionality available through lambda expressions that we saw in the previous chapter.

Local functions are not that, but they allow us to declare a function that is local to another closing function, with the ability to access the variables defined in the upper function. Therefore, they are local to the function in which they are declared.

Go back to our demo of ProcessArray and imagine you want to separate the code inside the ForEach loop aside in another function, but you want to modify the values directly (without the out references).

You could rewrite the process with an inside function of this kind with the following syntax:

static (int sum, int count) ProcessArrayWithLocal(List<int> numbers)
{
  var result = (s: 0, c: 0);
  foreach (var item in numbers)
  {
    ProcessItem(item, 1);
  }
  return result;
  void ProcessItem(int s, int c) { result.s+= s; result.c += c; };
}

This time, we go through the collection using a ForEach loop and, inside the loop, we call the local function ProcessItem, which has access to the result members.

In which cases do these inside functions make sense? One case would be when a helper method is only going to be used inside a single function, like in this case.

Ref return values

Finally, let's learn a bit about these feature, which is only partially available, since they plan to extend it along the quick-path-release cadence of the language they announced in Connect(); 2016 event.

The idea is that in the same way you can pass values by reference, now you can return reference values and even store values in reference variables.

The Mads Torgersen code mentioned previously includes the following (self-explaining) code, to see how we would declare such a function a how we could use it:

public ref int Find(int number, int[] numbers)
{
  for (int i = 0; i < numbers.Length; i++)
  {
    if (numbers[i] == number) 
    {
      return ref numbers[i]; // return the storage location, not the value
    }
  }
  throw new IndexOutOfRangeException($"{nameof(number)} not found");
}

int[] array = { 1, 15, -39, 0, 7, 14, -12 };
ref int place = ref Find(7, array); // aliases 7's place in the array
place = 9; // replaces 7 with 9 in the array
WriteLine(array[4]); // prints 9

In the code, the function is marked with ref right before declaring the return type (int). Later, after declaring an array of numbers, the function is called with a 7 as the first parameter.

The value 7 occupies the fifth position in the array, so its order number is 4. But since the returned value is stored as ref, a subsequent assignment of 9 changes that value inside the array to 9. That's why the final sentence prints 9.

In all, perhaps the changes included in this last version of the language are not as meaningful as there were those in versions 2, 3, or 4 of the language, but even so, they facilitate the programmer's tasks in some situations.

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

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