Intermédiaire

Les nouveautés de C# 6 et C# 7

csharp6-7Lors des TechDays 2015, Microsoft a présenté une nouvelle version 6 de son langage phare C#. A peine un an après, son successeur C# 7 prépare son arrivée. Chacune de ses versions apporte un ensemble de facilités aux développeurs.

 

A travers cet article, nous passons en revue l’ensemble des nouveautés apportées par C# 6 et celles attendues par C# 7. Chaque fonctionnalité sera décortiquée avec des exemples illustratifs.

 

Avant d’aller dans le vif du sujet, rappelons les versions actuelles de C#, des frameworks .NET respectifs, des versions Visual Studio relatives, de leurs dates de sortie et des principales fonctionnalités apportées.
csharp-versions

C# 6

 

Propriétés automatiques

Les propriétés automatiques, introduites depuis C# 3, rendent la déclaration d’une propriété plus concise puisque aucune logique additionnelle n’est requise dans les accesseurs de la propriété. C# 6 leur apporte deux améliorations principales.

 

  • Initialisation
    C# 6 permet de joindre l’initialisation d’une propriété automatique à sa déclaration. Cela n’était pas possible dans les versions antérieures où l’initialisation se faisait forcément après la déclaration (i.e. via le constructeur).
// Avant C# 6

// déclaration d'une auto-propriété
public string OldAutoProp { get; set; }

// initialisation dans le constructeur
public OldCSharp()
{
	OldAutoProp = "INIT";
}

// Avec C# 6

// déclaration et initialisation d'une auto-propriété
public string NewAutoProp { get; set; } = "INIT";

 

  • Lecture seule
    La déclaration d’une propriété automatique en lecture seule n’était pas possible jusqu’ici. Dans le meilleur des cas, un setter privé (ou protégé) est utilisé pour se rapprocher de cette notion. Nul besoin de le faire avec C# 6.
// Avant C# 6

// déclaration d'une auto-propriété (presque!) en « lecture seule »
// on dit presque, car le setter privé peut être appelé n'importe
// où au sein de la classe (dans un constructeur ou dans une méthode)
public string OldReadOnlyAutoProp { get; private set; }

// initialisation dans le constructeur
public OldCSharp()
{
	OldReadOnlyAutoProp = "INIT";
}

// Avec C# 6

// déclaration et intialisation d'une auto-propriété en « lecture seule »
public string NewReadOnlyAutoProp { get; } = "INIT";

// déclaration et initialisation peuvent se faire distinctement

// déclaration d'une auto-propriété en « lecture seule »
public string NewReadOnlyAutoProp { get; }

// initialisation dans le constructeur
public NewCSharp()
{
	NewReadOnlyAutoProp = "INIT";
}

 

Initialisation des indexeurs

C# 6 introduit une nouvelle façon d’initialiser les objets indexés (i.e. les dictionnaires).

// Exemple 1 : Initialisation des dictionnaires

// Avant C# 6
var oldDic = new Dictionary<int, string>;
{
	{1 , "a"},
	{2 , "b"},
	{3 , "c"},
};

// Avec C# 6
var newDic = new Dictionary<int, string>;
{
	[1] = "a",
	[2] = "b",
	[3] = "c"
};

// Exemple 2 : Initialisation des objets indexés

// classe indexée
public class IndexedClass
{
	private readonly string[] _array = new string[10];

	public string this[int i]
	{
		get
		{
			return _array[i];
		}
		set
		{
			_array[i] = value;
		}
	}
}

// Avant C# 6
var oldObj = new IndexedClass();
oldObj[1] = "a";

// Avec C# 6
var newObj = new IndexedClass {[1] = "a"};

 

Initialisation des collections

C# 6 prend en charge le support de l’extension « Add » ce qui permet d’avoir un code plus allégé et épuré lors de l’initialisation des collections.

// Avant C# 6

// initialisation d'une collection de personnes
var oldPers = new List<Person>;
{
	new Person("Walter", "White"),
	new Person"Jon", "Snow")
};

// Avec C# 6

// définition de méthodes d'extensions « Add »

// 1ère méthode d'extension « Add » acceptant deux paramètres « string »
public static void Add(this List<Person> persons, string first, string last)
{
	persons.Add(new Person(first, last));
}

// 2ème méthode d'extension « Add » acceptant un paramètre « int »
public static void Add(this List<Person> persons, int nbr)
{
	for (var index = 0; index < nbr; index++)
	{
		persons.Add(new Person("F_" + index, "L_" + index));
	}
}

