Introduction
This article explains new features of C#7.1, C#7.2 and lists some of the planned features of C#8.
Background
Since C# 7.0, Microsoft has created two point releases C#7.1 and C#7.2
In C#7.1 and C#7.2, Microsoft has introduced some small but useful features in the language.
Using the Code
Please find the attached project here or download it from my github page and check out the project. It's simple and easy to understand all the features even without going much into the details.
Note: To check different features in the project, you can change language version as explained in this article.
Index
The C# compiler supports C# 7.1 starting with Visual Studio 2017 version 15.3. The most recent version is C# 7.2, which was released in 2017 along with Visual Studio 2017 version 15.5. However, the 7.1 and 7.2 features are turned off by default. To enable these features, we need to change the language version setting for the project.
Following are two methods by which you can change C# language version.
Method 1
- First, right-click on the project node in Solution Explorer => Select Properties. It will open project properties.
- Select the Build tab => select the Advanced button. It will open advanced build settings window.
- In the Language version drop-down, select C# latest minor version (latest), or the specific version C# 7.1/C#7.2, etc. as shown in the image below:
The latest value means you want to use the latest minor version on the current machine. The C# 7.1 means that you want to use C# 7.1, even after newer minor versions are released.
Method 2
You can edit the "csproj" file and add or modify the following lines:
<PropertyGroup>
<LangVersion>latest</LangVersion>
</PropertyGroup>
Please note, if you use the Visual Studio IDE to update your csproj files, the IDE creates separate nodes for each build configuration. You'll typically set the value the same in all build configurations, but you need to set it explicitly for each build configuration or select "All Configurations" when you modify this setting. You'll see the following in your csproj file:
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<LangVersion>latest</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<LangVersion>latest</LangVersion>
</PropertyGroup>
Valid settings for the LangVersion
element are:
ISO-1
ISO-2
3
4
5
6
7
7.1
default
latest
The special string
s default and latest resolve to the latest major and minor language versions installed on the build machine, respectively.
Now let's consider we have the following class (to make it easy for us to understand the features):
public class Fruit
{
public int Id { get; set; }
public string Name { get; set; }
private static List<Fruit> GetFruits()
{
return new List<fruit>
{
new Fruit { Id= 1, Name= "Apples" },
new Fruit { Id= 2, Name= "Apricots" },
new Fruit { Id= 3, Name= "Avocados" },
new Fruit { Id= 4, Name= "Bananas" },
new Fruit { Id= 5, Name= "Boysenberries" },
new Fruit { Id= 6, Name= "Blueberries" },
new Fruit { Id= 7, Name= "Bing Cherry" },
new Fruit { Id= 8, Name= "Cherries" },
new Fruit { Id= 9, Name= "Cantaloupe" },
new Fruit { Id= 10, Name= "Crab apples" },
new Fruit { Id= 11, Name= "Clementine" },
new Fruit { Id= 12, Name= "Cucumbers" }
};
}
public static async Task<List<Fruit>> GetFruitsAsync
(CancellationToken cancellationToken = default(CancellationToken))
{
return await Task<List<Fruit>>.Factory.StartNew(() =>
{
return GetFruits();
});
}
}
Following are some of the new features in C# 7.1.
Before C#7, Tuples were available but without a language support so they were not much efficient. Also, Tuple elements were referenced as item1
, item2
and so on, which made that difficult to understand what it is referencing to. In C#7, Microsoft has added language support for Tuple so that it has allowed providing proper names to the Tuple
elements (still optional). The following code shows C#7.0 version where Tuple
elements have name specified:
var fruits = Fruit.GetFruits);
var tupleExampleQuery = from fruit in fruits
select (Id: fruit.Id, Name: fruit.Name);
var firstElementOld = tupleExampleQuery.First();
string firstFruitOld = $"First fruit is : Id: {firstElementOld.Id.ToString()},
Name: {firstElementOld.Name}";
However, here in the example (and most of the time), tuple names match the name of source elements from where we are referencing, in that kind of scenario, we might want to omit the name. Luckily in C#7.1, we have a feature where we can infer the name of the tuple element from the referencing elements. The following code shows C#7.1 version where tuple elements names are inferred.
var inferredTupleExampleQuery = from fruit in fruits
select (fruit.Id, fruit.Name);
var firstElementNew = inferredTupleExampleQuery.First();
string firstFruitNew = $"First fruit is : Id: {firstElementOld.Id.ToString()},
Name: {firstElementOld.Name}";
Note: In order to use this feature, you need to add a NuGet package System.ValueTuple
.
While learning C#, most of the time, we create console apps. In the scenarios, while testing code for the async method, we had to write some additional logic. We normally had to write boilerplate as follows:
public static void Main()
{
MainAsync().GetAwaiter().GetResult();
}
private static async Task MainAsync()
{
...
}
In our example, we might need to write something like follows:
static int Main(string[] args)
{
var fruits = Fruit.GetFruitsAsync().GetAwaiter().GetResult();
return 0;
}
Luckily, in C#7.1, the main
method can be async
, so in here, our code will now look more similar to the normal async
methods as follows:
static async Task Main(string[] args)
{
var fruits = await Fruit.GetFruitsAsync();
}
Following are allowed versions of main()
:
static void Main()
static void Main(string[])
static int Main()
static int Main(string[])
It has been extended to the following:
static Task Main()
static Task<int>Main()
static Task Main(string[])
static Task<int> Main(string[])
We can use this feature in default value expressions when the target type can be inferred. To understand this feature, let's continue with the above example. In the above example, fruit
method can optionally take CancellationToken
as an argument which can be optionally marked as default. So earlier, we used to do as follows:
public static async Task<List<Fruit>> GetFruitsAsync
(CancellationToken cancellationToken = default(CancellationToken))
{
return await Task<List<Fruit>>.Factory.StartNew(() =>
{
return GetFruits();
});
}
Here, target type can be inferred in default, so we can omit that and make code a little more better.
public static async Task<List<Fruit>> GetFruitsAsync(CancellationToken cancellationToken = default)
{
return await Task<List<Fruit>>.Factory.StartNew(() =>
{
return GetFruits();
});
}
Following are some of the new features in C# 7.2...
Until now, we had Private
, Public
, Protected
, Internal
and Internal Protected
access modifiers. All of the access specifiers are much discussed and we all know what they do, so I won't go into those details.
The Private Protected
access modifier gives the visibility to the member to be accessed for derived classes in the same assembly.
Following are some code examples that help to understand all access modifiers (just read comments in code to understand this feature).
Let's consider we have a private
class as follows:
public class Parent
{
private int private_var { get; set; }
protected int protected_var { get; set; }
public int public_var { get; set; }
internal int internal_var { get; set; }
internal protected int internal_protected_var { get; set; }
private protected int private_protected_var { get; set; }
}
A child/derived class within the current assembly:
public class Child_WithinCurrentAssembly : Parent
{
public Child_WithinCurrentAssembly()
{
this.protected_var = 0;
this.public_var = 0;
this.internal_var = 0;
this.internal_protected_var = 0;
this.private_protected_var = 0;
}
}
A non-child class within current assembly:
public class NonChild_WithinCurrentAssembly
{
public NonChild_WithinCurrentAssembly()
{
Parent p = new Parent();
p.public_var = 0;
p.internal_var = 0;
p.internal_protected_var = 0;
}
}
A child/derived class outside current assembly:
public class Child_OutsideCurrentAssembly : Parent
{
public Child_OutsideCurrentAssembly()
{
this.protected_var = 0;
this.public_var = 0;
this.internal_protected_var = 0;
}
}
A non-child class outside current assembly:
public class NonChild_OutsideCurrentAssembly
{
public NonChild_OutsideCurrentAssembly()
{
Parent p = new Parent();
p.public_var = 0;
}
}
Now, this may have led you to a little confusion in the difference between internal protected
and private protected
. Here is the difference (just remember bold lines):
protected internal
modifier means protected
OR internal
that is - class member is accessible to child class derived from a class (immediate child) and also to any class in the current assembly, even if that class is not a child of class A
(so restriction implied by "protected
" is relaxed).
private protected
means protected
AND internal
. That is - member is accessible only to child classes which are in the same assembly, but not to child classes which are outside assembly (so restriction implied by "protected
" is narrowed - becomes even more restrictive).
To understand this feature, let's first try to understand Named Arguments and its rules.
Normally, when calling a function, all arguments must be passed in order as specified in the function. Named Arguments allows caller method to send arguments in any order as long as the name of the parameter is specified during the call match the parameter name.
For example, consider the following sample method:
void MethodA(string arg1, int arg2, string arg3)
When called without named arguments, the order should be correct like:
MethodA("arg1_value", 1, "arg3_value")
Now if we want to call it with named argument, we can pass arguments in any order we want.
MethodA(arg2:1, arg3:"arg3_value", arg1:"arg1_value")
Now in case we want to pass only some parameters as named arguments, it is possible, but until C#7.2, the positional arguments were always needed to be placed before the named arguments. So we can call MethodA
as follows:
MethodA("arg1_value", 1, arg3:"arg3_value") OR
MethodA("arg1_value", arg2:1, arg3:"arg3_value") OR
MethodA("arg1_value", arg3:"arg3_value", arg2:1)
Now because the rule says the positional arguments always needed to be placed before the named arguments, following method call was not possible before C#7.2, which is now possible.
MethodA(arg1:"arg1_value", 1, arg3:"arg3_value")
This is a very small but useful feature. In C#7.0, Microsoft has introduced digit separator for readability purposes. So following are valid numbers in C#7.0.
int i = 123;
int j = 1_2_3;
int k = 0x1_2_3;
int l_binary = 0b101;
int m_binary = 0b1_0_1;
When used in binary and hexadecimal literals, they were not allowed to appear immediately following the 0x or 0b in C#7.0, now with C#7.2 it is allowed. So now, following are also valid numbers in C#7.2.
byte n_hex = 0x_1_2;
byte o_hex = 0b_1_0_1;
There are a lot of changes made within this feature but to cut it short, I will try to explain one interesting change, which I liked, called in
parameter.
The in
parameter is a complement of the existing ref
and out
keywords. For instance, see what each of the parameters does.
ref
: The ref
keyword is used to pass an argument as a reference. This means that when a value of that parameter is changed in the method, it gets reflected in the calling method. An argument that is passed using a ref
keyword must be initialized in the calling method before it is passed to the called method.
out
: The out
keyword is also used to pass an argument like ref
keyword, but the argument can be passed without assigning any value to it. An argument that is passed using an out
keyword must be initialized in the called method before it returns back to calling a method.
Now take a look at In
parameter in:
The in
keyword is used to pass an argument like ref
keyword and an argument that is passed using an in
keyword cannot be initialized by the called method.
So out
and in
are kind of the opposite: in case of out
parameters, they are required to be modified by the method whereas in
parameters, they are not required to be modified by the method.
The following code will help you to understand the difference and the new in
parameter:
class Program
{
static void Main(string[] args)
{
int a;
a = 1;
Console.WriteLine($"Inside Main-Method_Passbyval {a} before");
Method_Passbyval(a);
Console.WriteLine($"Inside Main-Method_Passbyval {a} after");
Console.WriteLine(Environment.NewLine);
a = 10;
Console.WriteLine($"Inside Main-Method_Passbyref {a} before");
Method_Passbyref(ref a);
Console.WriteLine($"Inside Main-Method_Passbyref {a} after");
Console.WriteLine(Environment.NewLine);
a = 20;
Console.WriteLine($"Inside Main-Method_out_parameter {a} before");
Method_out_parameter(out a);
Console.WriteLine($"Inside Main-Method_out_parameter {a} after");
Console.WriteLine(Environment.NewLine);
a = 30;
Console.WriteLine($"Inside Main-Method_in_parameter {a} before");
Method_in_parameter(in a);
Console.WriteLine($"Inside Main-Method_in_parameter {a} after");
Console.WriteLine(Environment.NewLine);
Console.WriteLine($"Press any key to exit.");
Console.ReadKey();
}
private static void Method_Passbyval(int a)
{
a++;
Console.WriteLine($"Inside Method_Passbyval (a++) {a}");
}
private static void Method_Passbyref(ref int a)
{
a++;
Console.WriteLine($"Inside Method_Passbyref (a++) {a}");
}
private static void Method_out_parameter(out int a)
{
a = 100;
a++;
Console.WriteLine($"Inside Method_out_parameter (a=100 & a++) {a}");
}
private static void Method_in_parameter(in int a)
{
Console.WriteLine($"Inside Method_in_parameter (no change in a) {a}");
}
}
The above code will show o/p as follows. The output shows the clear difference between ref
, out
and in
keywords.
Please note: There are more interesting changes added to this section, but I left you to explore that more.
Following are some features that are being discussed for the upcoming release in C#8.0, some of them have the only prototype designed. But I guess we all are very excited to check those features.
- Nullable reference types
- Async Streams
- Default Interface Implementations
- Extension Everything
I hope you have enjoyed the reading.
Happy programming!