
Dans le billet précédent, nous avons vu les différences entre le C# et le Java au niveau des fonctionnalités communes aux deux langages. Dans ce billet je vais tâcher de présenter les fonctionnalités réellement différenciatrices.
- Partie 1 : Différences dans les ressemblances
- Partie 2 : Un langage multi-paradigme
Délégués: des méthodes objets
Un délégué est un type particulier définissant la signature d’une méthode. On peut rapprocher la notion de délégué à celle des pointeurs de fonctions en C avec un type fort en sus. Ils permettent de manipuler des méthodes et de les utiliser comme paramètres ou comme champs de classe.
// Déclaration d'un type délégué
// Une méthode prenant une string et en revoyant une
delegate string StringOperation(string str);
public class Delegates
{
public static string Reverse(string str) // ne gère pas tous les caractères unicode
{
char[] arr = str.ToCharArray();
Array.Reverse(arr);
return new string(arr);
}
public static void Main(string[] args)
{
StringOperation normalizeOp;
// Création d'une instance du délégué
normalizeOp = Delegates.Reverse;
Console.WriteLine(norsmalizeOp("abcd")); // dcba
// Instantiation du délégué à l'aide d'une méthode anonyme.
normalizeOp = delegate(String str) { return str.Trim(); };
Console.WriteLine(normalizeOp(" abcd ")); // abcd
}
}
Pour reproduire cette fonctionnalité en Java, le développeur devra passer par la déclaration d’un type avec une méthode et l’implémentation de celui (en utilisant une classe anonyme par exemple).
interface StringOperation
{
String invoke(String str);
}
public class Delegates
{
public static String reverse(String str)
{
return new StringBuffer(str).reverse().toString();
}
public static void main(String[] args)
{
StringOperation normalizeOp;
normalizeOp = new StringOperation(){
public String invoke(String str)
{
return Delegates.reverse(str);
}
};
System.out.println(normalizeOp.invoke("abcd")); // dcba
normalizeOp = new StringOperation(){
public String invoke(String str)
{
return str.trim();
}
};
System.out.println(normalizeOp.invoke(" abcd ")); // abcd
}
}
La possibilité d’utiliser une méthode comme variable ou paramètre rend les délégués utiles dans beaucoup de cas, ils sont d’ailleurs utilisés un peu partout en C#: gestionnaire d’événements, actions exécutées dans des threads, callback pour de la programmation asynchrone, LINQ…
Evénements
Dans une interface utilisateur, un grand nombre de composants peuvent soulever des événements (click d’un bouton, sélection d’un menu, transfert terminé…), des classes de l’application peuvent vouloir répondre à ces événements et cela indépendamment de l’émetteur. Le C# simplifie la programmation à base d’événements en fournissant une syntaxe spécifique à base du pattern observateur.
class Button
{
public delegate void Action();
public event Action Click;
public void PerformClick()
{
OnClick();
}
private void OnClick()
{
if (Click != null)
{
Click();
}
}
}
class Program
{
static void PerformAction()
{
Console.WriteLine("Work!");
}
public static void Main()
{
Button button = new Button();
// Subscribe to the event
button.Click += PerformAction;
button.PerformClick(); // Work!
// Unsubscribe
button.Click -= PerformAction;
button.PerformClick(); // Nothing
}
}
En Java, il n’y a pas de mécanisme intégré pour la gestion des événements, il faut manuellement implémenter le pattern observateur.
public interface ClickListener extends EventListener
{
public void click();
}
public class Button
{
protected EventListenerList listenerList = new EventListenerList();
public void addMyEventListener(ClickListener listener)
{
listenerList.add(ClickListener.class, listener);
}
public void removeMyEventListener(ClickListener listener)
{
listenerList.remove(ClickListener.class, listener);
}
public void performClick()
{
onClick();
}
void onClick()
{
Object[] listeners = listenerList.getListenerList();
for (int i=0; i<listeners.length; i+=2)
{
if (listeners[i]==ClickListener.class)
{
((ClickListener)listeners[i+1]).click();
}
}
}
}
public class Program
{
public static void main(String[] args)
{
Button button = new Button();
button.addMyEventListener(new ClickListener() {
public void click(EventArgs evt)
{
System.out.println("Work!");
}
});
button.performClick();
}
}
Expressions Lambda
Une expression lambda est une fonction anonyme pouvant être utilisée pour instancier un délégué. Syntaxiquement, elle se décompose en trois parties: une liste de paramètres, l’opérateur lambda => et le corps de la méthode. Grâce à l’inférence de type, il est souvent inutile de préciser les types des paramètres : le compilateur les déduit à partir du corps de la méthode et du délégué sous-jacent.
delegate bool Assertion(int value); // Equivalent à (int x) => x == 42; Le type est déduit à partir du délégué Assertion isUniverseAnswer = x => x == 42; isUniverseAnswer(42); // true
Une des fonctionnalités les plus intéressantes des expressions lambda (et des méthodes anonymes) est qu’elles s‘exécutent dans le contexte de leurs déclarations, elles peuvent donc utiliser, dans leurs corps, les variables visibles au moment de leurs déclarations.
List<User> FindAllYoungerThan(List<User> users, int limit)
{
// L'expression lambda utilise le paramètre "limit"
return users.FindAll(user => user.Age < limit);
}
Les expressions lambda peuvent également être utilisées pour implémenter les closures. Les closures peuvent être définies comme des fonctions qui capturent l’état des variables visibles au moment de leurs déclarations.
delegate void Action();
static Action CreateAction()
{
int counter = 0;
// Renvoie un délégué, l'action n'est pas exécuté.
// L'état de la variable counter est "capturé" par la closure
return () => Console.WriteLine("counter={0}", counter++);
}
void Main()
{
Action action = CreateAction();
action(); // counter=0
action(); // counter=1
}
LINQ : un langage de requête intégré
LINQ (Language-Integrated Query) est un ensemble de fonctionnalités permettant l’intégration d’un langage de requête directement dans le langage C#.
IEnumerable<int> query =
from user in users
select user.Age;
Les requêtes sont écrites en utilisant une syntaxe déclarative permettant des opérations de filtrage, de tri et de groupage. La même expression peut servir à récupérer et transformer des données provenant de différentes sources : base de données, fichier xml, des collections ; ainsi, une même requête peut récupérer des données d’une base SQL et produire un flux XML en sortie. Un autre aspect intéressant est que l’exécution des requêtes est différée jusqu’au moment où le résultat est demandé. Ceci permet d’améliorer les performances lorsqu’on manipule un grand nombre de données et également d’enchaîner les requêtes.
// 1. Source de données
int[] numbers = new int[] { 0, 1, 2, 3, 4, 5, 6 };
// 2. Création de la requête.
IEnumerable<int> numQuery = from num in numbers
where (num % 2) == 0
select num;
// 3. Exécution de la requête.
// Les éléments sont renvoyés un à un.
foreach (int num in numQuery)
{
Console.Write("{0,1} ", num); // 0 2 4 6
}
Il n’y a pas de magie derrière LINQ : à la compilation, les requêtes sont converties en appels de méthodes.
IEnumerable<int> querySyntax =
from user in users
select user.Age;
IEnumerable<int> methodSyntax = users.Select(user => user.Age);
Bye bye StringUtils avec les méthodes d’extensions
Il est plutôt commun d’avoir dans un projet un ensemble de classes utilitaires permettant d’améliorer les fonctionnalités offertes par certaines classes (StringUtils, ArrayUtils…)
Les méthodes d’extension permettent d’ajouter des méthodes à des types existants sans avoir à les modifier ou à en dériver. Ces méthodes sont utilisées comme si elles étaient des méthodes d’instances, elles sont donc plus faciles à trouver à l’aide de la complétion qu’une classe utilitaire. On peut ainsi éviter de se retrouver avec plusieurs fois la même méthode dans des classes utilitaires différentes.
string reversed = "Example".Reverse();
reversed = StringUtils.Reverse("Example")
Les méthodes d’extensions sont utilisées pour rajouter les méthodes utilisés par LINQ au type IEnumerable (Select, Where, GroupBy, OrderBy, Average…)
Passage par référence
En Java, les paramètres des méthodes sont toujours passés par valeur. La méthode travaille sur une copie des éléments qui lui sont passés : une copie de la valeur pour les types primitifs ou une copie de la référence pour les objets. A cause de cela, des actions simples comme échanger la valeur de deux entiers dans une méthode ne sont pas facilement faisables en Java (il faut soit passer par un wrapper soit utiliser de l’arithmétique).
Par défaut cela fonctionne de la même manière en C#, cependant il est possible de préciser pour une méthode que ses paramètres doivent être passés par référence. En plus du cas trivial d’échange d’entiers cela peut être utilisé pour créer une méthode renvoyant plus d’un objet.
Ainsi, un des patterns communs dans le framework .NET est le pattern TryXXX. Par exemple les méthodes int Int32.Parse(string) et bool Int32.TryParse(string, out int), servant à convertir une chaîne de caractères en entier, utilisent ce pattern. La méthode TryParse est semblable à la méthode Parse si ce n’est qu’elle ne renvoie pas d’exception si la conversion échoue éliminant la nécessité de devoir les gérer.
// Utilisation de la method Parse, avec gestion des exceptions
int quantity;
try
{
quantity = int.Parse(input);
}
catch (FormatException)
{
quantity = 0;
}
// Utilisation de TryParse
int quantity;
if (!Int32.TryParse(txtQuantity.Text, out quantity))
{
quantity = 0;
}
Le passage par référence peut également être utilisé pour améliorer les performances lorsqu’on a affaire à des types valeurs. Quand un type valeur est passé à une méthode, sa valeur est copiée, si ce type occupe beaucoup de mémoire (e.g. un type valeur représentant une matrice 4×4 comporte au moins 16 floats) il est conseillé de passer ce type par référence plutôt que par valeur afin d’éviter la copie.
Matrix view = ...; Matrix projection = ...; Matrix viewProjection; Matrix.Multiply(view, projection, out viewProjection); // viewProjection = view * projection;
Type partiel
La génération automatique de code se retrouve un peu partout dans l’écosystème .NET : Visual Studio l’utilise entre autres pour Windows Form. Pour faciliter l’utilisation et l’édition du code généré, il est possible de séparer la définition de la classe, structure ou interface en plusieurs fichiers sources. Chaque fichier source contient une partie de la déclaration et tout se retrouve assemblé au moment de la compilation. Cela permet d’avoir une partie du type éditée par le générateur de code alors qu’une autre peut être éditée par le développeur sans craindre que le générateur écrase les modifications.
// Dans fichier A
partial class HumptyDumpty
{
private string name = "Humpty Dumpty";
public void Rhyme()
{
Sat();
Fall();
TryPutTogether();
}
public void TryPutTogether()
{
string putTogether = @"All the king's horses and all the king's men
Couldn't put Humpty together again.";
Console.WriteLine(putTogether);
}
}
// Dans fichier B
partial class HumptyDumpty
{
void Sat()
{
Console.WriteLine("{0} sat on a wall,", name);
}
void Fall()
{
Console.WriteLine("{0} had a great fall.", name);
}
}
Une autre utilisation intéressante se trouve au niveau des tests unitaires. Un jeu de tests pour une classe donnée comporte souvent plus de lignes de codes que la classe elle-même. On peut utiliser les types partiels pour diviser les tests unitaires en plusieurs fichiers. De cette manière, il est plus aisé de travailler sur les tests tout en conservant la possibilité de lancer les tests sur toute la classe en une seule session (on a toujours affaire à une seule classe de test).
Mouais…pas de quoi fouetter un chat…
Dynamic
Depuis la version 4 (la dernière en date), le C# fournit un type dynamique. Il est utilisé pour des problèmes d’interopérabilité avec les API COM (API Office pour ne citer qu’elle) mais également pour interagir avec des langages dynamiques tel que IronPython ou IronRuby (les versions de Python et Ruby tournant sur le CLR). Cette fonctionnalité n’est pas utilisée couramment, mais peut se révéler utile dans les situations où la programmation dynamique facilite les choses.
Exemple de code C# appelant une méthode Python en utilisant les dynamiques et IronPython.
ScriptRuntime py = Python.CreateRuntime();
dynamic random = py.UseFile("random.py");
int[] items = new int[]{0,1,2,3,4,5};
random.shuffle(items);
Opérateur ??
L’opérateur ?? (Null-coalescing) est utilisé pour définir une valeur par défaut. Il équivalent à un opérateur ternaire conditionnel testant la nullité d’une valeur.
string address = user.BillingAddress ??
user.ShippingAddress ??
user.ContactAddress;
// Equivalent to:
string address = user.BillingAddress;
if (address == null)
{
address = user.ShippingAddress;
if (address == null)
{
address = user.ContactAddress;
}
}
Gestion automatique des ressources
Le mot clé using fournit un moyen simple de gérer l’élimination des ressources de manière automatique.
string line = String.Empty;
StreamReader reader;
try
{
reader = new StreamReader("file.txt"));
line = reader.ReadLine();
}
finally
{
if (reader != null)
reader.Dispose();
}
Cet exemple utilisant le mot clé using est équivalent au précédent.
string line = String.Empty;
using (StreamReader reader = new StreamReader("file.txt"))
{
line = reader.ReadLine();
}
Console.WriteLine(line);
Cette fonctionnalité de gestion automatique des ressources sera introduite dans Java 7 (via l’interface java.lang.AutoCloseable).
try ( FileInputStream in = new FileInputStream("input.txt");
FileOutputStream out = new FileOutputStream("output.txt"))
{
int c;
while((c=in.read()) != -1 )
out.write();
}
Et il y en a encore
- Implémentation simple d’itérateur avec le mot clé
yield - Type Nullable
- Code non managé
- Compilation conditionnel
- …
Conclusion!
Ce billet vous a permis de découvrir les fonctionnalités différenciatrices du langage. Fonctionnalités faisant du C# un langage plus complet, possédant des facettes fonctionnelles et dynamiques, permettant d’augmenter l’expressivité du langage.
La puissance d’un langage ne réside pas uniquement dans les fonctionnalités qu’il propose, elle dépend également de l’écosystème dans lequel il gravite, des outils et librairies disponibles pour celui-ci. Dans le prochain billet, nous présenterons cet écosystème : framework, ide, outils de build, intégration continue…

Pingback: Why Java folks should look forward to Scala | /var/log/mind
Pingback: Why Java folks should stop looking down on C# [3/4] The ecosystem