// initialisation d'une collection de personnes
var newPers = new List<Person>;
{
	{ "Walter", "White" },
	{ "Jon", "Snow" },
	{ 2 },
};

 

Membres sous forme d’expression

C# 6 simplifie la déclaration des propriétés, méthodes et index dont le corps est constitué d’une seule instruction. La syntaxe rappelle les expressions lambda.

// Avant C# 6
public string FullName
{
	get { return FirstName + " " + LastName; }
}

// Avec C# 6
public string FullName =>FirstName + " " + LastName;

 

Filtres d’exception

C# permet jusqu’à maintenant d’intercepter les exceptions en se basant sur leurs types et en allant du plus spécifique vers le plus général lors de la définition des blocs catch appropriés. C# 6 étend les blocs catch avec des conditions grâce à l’introduction du mot clé « when ». Lorsque la condition n’est pas validée, le bloc catch n’est pas évalué. Lorsque plusieurs conditions sont valides à la fois, seul le premier bloc catch est évalué.

// Avant C# 6
try
{
	// un traitement levant une exception
	throw new ArgumentException("__ERR_2");
}
catch (Exception ex)
{
	if (ex is FormatException && ex.Message == "__ERR_1")
	{
		Log("__MSG_1");
	}
	else if (ex is ArgumentException && ex.Message == "__ERR_2")
	{
		Log("__MSG_2");
	}
	else
	{
		Log("__MSG_3");
	}
}

// Avec C# 6
try
{
	// un traitement levant une exception
	throw new ArgumentException("__ERR_2");
}
catch (FormatException ex) when (ex.Message == "__ERR_1")
{
	Log"__MSG_1");
}
catch (ArgumentException ex) when (ex.Message == "__ERR_2")
{
	Log("__MSG_2");
}
catch (Exception)
{
	Log("__MSG_3");
}

 

Await dans les catch / finally

Avant C# 6, il n’était pas possible d’utiliser le mot clé « await » au sein d’un bloc catch ou finally. Désormais, c’est possible de le faire. Cela est très pratique, par exemple, pour journaliser des erreurs dans un bloc catch ou libérer des ressources dans un bloc finally suite à une exception dans un code asynchrone.

// Avant C# 6
private static async Task AsyncJob()
{
	Exception exception;

	try
	{
	  // un traitement levant une exception
		throw new Exception("__ERR");
	}
	catch (Exception ex)
	{
		exception = ex;
	}

	if (exception != null)
	{
		await AsyncLog(exception.Message);
	}

	await AsyncDispose();
}
				
// Avec C# 6
private static async Task AsyncJob()
{
	try
	{
	  // un traitement levant une exception
		throw new Exception("__ERR");
	}
	catch (Exception ex)
	{
		await AsyncLog(ex.Message);
	}
	finally
	{
		await AsyncDispose();
	}
}

// journalisation des erreurs
private static Task AsyncLog(string msg)
{
	return Task.Factory.StartNew(() =>
	{
		Thread.Sleep(1000);
		Log(msg);
	});
}

// libération des ressources
private static Task AsyncDispose()
{
	return Task.Factory.StartNew(() =>
	{
		Thread.Sleep(1000);
		Dispose();
	});
}

 

Import statique

La directive « using static » permet d’importer les méthodes statiques d’une classe et de les utiliser sans préfixe comme si elles étaient déclarées dans la classe courante.

// Avec C# 6
namespace Csharp6Demo
{
	using System;
	using static System.Console;
	using static System.DateTime;

	public class StaticImport
	{
		// utiliser Now au lieu de DateTime.Now
		public DateTime Date { get; } = Now;

		public void Print()
		{
			// utiliser WriteLine au lieu de Console.WriteLine
			WriteLine(Date.ToShortDateString());
		}
	}
}

 

Opérateur « nameof »

Il est parfois utile de manipuler le nom d’une méthode ou d’une variable dans le code. Cela se faisait jusqu’ici via une chaîne « en dur », difficile à maintenir en cas de refactoring. C# 6 répond à ce besoin avec l’opérateur « nameof » permettant de retrouver le nom d’un symbole donné.

private double _price;
public double Price
{
	get { return _price; }
	
	// Avant C# 6
	set { _price = value; OnPropertyChanged("Price"); }
	
	// Avec C# 6
	set { _price = value; OnPropertyChanged(nameof(Price)); }
}

 

Opérateur de nullité conditionnelle « ? »

L’opérateur « ? » offre une façon élégante et concise pour accéder aux membres d’un objet sans lever d’exception de type « NullReferenceException ». On peut aussi chaîner cet opérateur pour naviguer au sein d’une arborescence de membres.

