Avec la sortie de Visual Studio 20017, nous avons eu également le droit à une nouvelle version du C#, le C# 7.0, avec son lot de nouveautés.
Alors ne vous attendez pas à une révolution, mais plutôt à un ensemble d'ajouts pour améliorer l'écriture et l'exécution du code.
Faison un rapide tour de tout ça.
Bon vous connaissez tous les paramètres "out". Par exemple la méthode:
static void DivAndModulo(int value, int divider, out int result, out int remainder)
{
remainder = value % divider;
result = value / divider;
}
que l'on invoke de cette manière
static void CallDivAndModulo()
{
int result, remainder;
DivAndModulo(10, 3, out result, out remainder);
Console.WriteLine($"10/3={result}, reste {remainder}");
}
L'un des points parfois agaçant des paramètres 'out' c'est de devoir prédéfinir toutes les variables qui vont être utilisées par les paramètres, comme 'result' et 'remainder' dans l'exemple précédent.
Avec C# 7.0, nous avons maintenant les 'variables out' avec la possibilité de définir nos variables au moment où on passe nos variables comme paramètres.
Dans notre exemple cela donne:
static void CallDivAndModuloWithOutVariables()
{
DivAndModulo(10, 3, out int result, out var remainder);
Console.WriteLine($"10/3={result}, reste {remainder}");
}
Comme on le constate, il suffit d'ajouter le type de la variable dans l'appel pour
déclarer les variable. Comme notre méthode appelée défini le type du paramètre, nous
pouvons utiliser var a la place du type.
A noter que la portée des variables 'out' se trouve dans le bloc englobant la définition, ce qui permet d'utiliser nos variables dans les lignes qui se trouvent dans le bloc de même niveau.
L'une des utilisations communes des variables 'out' est avec le modèle Try..., par exemple:
static void CallTryParseWithOutVariables(string s)
{
if (int.TryParse(s, out int n)) { Console.WriteLine($"Nombre: {n}"); }
else { Console.WriteLine("Pas un nombre"); }
}
On constate que l'écriture de ce code est plus court que d'habitude.
Dernier point intéressant, on peut ignorer une variable out grâce à _.
static void CallDivOnly()
{
DivAndModulo(10, 3, out int result, out var _);
Console.WriteLine($"10/3={result}");
}
Enfin terminées les variables dummy ;)
C# 7.0 introduit la notions de "patterns" (ou "modèles"), qui sont des éléments syntaxiques qui permettent de tester qu'une valeur correspond à une certaine "forme" et d'extraire des informations de ce test. On utilise ces patterns dans des éléments du language.
C# 7.0 gère les patterns suivants:
c (où c est une expression constante) qui test
si l'entrée est égale à c.T x (où T est un type et x est un identificateur) qui
test si l'entrée est du type T et si c'est le cas extrait la valeur de l'entrée dans
une nouvelle variable x de type Tvar x (où x est un identificateur) qui est toujours
valide et place simplement la valeur de l'entrée dans une nouvelle variable x avec le
même type que l'entrée.On utilise ces patterns dans deux extensions du language C# 7.0.
Les expressions is peuvent désormais supporter un pattern dans la partie droite, à la place d'un simple type.
static void IsWithPatterns(object o)
{
// constant pattern "null"
if (o is null) return;
if (o is "test") return;
// type pattern "int i"
if (!(o is int i)) return;
WriteLine(new string('*', i));
}
Voici deux exemples d'utilisation des patterns dans une expression is.
Dans le "constant pattern" l'utilité réside essentiellement dans le fait qu'il n'a pas
nécessaire de faire appelle aux méthodes d'égalité des objets (je vous rappelle à toutes
fins utiles que l'opérateur d'égalité == ne s'appliquer sur un object avec un string),
var l'opérateur is et les patterns nous avons un test de type, et ensuite une
comparaison de valeur.
Le second pattern est plus utile, nous vérifions que notre objet est d'un type en particulier et si c'est le cas on l'affecte dans une variable du type en question.
Comme pour les variables out (qui ont étrangement la même syntaxe) la portée des variables définies de cette manières est du bloc englobant.
On peut combiner is avec des patterns et Try...:
static void ComplexIsPattern(object o)
{
if(o is int i || (o is string s && int.TryParse(s,out i)))
{
WriteLine(new string('*', i));
}
}
L'instruction switch a été modifiée afin de:
casecasestatic void SwitchWithPattern(object shape)
{
switch (shape)
{
case Circle c:
WriteLine($"Cercle d'un rayon de {c.Radius}");
break;
case Rectangle s when (s.Width == s.Height):
WriteLine($"Carré {s.Width} x {s.Height}");
break;
case Rectangle r:
WriteLine($"Rectangle {r.Width} x {r.Height}");
break;
case "test":
WriteLine("C'est un test");
break;
default:
WriteLine("<forme inconnue>");
break;
case null:
throw new ArgumentNullException(nameof(shape));
}
}
On constate différentes choses avec cette nouvelle instruction switch:
case est important: comme pour les clauses catch,
les clauses case sont validées dans l'ordre, et la première qui est valide est
exécutée. Dans notre exemple il est important que le test du carré soit effectué AVANT
celui du rectangle. De même le compilateur vous prévient si une clause case n'est
jamais atteinte.default en dernier.is et qu'ils ne valident par le null, ce qui
nous garanti que les valeurs null ne sont pas validées par n'importe quel type qui
pourrait être défini dans une clause précédente.Les variables définies dans les patterns d'une clause cause ont une portée limitée au
bloc du case.
Attention les instructions goto case ... ne sont applicables qu'aux case constante,
pas avec des patterns.
Haa l'une de mes fonctionnalités préférées du C# 7.0 :)
Il est assez courant d'avoir besoin de retourner plusieurs valeurs depuis une méthode. Avec les versions précédentes du C# nous avions les options peu optimales suivantes:
System.Tuple<>: utilisation verbeuse, nécessite d'allouer un objet tuple.dynamic: réduction des performances de code et pas de vérification statique de type .Pour améliorer ça, le C# 7.0 apportent le type tuple et les tuples littéraux.
static (string, int, string) ReturnsTuple() // Type de retour tuple
{
return ("un", 2, "trois"); // Tuple littéral
}
Notre méthode renvoie trois valeurs encapsulées dans une valeur tuple.
L'appelant de la méthode recoit un tuple et peut accéder aux éléments individuels:
static void UseTuple()
{
var values = ReturnsTuple();
WriteLine($"{values.Item1}, {values.Item2}, {values.Item3}");
}
Les noms Item1, etc. sont les noms des membres par défaut des tuples, mais ils ne sont vraiment descriptifs. Heureusement désormais on peut si on le souhaite nommer nos éléments de tuple.
static (string first, int, string last) ReturnsTupleWithNames() // tuple with names
{
return ("un", 2, "trois"); // tuple literal
}
maintenant nous pouvons utiliser des noms plus explicites
static void UseTupleWithNames()
{
var values = ReturnsTupleWithNames();
WriteLine($"{values.first}, {values.Item2}, {values.last}");
}
On peut également sépcifier les noms explicitement lors de la définition des tuples.
static void UseTupleWithExplicitNames()
{
var values = (f: "un", second: 2, last: "trois");
WriteLine($"{values.f}, {values.second}, {values.last}");
}
A savoir que les noms ne sont que des alias aux noms Item*, donc les noms Item*
existent toujours. Ce n'est pas un nouveau type qui est généré, mais le type ValueTuple<>
qui est "décoré" par le compilateur.
De ce fait, on peut assigner n'importe quel tuple dans un autre a partir du moment où chacun des membres est du même type et que le nombre de membres eset identique, les noms "originaux" ne changeant pas.
ATTENTION: Si vous utilisez les ValueTuple<> dans un framework qui ne les supportent pas
(vous aurez une erreur de compilation ou un type tuple est introuvable),
il faut installer le package Nuget System.ValueTuple.
Une autre manière d'exploiter les tuples, est la déconstruction.
Une déclaration de déconstruction est une syntaxe qui permet de décomposer chaque élément du tuple dans une variable.
static void DeconstructingDeclaration()
{
(string f, int c, string l) = ReturnsTuple(); // deconstructing
WriteLine($"{f}, {c}, {l}");
}
On peut utiliser var dans la déclaration individuelle des variables.
static void DeconstructingDeclarationVarInside()
{
(var f, var c, var l) = ReturnsTuple(); // var inside
WriteLine($"{f}, {c}, {l}");
}
Mais on peut également définir var de manière plus globale en dehors des parenthèse
comme abréviation.
static void DeconstructingDeclarationVarOutside()
{
var (f, c, l) = ReturnsTuple(); // var outside
WriteLine($"{f}, {c}, {l}");
}
On peut également déconstruire dans des variables existantes, c'est ce qu'on appelle affectation de déconstruction.
static void DeconstructingAssignment()
{
string first = "first";
int count = -5;
string last = "last";
(first, count, last) = ReturnsTuple(); // deconstructing assignment
WriteLine($"{first}, {count}, {last}");
}
La déconstruction n'est pas valable uniquement pour les tuples. N'importe quel type peut être déconstruit a partir du moment ou une méthode (d'instance ou d'extension) deconstrucor est définie de la forme suivante:
public void Deconstruct(out T1 x1, ..., out Tn xn) { ... }
Les paramètres de sorties constituent les valeurs qui résultent de la déconstruction.
class DeconstructObject
{
public void Deconstruct(out string name, out int count)
{
name = Name;
count = Count;
}
public string Name { get; set; }
public int Count { get; set; }
}
static void DeconstructingObject()
{
var dobj = new DeconstructObject { Name = "Nom", Count = 2 };
var (n, c) = dobj;
WriteLine($"{n}, {c}");
}
L'intérêt de déconstruire un objet plutôt que de renvoyer un tuple, est qu'on peut surcharger la méthode avec autant de déclinaisons que l'on veut.
Tout comme pour les variables 'out' il est possible d'ignorer un valeur de déconstruction
avec _.
Parfois des méthodes utilitaires ne sont utilisées qu'à l'intérieur d'une méthode. On peut définir une méthode anonyme à l'intérieur de la méthode, malheureusement elles ne supportent pas certains choses (paramètres out, mot clé yield, etc.).
Désormais les fonctions locales vont nous permettrent de faire tout celà. Tout comme les méthodes anonymes les fonctions locales ont accès aux paramètres et variables de la portée du bloc où est déclarée la fonction locale.
static void LocalFunc()
{
foreach (var line in GetLines())
{
WriteLine(line);
}
void Pow(int value, out int result)
{
result = value * value;
}
IEnumerable<string> GetLines()
{
for (int i = 0; i < 5; i++)
{
Pow(i, out int r);
yield return $"{i}*{i}={r}";
}
}
}
Les nombres littéraux ont également leur part d'amélioration.
On peut désormais utiliser _ dans un nombre "séparateur digital", celà ne représente
rien, ce caractère sert de séparateur afin de permettre une meilleure lecture d'un
nombre.
De plus C# 7.0 les littéraux binaires.
static void Litterals()
{
int dec = 12_34; // decimal
int hex = 0xA1_23; // hexadecimal
int boo = 0b1010_1100; // binaire
WriteLine($"{dec}, {hex}, {boo}");
}
Oui vous avez bien lu! Tout comme vous pouvez transmettre des éléments par référence
(grâce au modificateur ref), vous pouvez désormais les retourner par référence, et
enregistrer leur référence au lieu de leur valeur.
static ref int Find(int number, int[] numbers)
{
for (int i = 0; i < numbers.Length; i++)
{
if (numbers[i] == number)
{
return ref numbers[i]; // return the storage location, not the value
}
}
throw new IndexOutOfRangeException($"{nameof(number)} not found");
}
static void RefReturns()
{
int[] array = { 1, 15, -39, 0, 7, 14, -12 };
ref int place = ref Find(7, array); // aliases 7's place in the array
place = 9; // replaces 7 with 9 in the array
WriteLine(array[4]); // prints 9
}
Cette syntaxe est très utile pour transmettre des emplacements dans de grosses structures. Par exemple un jeu peut maintenir un grand nombre de données préallouées (pour éviter trop de sollicitation du garbage collector). Ce type méthode peut désormais nous transmettre une référence directement sur une structure grâce à laquelle on peut la lire et la modifier.
Il y a quelques restrictions pour rester sécurisé
Jusqu'à présent les méthodes async en C# devaient retourner soit void, soit Task,
soit Task<T>. C# 7.0 autorise d'autres types à être définis comme type retourné
par une méthode async.
Les expressions de corps de méthodes proposées par C# 6.0 (ces méthodes dont on définis le corps comme des expressions) ne couvraient pas tous les types de membre.
Le C# 7.0 ajoute les accesseurs (getters/setters), les constructeurs et les finaliseurs.
class Person
{
private static ConcurrentDictionary<int, string> names = new ConcurrentDictionary<int, string>();
private int id = GetId();
static int GetId() => 123;
public Person(string name) => names.TryAdd(id, name); // constructors
~Person() => names.Clear(); // destructors
public string Name
{
get => names[id]; // getters
set => names[id] = value; // setters
}
}
A partir du C# 7.0 nous pouvons désormais provoquer une exception dans une expression.
class User
{
public string Name { get; }
public User(string name) => Name = name ?? throw new ArgumentNullException(nameof(name));
public string GetFirstName()
{
var parts = Name.Split(new string[] { " " }, StringSplitOptions.None);
return (parts.Length > 0) ? parts[0] : throw new InvalidOperationException("No name!");
}
public string GetLastName() => throw new NotImplementedException();
}
Voilà c'est tout. Comme je l'ai indiquer en préambule, pas de révolution majeur, mais quelques ajouts bien sympathique pour nous faciliter l'écriture de notre code.
Vous trouverez tous les codes de cet article à cette adresse: https://github.com/ygrenier/tests/tree/master/CSharp7NewFeatures.
A bientôt
Yanos