With the release of
Visual Studio “15” Preview 4, many new C # 7 features can be tried by yourself. The main innovations of C # 7 are designed to facilitate the work with data, simplify the code and improve performance. From myself I’ll say that C # is moving towards a functional language, adding things like tuples and pattern matching. Not all of the new functionality works as planned, in Preview 4, in these cases it will be indicated what you can still use and how it will work in the future. Well, let's get started.
Out variables
Now using out variables is not as easy as we would like. Before calling a method with out arguments, you must declare the variables that will be passed to this method. Since the values are usually not assigned to these variables during the declaration (which is logical - they will still be overwritten by the method), the
var keyword cannot be used. You need to declare variables with their type:
public void PrintCoordinates(Point p) { int x, y;
In C # 7, added
out variables that allow you to declare variables immediately in a method call:
public void PrintCoordinates(Point p) { p.GetCoordinates(out int x, out int y); WriteLine($"({x}, {y})"); }
The scope for such variables is an external block, which is why you can use them in the expression following the method call.
')
Attention: In Preview 4, there are more stringent constraints on scope: out variables can only be used inside the expression where they were defined. Therefore, in order for the above example to work, you have to wait for the next release.
Since the out declaration of variables occurs in the same expression as passing them as method arguments, the compiler can infer their type (if there are no conflicting overloads for this method), so you can use the
var keyword instead of type:
p.GetCoordinates(out var x, out var y);
Out arguments are widely used in the
Try ... family of methods, where the returned boolean value indicates the success of the operation and out arguments contain the resulting value:
public void PrintStars(string s) { if (int.TryParse(s, out var i)) { WriteLine(new string('*', i)); } else { WriteLine(" - !"); } }
Warning: In this example,
i is used only inside the if block in which it is defined, so this example also works in Preview 4.
One possible improvement (which does not necessarily fall into C # 7) may be the use of wildcard characters (*) instead of those out parameters that will not be used further. For example:
p.GetCoordinates(out int x, out *);
Pattern matching
In the seventh version of C #, the concept of a
pattern (pattern) appears, which in general is a syntactic construct that allows you to check the correspondence of a variable to a specific pattern and extract information from it, if there is such a correspondence.
C # 7 has the following patterns:
- Constant patterns : c (where c is a constant C # expression); check whether a variable is equal to this constant or not.
- Templates of type : T x (where T is a type and x is a variable); check if the variable is of type T, and if it is, then retrieves its value into a new variable x of type T.
- Var templates : var x (where x is a variable); this template is always calculated in true, used to create a new variable of the same type and with the same value.
This is just the beginning, and in the future we will definitely add new templates in C #. To support the templates 2 existing language constructs were changed:
- is now can be used not only with the type, but also with the template (as the right argument).
- The case in the switch statement can now use patterns, not just constants.
In the future, we will add more options for using templates.
Templates with is
Consider a simple example that uses both a constant pattern and a type pattern.
public void PrintStars(object o) { if (o is null) return;
As you can see from the example, template variables (which were declared in the template) have the same scope as out variables, so they can be used inside the external visibility unit.
Note: In Preview 4, for template variables the same as for out variables, more stringent visibility rules apply, so the example will work only in future releases.
Templates and Try methods can be used together:
if (o is int i || (o is string s && int.TryParse(s, out i)) { }
Switch templates and expression
The use of
switch options has been expanded, now you can:
- Use any types (not only primitive).
- Use patterns in case expressions.
- Add additional conditions to case expressions (using the when keyword).
Now consider an example:
switch(shape) { case Circle c: WriteLine($" {c.Radius}"); break; case Rectangle s when (s.Length == s.Height): WriteLine($"{s.Length} x {s.Height} "); break; case Rectangle r: WriteLine($"{r.Length} x {r.Height} "); break; default: WriteLine("< >"); break; case null: throw new ArgumentNullException(nameof(shape)); }
Note the following features of the new extended
switch :
- The order of case expressions now matters. Now the matching logic is the same as for catch expressions: the first order of the expression that satisfies the condition will be selected. Therefore, in this example, it is important that a more specific condition for a square comes before a more general condition for a rectangle; if you swap them, the condition for a square will never work. In such cases, the compiler comes to the rescue, which will mark the explicit unreachable conditions (as well as for the catch ). This change is not a change to an existing behavior: until C # 7, the order in which case expressions were executed was not defined.
- The default condition is always last calculated. Even though the null condition is after it, the default condition will be checked after it. This was done to support existing logic, but it is good practice to make the default condition last.
- A null condition at the end is achievable. This is because the type template follows the current logic of the is operator and does not work for null . Due to this behavior, null will not be mapped to the first type pattern; you must explicitly specify a pattern for it or leave the logic for the silence condition.
The scope for pattern variables declared in a
case is the
switch expression.
Tuples
Sometimes you want to return several values from a method. None of the currently available methods looks optimal:
- Out parameters: the syntax looks overloaded (even if you use the innovations discussed above), not applicable to asynchronous methods.
- System.Tuple <...>: again it looks verbose and requires the creation of an additional object.
- A separate class for each such case: too much code for a type, the only purpose of which is a temporary grouping of several values.
- The dynamic object: performance loss and no type checking at compile time.
To simplify this task, tuples and tuple literals have been added to C # 7:
(string, string, string) LookupName(long id)
The method now returns 3 strings combined into a tuple. The calling code can use them as follows:
var names = LookupName(id); WriteLine($" {names.Item1} {names.Item3}.");
The names of the fields
Item1, Item2, ... are the default names for each tuple, however, it is possible to give the data combined into a tuple better names:
(string first, string middle, string last) LookupName(long id)
Now the elements of the tuple can be accessed as follows:
var names = LookupName(id); WriteLine($" {names.first} {names.last}.");
You can also specify element names in the literal tuple immediately:
return (first: first, middle: middle, last: last);
Tuples can be assigned to each other if the names of their elements do not match: the main thing is that the elements themselves can be assigned to each other. Restrictions will be added in the future, mainly for tuple literals that will signal errors such as randomly reversed element names, etc. In Preview 4, there are no such restrictions yet.
Tuples are a significant type, and their elements are mutable open fields. Tuples can be compared for equality: two tuples are equal (and have the same hash code) if all constituent elements are equal to each other in pairs (and have the same hash code). This behavior makes tuples useful not only for returning multiple values from a method. For example, if you need a dictionary with a composite key, use a tuple as a key. If you need a list where there should be several values at each position, it also uses a list of tuples. (From the translator: do not take this as a guide to using tuples in 100% situations, sometimes a simple class with a couple of properties will better express your intentions and will be easier to support in the future).
Note: Tuples in their work rely on types that are not already in Preview 4, but you can add them to your project using NuGet (do not forget to select “Include prerelease” and specify “nuget.org” as “Package source”), package called
System.ValueTuple .
Unpacking Tuples
Another way to work with a tuple is to unpack it, which is to assign its elements to new variables:
(string first, string middle, string last) = LookupName(id1);
You can also use the
var keyword instead of the type for each variable:
(var first, var middle, var last) = LookupName(id1);
Or even place a
var in front of the brackets:
var (first, middle, last) = LookupName(id1);
You can also unpack a tuple into already declared variables:
(first, middle, last) = LookupName(id2);
You can not only unpack a tuple, any type can be unpacked. To do this, it must have a method of the following form:
public void Deconstruct(out T1 x1, ..., out Tn xn) { ... }
Out parameters correspond to the values that will be assigned as a result of unpacking. Why are out parameters and not tuples used? So that you can have several method overloads with a different number of parameters.
class Point { public int X { get; } public int Y { get; } public Point(int x, int y) { X = x; Y = y; } public void Deconstruct(out int x, out int y) { x = X; y = Y; } } (var myX, var myY) = GetPoint();
This approach will allow you to create a "symmetric" constructor and unpacking method.
As for the out variables, we plan to add wildcard characters to ignore some return parameters.
(var myX, *) = GetPoint();
Warning: It is still unknown whether wildcards will be added in C # 7.
Local functions
Sometimes an auxiliary function only makes sense within the single method in which it is called. Now this function can be declared inside the method:
public int Fibonacci(int x) { if (x < 0) throw new ArgumentException(" !", nameof(x)); return Fib(x).current; (int current, int previous) Fib(int i) { if (i == 0) return (1, 0); var (p, pp) = Fib(i - 1); return (p + pp, p); } }
The arguments of the external method and its local variables are available for the local function, as well as for lambda expressions.
As another example, consider the method implemented as an iterator. In this case, such a method usually requires a non-eager wrapper method to validate the arguments (because the iterator itself is not called until the MoveNext method is called).
With the help of local functions, this problem is solved more elegantly than usual:
public IEnumerable<T> Filter<T>(IEnumerable<T> source, Func<T, bool> filter) { if (source == null) throw new ArgumentNullException(nameof(source)); if (filter == null) throw new ArgumentNullException(nameof(filter)); return Iterator(); IEnumerable<T> Iterator() { foreach (var element in source) { if (filter(element)) { yield return element; } } } }
If the
Iterator method were an ordinary private method, then it could have been called accidentally, without checking the arguments. In addition, it would need to pass the same arguments to the Filter method.
Caution: In Preview 4, local functions must be declared before the call. In the future, this restriction will be relaxed: local functions can be called after all local variables used by them are assigned values.
Literal improvements
C # 7 now has the ability to add
_ as a delimiter to numeric literals:
var d = 123_456; var x = 0xAB_CD_EF;
Separator can be added anywhere between the numbers, it does not affect the value.
Also in C # 7, binary literals appeared:
var b = 0b1010_1011_1100_1101_1110_1111;
Local variables and return values by reference.
Now you can not only pass the parameters to the method by reference (using the
ref keyword), but also return the data from the method by reference, and also save it to the local variable by reference.
public ref int Find(int number, int[] numbers) { for (int i = 0; i < numbers.Length; i++) { if (numbers[i] == number) { return ref numbers[i];
Now it will be convenient to transfer references to specific places in large data structures. For example, in a game, information is contained in a large pre-allocated array of structures (to avoid pauses for garbage collection). Now methods can return a reference to one of such structures, with the help of which the calling code can read and modify this structure.
In order to work with links safely, the following restrictions were introduced:
- You can only return links that are safe to return: references to the objects passed to the method and references to the fields of the objects.
- Variables are initialized with a specific reference and do not change in the future.
Expansion of the list of types returned by asynchronous methods
Until today,
async methods could return only
void ,
Task or
Task <T> . C # 7 now has the ability to create types that can also be returned by the asynchronous method. For example, you can create a
ValueTask <T> structure that will help you avoid creating a
Task <T> object when the result of an asynchronous operation is already available. For many asynchronous scenarios, for example, where buffering is used, this approach can significantly reduce the number of memory allocations and thus improve performance.
Of course, you can come up with other situations in which Task-like objects will be useful. Proper creation of these types will not be an easy task, so we do not expect a large number of developers to create them, but we think that in various frameworks they will be useful and the calling code can simply use
await , as it is now for Task.
Warning: In Preview 4, these types are not yet available.
More class members in the form of expressions
Methods and properties in the form of expressions (expression bodied members), which appeared in C # 6, were often used, but not all types of members of a class could be so declared. Now support for setters, getters, constructors and destructors has been added to C # 7:
class Person { private static ConcurrentDictionary<int, string> names = new ConcurrentDictionary<int, string>(); private int id = GetId(); public Person(string name) => names.TryAdd(id, name);
This is an example of a new functionality added by the community, not by the compiler development team! Hurray, open source!
Warning: In Preview 4, support for these class members is not available.
Throw expressions
Throwing an exception in the middle of an expression is not so difficult: just call the method that does it. But in C # 7, you can now use
throw as part of the expression:
class Person { public string Name { get; } public Person(string name) => Name = name ?? throw new ArgumentNullException(name); public string GetFirstName() { var parts = Name.Split(" "); return (parts.Length > 0) ? parts[0] : throw new InvalidOperationException("No name!"); } public string GetLastName() => throw new NotImplementedException(); }
Warning: In Preview 4, such expressions are not yet available.
Conclusion
Although it’s still far from the release of C # 7, you can already play with most of the new features and understand where C # and .Net generally move (as for me, C # takes some of the features from functional languages, and this often makes the code more readable and less verbose. But everywhere you need to know when to stop, of course).