// Exemple 1 : accès aux membres d'un objet

// Avant C# 6
string country = "Undefined";
IEnumerable<Person> persons = FindPersons(take: 1);
if (persons != null)
{
	Person person = persons.FirstOrDefault();
	if (person != null && person.Address != null && person.Address.Country != null)
	{
		country = person.Address.Country;
	}
}

// Avec C# 6
string country = FindPersons(take: 1)?.FirstOrDefault()?.Address?.Country ?? "Undefined";

// Exemple 2 : accès aux membres d'un tableau

// Avant C# 6
string firstname = null;
Person[] persons = GetPersons();
if (persons != null && persons.Length >= 2 && persons[1] != null)
{
	firstname = persons[1].FirstName;
}

// Avec C# 6
string firstname = GetPersons()?[1]?.FirstName;

// Exemple 3 : invocation d'un delegate

// Avant C# 6
public void OnPropertyChanged(string propertyName)
{
	if (this.PropertyChanged != null)
	{
		this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
	}
}

// Avec C# 6
public void OnPropertyChanged(string propertyName)
{
	this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

 

Chaînes interpolées

La construction des chaînes de caractères a été améliorée dans C# 6 grâce à une syntaxe plus facile à lire et écrire que le fameux « String.Format ». Il faut noter que les valeurs d’une chaîne interpolée sont formatées par défaut dans la culture courante. Pour changer ce comportement, C# 6 introduit la notion de « FormattableString ». Cela signifie qu’une chaîne interpolée est convertible et peut être donc formatée selon une culture différente de la culture courante.

// Exemple 1 : interpolation de chaines

// Avant C# 6
public override string ToString()
{
	return string.Format("My name is {0} {1}", FirstName, LastName);
}

// Avec C# 6
public override string ToString()
{
	return $"My name is {FirstName} {LastName}";
}

// Exemple 2 : formatage selon une culture

// déclaration d'une valeur de type double
double dbl = 1.5;

// affichage de la chaine interpolée selon la culture courante
Console.WriteLine($"double in french culture = {dbl}"); // 1,5

// affichage de la chaine interpolée selon une culture invariante (V1)
Console.WriteLine(FormattableString.Invariant($"double in invariant culture = {dbl}")); // 1.5

// affichage de la chaine interpolée selon une culture invariante (V2)
IFormattable str = $"double in invariant culture = {dbl}";
Console.WriteLine(str.ToString(null, CultureInfo.InvariantCulture)); // 1.5

Remarques

  • .NET 4.5 et VS 2013
    Bien que C# 6 ait accompagné la sortie du .NET 4.6 et de VS 2015, il est possible d’utiliser la plupart de ses fonctionnalités (hormis FormattableString) sous .NET 4.5. Il est également possible d’utiliser du C# 6 sous VS 2013 à condition d’inclure le package nuget « Microsoft.Net.Compilers ». Cependant, VS 2013 ne considère pas les instructions C# 6 comme syntaxiquement valides et les remonte au niveau de l’onglet « Erreurs » même si la compilation se passe correctement.
  • ASP.NET MVC 5
    C# 6 est pris en charge nativement dans ASP.NET MVC 6 et VS 2015. Cependant, il n’est pas compatible avec ASP.NET MVC 5. Pour utiliser C# 6 dans ASP.NET MVC 5 sous VS 2013/2015, il suffit d’inclure le package nuget « Microsoft.CodeDom.Providers.DotNetCompilerPlatform ».
  • ReSharper et VS 2013
    ReSharper sous VS 2013 considère le code C# 6 comme erroné et le surligne en rouge. Pour résoudre le problème, il suffit d’activer la fonctionnalité C# 6 au niveau ReSharper (via le tooltip à gauche du code).
  • VS 2013 et VS 2015
    Pour de multiples raisons (migration des VMs pas terminée, licences limitées, etc.), certaines équipes peuvent utiliser à la fois des versions VS 2013 et VS 2015. Au premier abord, cela n’a pas d’impact tant que le package nuget « Microsoft.Net.Compilers » est inclus dans la solution. Cependant, cela impacte de manière significative le temps de compilation sous VS 2015. Ce dernier ne compile plus de manière native et s’appuie sur le package nuget lors de la compilation, ce qui explique la lenteur. Une des astuces pour résoudre ce problème, consiste à éditer le(s) csproj pour rajouter une condition sur l’import du package nuget en question.
// bypass de l'import si un fichier « vs15.flag » se trouve sous le dossier parent de la solution
// si le fichier « vs15.flag » existe, la compilation se fera de manière native et donc rapide
<Import Project="..\packages\Microsoft.Net.Compilers.1.3.2\build\Microsoft.Net.Compilers.props"
Condition="Exists('..\packages\Microsoft.Net.Compilers.1.3.2\build\Microsoft.Net.Compilers.props') 
AND !Exists('..\vs15.flag')" />

 

C# 7

Au moment de l’écriture de cet article, il n’y a toujours pas de date officielle de sortie pour C# 7 ni pour la liste définitive de ses fonctionnalités. Cependant, la Preview 5 de Visual Studio ’15’ nommé Visual Studio 2017 suite aux dernières nouvelles de la conférence Connect(), lève le voile sur les fonctionnalités potentielles de C# 7.

 

Les littéraux binaires

Les versions actuelles de C# permettent de représenter des littéraux dans une forme digitale ou hexadécimale. C# 7 introduit en plus la forme binaire. Le préfixe à utiliser pour définir un littéral binaire est 0b.

// Avec C# 7
int b = 0b1010;

 

Les séparateurs digitaux

C# 7 apporte une facilité syntaxique, existante en Java 7, permettant d’améliorer la lisibilité des littéraux binaires, digitaux ou hexadécimaux. Le caractère underscore « _ » est utilisé comme séparateur dans un littéral.

// Avant C# 7
int d = 123456789;
int x = 0xABCDEF;

// Avec C# 7
int d = 123_456_789;
int x = 0xAB_CD_EF;
int b = 0b1010_1011_1100_1101_1110_1111;

 

Membres sous forme d’expression

C# 6 a introduit cette fonctionnalité pour les propriétés, méthodes et index. C# 7 l’étend pour les constructeurs, les destructeurs et les exceptions.

// Avec C# 7
class Point
{
	private static int Total;

	public int x { get; set; }
	public int y { get; set; }

	// expression constructeur
	public Point() => Total++;

	// expression destructeur
	~Point() => Total--;

	// expression throw
	public Point Move() => throw new NotImplementedException();
}

 

Les variables de sortie

Actuellement, avant d’utiliser une variable de sortie « out », il faut la déclarer. Avec C# 7, cela n’est plus nécessaire. La variable de sortie est déclarée au moment de son utilisation.

// Exemple 1

// Avant C# 7
static int? OldConvert(string str)
{
	int val;
	if (int.TryParse(str, out val)) return val;
	return null;
}

// Avec C# 7
static int? NewConvert(string str)
{
	if (int.TryParse(str, out int val)) return val;
	return null;
}

// Exemple 2

// méthode avec une variable de sortie « out »
static void Modify(out int val)
{
	val = new Random().Next();
}

// Avant C# 7
int x;
Modify(out x);
Console.WriteLine(x);

// Avec C# 7
Modify(out int y);
Console.WriteLine(y);

 

Les variables de référence

Les variables de référence « ref » étaient jusqu’ici utilisées lors du passage des paramètres à une fonction. C# 7 étend leur utilisation aux variables locales et aux retours de fonctions. Ainsi, il est désormais possible qu’une fonction renvoie en retour une référence. Il est également possible de stocker une référence dans une variable locale.

// Avec C# 7

// une fonction qui renvoie une référence
public static ref int Find(int number, int[] numbers)
{
	for (int i = 0; i < numbers.Length; i++)
	{
		if (numbers[i] == number)
		{
			return ref numbers[i];
		}
	}
	
	throw new IndexOutOfRangeException($"{number} not found");
}

// déclaration d'un tableau d'entiers
int[] array = { 2, 14, -3, 0, 8, 9, -6 };

// une variable locale qui stocke une référence
ref int place = ref Find(8, array);

Console.WriteLine(array[4]); // affiche 8
place = 5;
Console.WriteLine(array[4]); // affiche 5

Les fonctions locales

Avec C# 7, on peut déclarer et appeler une fonction locale à l’intérieur d’un scope donné (constructeur, propriété ou méthode). La fonction locale a accès aux paramètres de l’appelant. Elle peut être récursive ou asynchrone. Le corps d’une fonction locale peut être défini sous forme d’expression lambda.

// Avec C# 7
public int MyProperty
{
	get
	{
		// déclaration d'une fonction locale
		int Func() => new Random().Next(10);
		
		// utilisation d'une fonction locale
		return Func() * Func() - Func();
	}
}

 

Les tuples

Avec les versions précédentes de C#, avoir plusieurs valeurs de retour dans une fonction n’était pas possible sans le recours à des variables de sortie « ref » ou à des objets créés sur mesure. Désormais, c’est de l’histoire ancienne avec C# 7, car une fonction peut renvoyer plusieurs valeurs de retour à la fois. C# 7 apporte également des améliorations au niveau de la construction et la déconstruction d’un tuple.

// Avec C# 7

// Exemple 1 : fonction avec plusieurs valeurs de retour « nommées »
public static (int sum, int sub) SumSubOne(int a, int b)
{
	return (a + b, a - b);
}

// construction d'un tuple
var one = SumSubOne(4,2);

// affichage d'un tuple
Console.WriteLine($"{one} {one.sum} {one.sub}");

// Exemple 2 : fonction avec plusieurs valeurs de retour « anonymes »
public static (int, int) SumSubTwo(int a, int b)
{
	return (a + b, a - b);
}

// construction d'un tuple
var two = SumSubTwo(4,2);

// affichage d'un tuple
Console.WriteLine($"{two} {two.Item1} {two.Item2}");

// Exemple 3 : construction d'un tuple
var t1 = new Tuple<int, bool>(1, true); // Avant C# 7
var t2 = (foo: 1, bar: true); // Avec C# 7
Console.WriteLine($"{t2.foo} {t2.bar}");

// Exemple 4 : déconstruction d'un tuple
(var sum, var sub) = SumSubOne(4,2);

// traitement quelconque
var mul = sum * sub;
Console.WriteLine("{sum} {sub} {mul}");

 

Le filtrage par motif

Le filtrage par motif, en anglais pattern matching, existe déjà dans les langages fonctionnels comme F#. Il consiste à vérifier si une valeur donnée correspond à un motif et si c’est le cas déclencher le traitement associé. C# 7 s’appuie les opérateurs « is » et « when » pour la définition des motifs dans les instructions conditionnelles « if » et « switch ».

// Avant C# 7
static void OldPrint(object o)
{
	if (o is DateTime)
	{
		var d = (DateTime)o;
		Console.WriteLine(d);
	}
}

// Avec C# 7
static void NewPrint(object o)
{
	if (o is DateTime d)
	{
		Console.WriteLine(d);
	}
}

// Avec C# 7
static void NewSwitch(object o)
{
	switch (o)
	{
		case string s:
			Console.WriteLine($"object is a string of length {s.Length}");
			break;
		case int i when i % 2 == 0 :
			Console.WriteLine($"object is an even int");
			break;
		case int i when i % 2 != 0:
			Console.WriteLine($"object is an odd int");
			break;
		case Point p:
			Console.WriteLine($"object is point(${p.x},${p.y})");
			break;
		case null:
			Console.WriteLine($"object is null");
			break;
		default:
			Console.WriteLine($"object is something else");
			break;
	}	
}

 

Remarque

Les fonctionnalités C# 7 présentées ont été testées sous VS 2017 et .NET 4.5.2. Certaines, telles que les types « record », n’ont pas été présentées dans l’article vu qu’elles ont été introduites dans VS ’15’ Preview mais retirées dans VS 2017. Enfin, il faut noter que certaines fonctionnalités sur les tuples nécessitent d’inclure le package nuget « System.ValueTuple ».

 

Conclusion

À travers cet article, nous avons présenté une synthèse des fonctionnalités apportées par C# 6 et celles qui sont attendues par C# 7. La version 6 apporte des améliorations plutôt syntaxiques dans l’ensemble. La version 7 comble des fonctionnalités longtemps manquantes telles que les littéraux binaires et prend en charge d’autres issues de langages fonctionnels (i.e. F#) telles que le filtrage par motif.
Le langage C# continue à évoluer assez constamment grâce aux efforts de Microsoft notament via l’adoption d’un modèle open source pour .NET Core (compilateur Roslyn, etc.) et la création de la Fondation .NET (que Google a rejoint récemment).
La modernisation de C# se fait de manière agile, comme en témoignent les échanges sur github entre les développeurs et les concepteurs. Cela joue un rôle clé dans l’appréciation ou la dépréciation des fonctionnalités futures. Le délai (~ 1 an et demi) entre les deux versions 6 et 7 est l’un des plus courts comparé aux versions précédentes et peut être le fruit de cette conduite agile. C# devient de facto le langage de référence de Microsoft à la fois riche et évolutif pour faire du service (web api, wcf), du web (asp.net mvc) ou du mobile (xamarin).

© SOAT
Toute reproduction interdite sans autorisation de la société SOAT

Nombre de vue : 1335

AJOUTER UN COMMENTAIRE