Original text: What's new in C# 7
2016-12-21
Translator's Note: The original text was published in December 2016, when Visual Studio 2017 was still 15 Preview 5, but they remained unchanged until VS 2017.
A number of language features have been added to C# 7:
-
out variable
You can define out variables when you pass parameters to a method
Tuples
It can be used to create lightweight, nameless types that contain multiple public fields. Compilers and IDE tools know these types of semantics.
pattern matching
Create logical branches based on arbitrary types and their values.
Local variables and return values defined as ref
Method parameters and local variables can be referenced elsewhere
Local function
Function definitions can be embedded in functions, which are limited by the scope and visibility of external functions.
More members can use expression grammar
The list of members that can be written using expressions has been expanded.
throw expression
You can throw an exception in the code structure, which was previously impossible because it was a statement.
Genericization of Asynchronous Return Types
Using the method defined by async, you can return types other than Task and Task<T>.
Improving Digital Character Quantity Grammar
Use new symbols to improve the readability of digital constants.
Each feature is described in detail below. You can understand the reasons behind each feature, and you can also learn the relevant grammar. There are also some scenario examples that use these features. All of this will make you a more efficient developer.
out variable
The existing syntax already supports out parameters, but it has been improved in this version.
Before that, you need to declare and use output variables in two statements:
int numericResult; if (int.TryParse(input, out numericResult)) WriteLine(numericResult); else WriteLine("Could not parse input");
Now you can define out variables directly in the parameter list when calling a method, avoiding separate declarations:
if (int.TryParse(input, out int result)) WriteLine(result); else WriteLine("Could not parse input");
You can specify the type of out variable as clearly as above, but the language itself supports implicit types for local variables (automatic inference):
if (int.TryParse(input, out var answer)) WriteLine(answer); else WriteLine("Could not parse input");
-
This code is very readable
Declare the output variable when using it, without adding another line of code
-
There is no need to assign an initial value
When the out variable is declared in the method, it will not be used without assignment.
The most common use of this feature is when using try mode. In this pattern, the method returns a bool value to indicate whether it succeeds or fails, and if it succeeds, the result of its processing is provided through the out variable.
Tuples
C# provides rich syntax to support classes and structures, and classes and structures are mainly used to explain your design ideas. Sometimes, however, rich grammar also requires some minor advantages of extra work. You may often find that when you write about a method, you need a simple structure that contains multiple data elements. To support this situation, C # adds tuples. Tuples are lightweight data structures that contain multiple fields to represent data members. These fields are not validated. In addition, you can't define your own methods in tuples.
Note: Before C 7, tuples had been implemented through API, but there were many limitations in this implementation. Most importantly, members of tuples are named Item1, Item2, etc. Language (Translator's Note: C 7 supports the definition of semantically defined names for tuple fields.
Tuples can be defined by assigning values to each member of a tuple:
var letters = ("a", "b");
This assignment statement creates a tuple through tuple syntax, whose members are Item1 and Item2. You can modify this assignment statement to make tuples semantically member:
(string Alpha, string Beta) namedLetters = ("a", "b");
Note: The new tuple feature requires the System.ValueTuple type. In the preview versions of Visual Studio 2017 and before, you need to add NuGet package System.ValueTuple.
The namedLatters tuple contains two fields, Alpha and Beta. In the tuple assignment statement, you can also specify the name of the field on the right side:
var alphabetStart = (Alpha: "a", Beta: "b");
C# allows you to specify field names on both the left and right sides of the assignment statement:
(string First, string Second) firstLetters = (Alpha: "a", Beta: "b");
The above line produces a warning, CS8123, that the names Alpha and Beta specified on the right side of the assignment statement will be ignored because they conflict with the names First and Econd specified on the left.
The above example shows the basic tuple syntax. Tuples are usually used for the return types of private and internal methods. Tuples provide simple syntax for these methods to return multiple values: it is no longer necessary to define a class or struct type for the return data, and it is easy.
Creating tuples is more efficient. This is a simple and lightweight syntax for defining multivalued data. As an example, the following method finds and returns the minimum and maximum values of a set of integers:
private static (int Max, int Min) Range(IEnumerable<int> numbers) { int min = int.MaxValue; int max = int.MinValue; foreach(var n in numbers) { min = (n < min) ? n : min; max = (n > max) ? n : max; } return (max, min); }
The use of tuples here brings the following advantages:
Save trouble. There is no need to define a class or struct for the return type
No need to create new types
Enhanced languages no longer need to be invoked Create<T1>(T1) Method
The method declaration provides a name for the field returning tuple data. When this method is called, the returned tuple will have Max and Min fields:
var range = Range(numbers);
Sometimes you may want to disassemble the tuple data returned by the method. No problem. You can declare variables individually for each field of a tuple. This is called deconstructing tuples:
(int max, int min) = Range(numbers);
You can also provide similar deconstruction for any class type in. NET by providing a Deconstruct member method for the class. The Deconstruct method provides a set of out parameters corresponding to each attribute you want to deconstruct. The following Point class provides a deconstruction method for extracting X and Y coordinates:
public class Point { public Point(double x, double y) { this.X = x; this.Y = y; } public double X { get; } public double Y { get; } public void Deconstruct(out double x, out double y) { x = this.X; y = this.Y; } }
Now you can extract fields by assigning Point objects to tuples:
var p = new Point(3.14, 2.71); (double X, double Y) = p;
There is no binding name when defining the Deconstruct method. You can name variables when you extract them from an assignment statement:
(double horizontalDistance, double verticalDistance) = p;
For more information, see Tuple theme
pattern matching
The pattern matching function allows you to process in methods based on attributes other than object types. You may already be familiar with object-type-based methods. In object-oriented programming, virtual method and overloaded method grammar are used to implement object-type-based method processing. Base and derived classes provide different implementations. The pattern matching grammar extends this concept by making it easy to implement similar processing patterns based on types and data elements, which are independent of inheritance.
Pattern matching supports is and switch expressions. It checks the object and its attributes to determine whether the object meets the required pattern. Use the when keyword to specify additional rules for the schema.
is expression
The is schema expression extends the familiar is operator to query objects in a way that is not limited to types.
Let's start with a simple question. We will expand this issue to demonstrate how pattern matching can be handled easily. First, let's calculate the sum of the values of a roll.
public static int DiceSum(IEnumerable<int> values) { return values.Sum(); }
Soon you'll find that there are times when you don't roll just one dice in the roll list of results that need to be counted. Each entry entered may have multiple results, not just a number:
public static int DiceSum2(IEnumerable<object> values) { var sum = 0; foreach(var item in values) { if (item is int val) sum += val; else if (item is IEnumerable<object> subList) sum += DiceSum2(subList); } return sum; }
Here is schema expression works well. When checking the type of an item, variables can be initialized at the same time. A valid runtime type variable is created here.
Continuing to expand on the problems in this example, you may find that you need more if and else statements. In this way, you will want to use switch schema expressions.
Upgraded switch statement
This matching expression has a similar syntax to the switch statement that already exists in C#. Before adding new conditions, convert the above code into a matching expression:
public static int DiceSum3(IEnumerable<object> values) { var sum = 0; foreach (var item in values) { switch (item) { case int val: sum += val; break; case IEnumerable<object> subList: sum += DiceSum3(subList); break; } } return sum; }
The grammar of a matching expression is slightly different from that of an is expression, which declares types and variables at the beginning of a case expression.
Matching expressions also support constants, which saves a lot of time when facing simple conditional judgments:
public static int DiceSum4(IEnumerable<object> values) { var sum = 0; foreach (var item in values) { switch (item) { case 0: break; case int val: sum += val; break; case IEnumerable<object> subList when subList.Any(): sum += DiceSum4(subList); break; case IEnumerable<object> subList: break; case null: break; default: throw new InvalidOperationException("unknown item type"); } } return sum; }
The above code adds 0 as a special case of int, while null is another special case, representing no input. This demonstrates an important feature of switch schema expressions: you need to pay attention to the order of case expressions. 0 This condition must precede other int conditions. Otherwise, the int condition matches first, even if the value is 0. If you mistake the order of matching expressions, a condition that should have been matched later is preprocessed, and the compiler marks it up and generates an error.
A similar situation exists when dealing with empty input. As you can see, the branch of a particular IEnumerable must appear before the branch of a general IEnumerable.
This version of the code also adds default branches. No matter where default is placed in the source code, it always makes the final judgement. Therefore, it is generally agreed that default branches should be placed at the end.
Finally, let's add the last case to handle a new dice we added to the game. Some games use percentage dice to represent a larger range of numbers.
Note: Two 10-sided percentile dice can represent each number from 0 to 99. One dice is marked 00, 10, 20,..., 90 on each side, and the other is marked 0, 1, 2,..., 9. Add up the values of two dices to get a number between 0 and 99.
To add this type of dice to a collection, you need to define the corresponding type first:
public struct PercentileDie { public int Value { get; } public int Multiplier { get; } public PercentileDie(int multiplier, int value) { this.Value = value; this.Multiplier = multiplier; } }
Then, a case matching expression is added to handle this new type:
public static int DiceSum5(IEnumerable<object> values) { var sum = 0; foreach (var item in values) { switch (item) { case 0: break; case int val: sum += val; break; case PercentileDie die: sum += die.Multiplier * die.Value; break; case IEnumerable<object> subList when subList.Any(): sum += DiceSum5(subList); break; case IEnumerable<object> subList: break; case null: break; default: throw new InvalidOperationException("unknown item type"); } } return sum; }
When dealing with algorithms based on object types and other attributes, the new pattern matching expression grammar is simpler and more concise. Schema matching expressions organize code through data types, irrespective of inheritance.
For more topics on pattern matching, see Patterns Matching in C#
Reference to local variables and return values
This grammatical feature allows the use and return of variable references defined elsewhere. One example is about large matrices in which you need to find the location of a particular data. Here we define a method to return two indexes in the matrix that represent a location:
public static (int i, int j) Find(int[,] matrix, Func<int, bool> predicate) { for (int i = 0; i < matrix.GetLength(0); i++) for (int j = 0; j < matrix.GetLength(1); j++) if (predicate(matrix[i, j])) return (i, j); return (-1, -1); // Not found }
There are many problems with this code. First, it's a common way to return tuples, and although it's grammatically okay, it's better to use user-defined types (class es or structs) for public API s.
Secondly, this method returns the index of an item in the matrix, through which the caller can refer to the element in the matrix and modify its value.
var indices = MatrixSearch.Find(matrix, (val) => val == 42); Console.WriteLine(indices); matrix[indices.i, indices.j] = 24;
In contrast, you may be more willing to write a method that returns a matrix element reference to change the value of the element. In the past, you could only use unsafe code to return integer pointers.
Let's demonstrate the characteristics of referencing local variables through a series of changes and show how to create a method to return references stored internally. Through these changes, you will learn the rules for returning references and local references to features to avoid accidental abuse of this feature.
We start by modifying the return type of find to ref int. Then modify the return statement to return a value stored in the matrix instead of its pair of indexes:
// Note that this code cannot be compiled. // The method is declared to return the reference type. // But the return statement returns a specific value. public static ref int Find2(int[,] matrix, Func<int, bool> predicate) { for (int i = 0; i < matrix.GetLength(0); i++) for (int j = 0; j < matrix.GetLength(1); j++) if (predicate(matrix[i, j])) return matrix[i, j]; throw new InvalidOperationException("Not found"); }
When you declare that a method returns a ref variable, you must add the ref keyword to all the return statements, which means that the return is a reference, which helps developers to clearly know when they read this code that the return is a reference.
public static ref int Find3(int[,] matrix, Func<int, bool> predicate) { for (int i = 0; i < matrix.GetLength(0); i++) for (int j = 0; j < matrix.GetLength(1); j++) if (predicate(matrix[i, j])) return ref matrix[i, j]; throw new InvalidOperationException("Not found"); }
Now that this method returns a reference to an integer value of the matrix, you need to modify it at the location of the call. var states that it knows that varItem is now int rather than tuple:
var valItem = MatrixSearch.Find3(matrix, (val) => val == 42); Console.WriteLine(valItem); valItem = 24; Console.WriteLine(matrix[4, 2]);
The second WriteLine statement in the preceding example outputs a value of 42 instead of 24. The variable varItem is int, not ref int. The var keyword makes the compiler understand the type, but it does not implicitly add ref modifiers. Because this variable is not a ref variable, ref return copies the value of the variable to the left of the assignment statement.
ref var item = ref MatrixSearch.Find3(matrix, (val) => val == 42); Console.WriteLine(item); item = 24; Console.WriteLine(matrix[4, 2]);
Now the second WriteLine statement prints out 24, which means that the contents saved in the matrix have been modified. Local variables declared with ref modifiers can be used to obtain ref returns. You must initialize the ref variable when it is declared. You cannot separate the declarative statement from the initialization statement.
There are two other rules in C# to prevent you from misusing ref local variables and ref returns:
-
Can't assign ref variable
That is to say, no such statement is allowed: ref int i = sequence.Count();
-
A ref variable whose lifecycle is shorter than the method execution time cannot be returned.
That is, it cannot return a reference to a local variable or a variable in a similar scope.
These rules ensure that you do not accidentally mix value variables and reference variables, and that you do not refer to data that will be garbage collected.
In addition, local references and return references avoid copying values in the algorithm, or do multiple de-references, so they are conducive to improving efficiency.
Local function
Many class designs have methods that are invoked only in one place. These additional private methods make the method smaller and more focused. However, they also make it difficult to read class code. These methods must be understood outside the call context.
For these designs, local functions allow you to declare another method in the context of one method. Local methods make it easier for readers to see the context in which it is called.
Local methods have two very common uses: common iterative methods and common asynchronous methods. Both types of methods generate code that reports errors later than programmers expect. In an iterative method, exceptions are only found when an enumeration call is made. In the asynchronous method, only when the returned task is completed can the abnormal occurrence be observed.
Let's start with an iterative method:
public static IEnumerable<char> AlphabetSubset(char start, char end) { if ((start < 'a') || (start > 'z')) throw new ArgumentOutOfRangeException(paramName: nameof(start), message: "start must be a letter"); if ((end < 'a') || (end > 'z')) throw new ArgumentOutOfRangeException(paramName: nameof(end), message: "end must be a letter"); if (end <= start) throw new ArgumentException($"{nameof(end)} must be greater than {nameof(start)}"); for (var c = start; c < end; c++) yield return c; }
Check that the following code incorrectly calls an iterative method:
var resultSet = Iterator.AlphabetSubset('f', 'a'); Console.WriteLine("iterator created"); foreach (var thing in resultSet) Console.Write($"{thing}, ");
Exceptions are thrown during the resultSet iteration, not when the resultSet is created. In this example, most developers can quickly diagnose the problem. However, in a larger code base, the code that creates iterators is usually not put together with the code that enumerates the results. You can refactor the code so that public methods validate all parameters, and then use private methods to generate enumerations:
public static IEnumerable<char> AlphabetSubset2(char start, char end) { if ((start < 'a') || (start > 'z')) throw new ArgumentOutOfRangeException(paramName: nameof(start), message: "start must be a letter"); if ((end < 'a') || (end > 'z')) throw new ArgumentOutOfRangeException(paramName: nameof(end), message: "end must be a letter"); if (end <= start) throw new ArgumentException($"{nameof(end)} must be greater than {nameof(start)}"); return alphabetSubsetImplementation(start, end); } private static IEnumerable<char> alphabetSubsetImplementation(char start, char end) { for (var c = start; c < end; c++) yield return c; }
This refactoring version throws exceptions immediately because the public method is not an iterative method; only the private method uses yield return syntax. However, this reconstruction has potential problems. Private methods should be invoked only by public interface methods, because it skips all parameter validation. Readers of this class must discover this fact from the entire class and find out if there are references to the alphabetSubsetImplementation method elsewhere.
It is much clearer to define alphabetSubset Implementation as a local function in the public API method:
public static IEnumerable<char> AlphabetSubset3(char start, char end) { if ((start < 'a') || (start > 'z')) throw new ArgumentOutOfRangeException(paramName: nameof(start), message: "start must be a letter"); if ((end < 'a') || (end > 'z')) throw new ArgumentOutOfRangeException(paramName: nameof(end), message: "end must be a letter"); if (end <= start) throw new ArgumentException($"{nameof(end)} must be greater than {nameof(start)}"); return alphabetSubsetImplementation(); IEnumerable<char> alphabetSubsetImplementation() { for (var c = start; c < end; c++) yield return c; } }
The above version clearly shows that local methods are referenced only within the scope of external methods. Local functions also ensure that developers do not accidentally call local functions from other locations in the class to skip parameter validation.
The same technique can also be used in the async method to ensure that the parameters are validated before the actual work and that the exception is thrown immediately:
public Task<string> PerformLongRunningWork(string address, int index, string name) { if (string.IsNullOrWhiteSpace(address)) throw new ArgumentException(message: "An address is required", paramName: nameof(address)); if (index < 0) throw new ArgumentOutOfRangeException(paramName: nameof(index), message: "The index must be non-negative"); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException(message: "You must supply a name", paramName: nameof(name)); return longRunningWorkImplementation(); async Task<string> longRunningWorkImplementation() { var interimResult = await FirstWork(address); var secondResult = await SecondStep(index, name); return $"The results are {interimResult} and {secondResult}. Enjoy."; } }
Note: Some designs use Lambda expressions as local functions. Interested friends can Look at the difference between them.
More members support expressions as function bodies
C# 6 introduces member functions and read-only attributes Use expressions as members of function bodies . C# 7 extends the membership that allows this feature to be used. In C# 7, you can use this feature for constructors, destructors, get and set accessors for attributes, and indexes. Here's an example:
// Expression-bodied constructor public ExpressionMembersExample(string label) => this.Label = label; // Expression-bodied finalizer ~ExpressionMembersExample() => Console.Error.WriteLine("Finalized!"); private string label; // Expression-bodied get / set accessors. public string Label { get => label; set => this.label = value ?? "Default label"; }
Note: This example does not require a destructive method, but it proves that the grammar is valid. Normally you should not implement a class destructor unless you have to release unmanaged resources in it. In addition, you should consider using SafeHandle classes to manage unmanaged resources rather than managing them directly.
These new members supporting the body of expression functions mark an important milestone in the C# language: these features are open source by community members Roslyn Project implementation.
## throw expression
Throw in C # used to be a statement. Because throw is a statement rather than an expression, it can't be used in some places. These include conditional expressions, null-valued merge expressions, and some Lambda expressions. Throw expression is undoubtedly useful as a new member of expression. At this point, you can write the throw expression introduced by C 7 in any structure.
Its grammar is similar to the throw sentence grammar used before. The only difference is that you can use it in some new places, such as conditional expressions:
public string Name { get => name; set => name = value ?? throw new ArgumentNullException(paramName: nameof(value), message: "New name must not be null"); }
This feature also allows throw expressions to be used in initialization expressions:
private ConfigResource loadedConfig = LoadConfigResourceOrDefault() ?? throw new InvalidOperationException("Could not load config");
Previously, these initialization processes needed to be done in the construction method, using throw statements in the body of the function:
public ApplicationOptions() { loadedConfig = LoadConfigResourceOrDefault(); if (loadedConfig == null) throw new InvalidOperationException("Could not load config"); }
Note: Both of the above structures throw exceptions when constructing objects, which is usually difficult to recover. Therefore, we should try our best not to throw an exception in the construction process.
Expanding async return type
Returning Task objects from async methods can cause performance bottlenecks for some paths. Task is a reference type, so using it means assigning objects. The method declared async may return a cached result or complete synchronization, in which case additional allocation becomes an important time cost, and this code is critical to performance. If these allocations occur frequently, they can become very expensive.
The new language features allow async methods to return research beyond Task, Task<T> and void. The return type must still satisfy the async pattern, i.e. there must be accessible GetAwaiter methods. ValueTask has been added to the. NET framework as a concrete example:
public async ValueTask<int> Func() { await Task.Delay(100); return 5; }
Note: You need to add NuGet package System.Threading.Tasks.Extensions before you can use ValueTask in Visual Studio 2017.
Using ValueTask where Task was previously used is a simple optimization. However, if you want to manually optimize more, you can cache the results returned by the asynchronous operation and use it on subsequent calls. The ValueTask structure has a constructor that uses Task as a parameter, so you can construct ValueTask from the return results of existing async methods:
public ValueTask<int> CachedFunc() { return (cache) ? new ValueTask<int>(cacheResult) : new ValueTask<int>(loadCache()); } private bool cache = false; private int cacheResult; private async Task<int> loadCache() { // simulate async work: await Task.Delay(100); cache = true; cacheResult = 100; return cacheResult; }
As with all performance recommendations, you should baseline test both versions before making major rule changes to your code.
Improving numeric literal grammar
Misreading values makes it difficult to use reading code. This often happens when a number is used as a binary mask or other symbol rather than as a numerical value. C# 7 introduces two new features to improve this situation, making the code more fashionable and easier to read: binary literals and digital separators.
When creating a binary mask, or when a binary value is required, in order to make the code more readable, you can write a binary literal directly:
public const int One = 0b0001; public const int Two = 0b0010; public const int Four = 0b0100; public const int Eight = 0b1000;
Constants starting at 0b indicate that they are written in binary numbers.
Binary numbers can be very long, so is introduced as a numeric separator:
public const int Sixteen = 0b0001_0000; public const int ThirtyTwo = 0b0010_0000; public const int SixtyFour = 0b0100_0000; public const int OneHundredTwentyEight = 0b1000_0000;
A numeric delimiter can appear anywhere in a constant. For decimal numbers, they are often used as thousands of delimiters:
public const long BillionsAndBillions = 100_000_000_000;
Digital separators can also be used for decimal, float, and double:
public const double AvogadroConstant = 6.022_140_857_747_474e23; public const decimal GoldenRatio = 1.618_033_988_749_894_848_204_586_834_365_638_117_720_309_179M;
Using these two new features, you can declare more readable numeric constants.