Webinar: C++ semantics - 06.11
This article covers the new version of the C# language - C# 10. Compared to C# 9, C# 10 includes a short list of enhancements. Below we described the enhancements and added explanatory code fragments. Let's look at them.
Now you can set initialization of fields and properties in structures:
public struct User
{
public User(string name, int age)
{
Name = name;
Age = age;
}
string Name { get; set; } = string.Empty;
int Age { get; set; } = 18;
}
Beginning with C# 10, you can declare a parameterless constructor in structures:
public struct User
{
public User()
{
}
public User(string name, int age)
{
Name = name;
Age = age;
}
string Name { get; set; } = string.Empty;
int Age { get; set; } = 18;
}
Important. You can use parameterless constructors only if all fields and/or properties have initializers. For example, if you do not set the Age initializer, a compiler will issue an error:
Error CS0843: Auto-implemented property 'User.Age' must be fully assigned before control is returned to the caller.
Before, you could use the with expression with records. With C#10, you can use this expression with structures. Example:
public struct User
{
public User()
{
}
public User(string name, int age)
{
Name = name;
Age = age;
}
public string Name { get; set; } = string.Empty;
public int Age { get; set; } = 18;
}
User myUser = new("Chris", 21);
User otherUser = myUser with { Name = "David" };
It is clear that the property that we are changing (in this case, the Name field) must have a public access modifier.
Beginning with C# 10, you can use the using directive across an entire project. Add the global keyword before the using phrase:
global using "Library name"
Thus, the using directive allows you not to duplicate the same namespaces in different files.
Important. Use global using construction BEFORE code lines that include using without global keyword. Example:
global using System.Text;
using System;
using System.Linq;
using System.Threading.Tasks;
// Correct code fragment
Otherwise:
using System;
using System.Linq;
using System.Threading.Tasks;
global using System.Text;
// Error CS8915
// A global using directive must precede
// all non-global using directives.
If you wrote the namespace that was previously written with the global keyword, the IDE will warn you (IDE: 0005: Using directive is unnecessary).
Sometimes you need to use the namespace within the entire file. This action may shift the tabs to the right. To avoid this problem, you can now use the namespace keyword. Write the namespace keyword without braces:
using System;
using System.Linq;
using System.Threading.Tasks;
namespace TestC10;
public class TestClass
{
....
}
Before C# 10, it was necessary to keep namespace braces open on the entire file:
using System;
using System.Linq;
using System.Threading.Tasks;
namespace TestC10
{
public class TestClass
{
....
}
}
Clearly, you can declare only one namespace in the file. Accordingly, the following code fragment is incorrect:
namespace TestC10;
namespace MyDir;
// Error CS8954
// Source file can only contain
// one file-scoped namespace declaration
as well as the following piece of code:
namespace TestC10;
namespace MyDir
{
....
}
// Error CS8955
// Source file can not contain both
// file-scoped and normal namespace declarations.
C# 10.0 introduces the optional keyword - class. The class keyword helps understand whether a record is of a reference type.
Therefore, the two following records are identical:
public record class Test(string Name, string Surname);
public record Test(string Name, string Surname);
Now it's possible to create record structs:
record struct Test(string Name, string Surname)
By default, the properties of the record struct are mutable, unlike the standard record that have init modifier.
string Name { get; set; }
string Surname { get; set; }
We can set the readonly property to the record struct. Then access to the fields will be equivalent to the standard record:
readonly record struct Test(string Name, string Surname);
where the properties are written as:
string Name { get; init; }
string Surname { get; init; }
The equality of two record struct objects is similar to the equality of two structs. Equality is true if these two objects store the same values:
var firstRecord = new Person("Nick", "Smith");
var secondRecord = new Person("Robert", "Smith");
var thirdRecord = new Person("Nick", "Smith");
Console.WriteLine(firstRecord == secondRecord);
// False
Console.WriteLine(firstRecord == thirdRecord);
// True
Note that the compiler doesn't synthesize a copy constructor for record struct types. If we create a copy constructor and use the with keyword when initializing a new object, then the assignment operator will be called instead of the copy constructor (as it happens when working with the record class).
As my colleague wrote in the article on the enhancements for C# 9 , records have the overridden toString method. There is an interesting point about inheritance as related to this method. The child objects cannot inherit the overriden toString method from the parent record. C# 10 introduces the sealed keyword so that the child objects can inherit the ToString method. This keyword prevents the compiler from synthesizing the ToString implementation for any derived records. Use the following keyword to override the ToString method:
public sealed override string ToString()
{
....
}
Let's create a record that tries to override the toString method:
public record TestRec(string name, string surname)
{
public override string ToString()
{
return $"{name} {surname}";
}
}
Now let's inherit the second record:
public record InheritedRecord : TestRec
{
public InheritedRecord(string name, string surname)
:base(name, surname)
{
}
}
Now let's create an instance of each record and type the result to the console:
TestRec myObj = new("Alex", "Johnson");
Console.WriteLine(myObj.ToString());
// Alex Johnson
InheritedRecord mySecObj = new("Thomas", "Brown");
Console.WriteLine(mySecObj.ToString());
// inheritedRecord { name = Thomas, surname = Brown}
As we can see, the InheritedRecord did not inherit the toString method.
Let's slightly change the TestRec record and add the sealed keyword:
public record TestRec(string name, string surname)
{
public sealed override string ToString()
{
return $"{name} {surname}";
}
}
Now let's re-create two instances of the records and type the result to the console:
TestRec myObj = new("Alex", "Johnson");
Console.WriteLine(myObj.ToString());
// Alex Johnson
InheritedRecord mySecObj = new("Thomas", "Brown");
Console.WriteLine(mySecObj.ToString());
// Thomas Brown
And.. woohoo! The InheritedRecord inherited the toString method from the TestRec.
C# 8.0 introduced the property pattern that allows you to easily match on fields and/or properties of an object with the necessary expressions.
Before, if you needed to check any nested property, the code could look too cumbersome:
....{property: {subProperty: pattern}}....
With C#10, you just need to add the dots between the properties:
....{property.subProperty: pattern}....
Let's see the change using the example of the method of taking the first 4 symbols of the name.
public record TestRec(string name, string surname);
string TakeFourSymbols(TestRec obj) => obj switch
{
// old way:
//TestRec { name: {Length: > 4} } rec => rec.name.Substring(0,4),
// new way:
TestRec { name.Length: > 4 } rec => rec.name.Substring(0,4),
TestRec rec => rec.name,
};
The example above shows that the new type of property access is simpler and clearer than before.
Before, this feature was not supported. C# 10 allows you to use string interpolation for constant strings:
const string constStrFirst = "FirstStr";
const string summaryConstStr = $"SecondStr {constStrFirst}";
Interesting fact. This change relates only to string interpolation for constant strings, i.e the addition of a constant character is not allowed:
const char a = 'a';
const string constStrFirst = "FirstStr";
const string summaryConstStr = $"SecondStr {constStrFirst} {a}";
// Error CS0133
// The expression being assigned to
// 'summaryConstStr' must be constant
In earlier versions of C#, a deconstruction could assign values to EITHER declared variables (all are declared), OR variables that we initialize during calling (all are NOT declared):
Car car = new("VAZ 2114", "Blue");
var (model, color) = car;
// Initialization
string model = string.Empty;
string color = string.Empty;
(model, color) = car;
// Assignment
The new version of the language allows simultaneous use of both previously declared and undeclared variables in deconstruction:
string model = string.Empty;
(model, var color) = car;
// Initialization and assignment
The following error occurred in the C#9 version:
Error CS8184: A deconstruction cannot mix declarations and expressions on the left-hand-side.
As mentioned earlier, the list of changes is not as large as in the C#9 version. Some changes simplify the work, while others provide previously unavailable features. The C# is still evolving. We're looking forward for new updates of the C# language.
Haven't read about new C# 9 features yet? Check them out in our separate article.
If you want to see the original source, you can read the Microsoft documentation.
0