RSS

Archives de Tag: .Net

Dispose parallèle et efficace

Dans .NET, pour chaque classe utilisée en parallèle (par plusieurs tâches, ou « thread safe » pour les anglophones) et implémentant IDisposable, il faut péniblement implémenter une méthode « Dispose » fonctionnant en parallèle aussi.
Ici je propose une petite structure qui nous facilite la vie, et qui en plus a le mérite d’être efficace: rapide et petite.

Code de la structure:

internal struct DisposeParallèle
{
enum LibéréOuPas { PasEncoreSupprimé, DéjàSupprimé }

int déjàSuppriméOuPas;

internal void Dispose(Action MéthodeDeSuppression)
{
// "Interlocked" est bien plus rapide que "lock".

LibéréOuPas l = (LibéréOuPas)
System.Threading.Interlocked.CompareExchange(
ref this.déjàSuppriméOuPas,
(int)LibéréOuPas.DéjàSupprimé,
(int) LibéréOuPas.PasEncoreSupprimé);

// Ici, "déjàSuppriméOuPas" a toujours pour valeur DéjàSupprimé, et il n'en changera plus. 😉

if (l == LibéréOuPas.PasEncoreSupprimé)
MéthodeDeSuppression();
}
}

Remarques:

  1. On utilise « Interlocked », qui est bien plus rapide que « lock ».
    La preuve: http://www.drdobbs.com/windows/boosting-performance-with-atomic-operati/226900048
  2. On se contente de mémoriser un simple entier 32 bits, là où un « lock » aurait nécessité un objet et un booléen.
    J’aurais aimé ne mémoriser qu’un booléen, d’ailleurs, mais « Interlocked »
    ne propose pas de méthode « CompareExchange » sur un booléen.
  3. En tant que simple structure, on économise les ressources objets, et on n’a pas besoin d’appeler un constructeur.

Exemple d’utilisation:

internal class MaClasse : IDisposable
{
DisposeParallèle disposeParallèle;

void IDisposable.Dispose()
{
this.disposeParallèle.Dispose(this.suppression);
}

void suppression()
{
// votre code...
}

~MaClasse() // si nécessaire
{
((IDisposable)this).Dispose();
}
}

Ça reste assez simple à utiliser, avec un code bien découpé donc facile à maintenir.

Publicités
 

Étiquettes : , , , , , , , ,

UniversalSerializer

[ Le code source se trouve dans l’article original en anglais et mis à jour, voir ici ]

Un sérialiseur universel pour .Net .

C’est quoi ?

UniversalSerializer est un sérialiseur pour .Net.
En d’autres termes, il transcode un arbre d’instances d’objets en un tableau d’octets, et vice versa.

En résumé

L’objectif de UniversalSerializer est d’être capable de sérialiser tout type sans effort.

  • Pas besoin d’ajouter des attributs ni des interfaces aux types (classes & structures).

  • Lorsqu’une instance de classe est référencée plusieurs fois, elle n’est sérialisée qu’une seule fois.

  • Les références circulaires sont autorisées.

  • Les mécanismes de sérialisation et de transcodage existant sont réutilisés.
    Actuellement: [Serializable], ISerializable, [ValueSerializer] et [TypeConverter].

  • Les types sans constructeur par défaut (sans paramètres) sont autorisés, si un constructeur paramétrique peut être exploité (c’est automatique).

  • Les classes ICollection non génériques peuvent être sérialisées si une méthode Add ou Insert peut être exploitée (c’est automatique).

Bien entendu, toutes les constructions ordinaires telles que les classes, structures, propriétés publiques, champs publics, énumérations, collections, dictionnaires, etc.. sont sérialisées par UniversalSerializer.

Lorsqu’un type n’est pas directement sérialisable, UniversalSerializer propose deux mécanismes:

  • ITypeContainers
    Nous pouvons englober le type (ou l’ensemble de types) qui pose problème dans une classe personnalisée qui gérera sa sérialisation et sa désérialisation.

  • Un ensemble de filtres
    Nous pouvons bloquer certains types, et demander au sérialiseur de stocker certains champs privés de la source.

Mon plus grand souhait est que les gens ajoutent de nouveaux Conteneurs et filtres dans le futur, et qu’ainsi peut-être un jour tous les types pourront être sérialisés.

Exemples d’utilisation

var data = new Window();
byte[] DataBytes = UniversalSerializer.Serialize(data);
var data2 = UniversalSerializer.Deserialize<Window>(DataBytes);

C’est aussi simple que ça !

Exemple pour WPF

Il existe une DLL spécialisée pour WPF, qui gère plus de types de WPF:

var data = new Window();
byte[] bytesData = UniversalSerializerWPF.SerializeWPF(data);
var data2 = UniversalSerializerWPF.DeserializeWPF<Window>(bytesData);

Exemple pour WinForms

Il existe une DLL spécialisée pour WinForms, qui gère plus de types de WinForms:

var data = new Form();
byte[] DataBytes = UniversalSerializerWinForm.SerializeWinForm(data);
var data2 = UniversalSerializerWinForm.DeserializeWinForm<Form>(DataBytes);

Pourquoi et comment est-ce fait ?

J’avais besoin d’un sérialiseur universel, capable de sérialiser n’importe quoi sans modification.

Mais les sérialiseurs existant posent des problèmes:

  • Certains nécessitent des attributs ou des interfaces spéciaux.
  • D’autres ne prennent pas en compte les champs.
  • Les références sont un vrai problème. En général, les instances sont dupliquées, et les références des objets désérialisés pointent vers différentes instances.
    Et les références circulaires ne sont jamais gérées.
  • Aucun ne peut gérer des types sans constructeur par défaut (sans paramètres), pour autant que je sache.
  • Certaines classes .Net particulières nécessitent plus d’attention mais sont scellées et donc ne peuvent pas être héritées avec des attributs de sérialisation.

Donc la solution nécessitait un ensemble de techniques et de mécanismes.
Nous allons passer en revue ces mécanismes dans les prochains chapitres.

Le code original

UniversalSerializer est construit à partir de fastBinaryJSON 1.3.7, de Mehdi Gholam, qui sérialise vers un format binaire inspiré par JSON.
UniversalSerializer y ajoute une certaine universalité en permettant de sérialiser n’importe quel type.
Notez que j’ai dû modifier fastBinaryJSON en profondeur, donc je ne peux plus me synchroniser avec le code source original de fastBinaryJSON. Par conséquent, UniversalSerializer n’est une extension de fastBinaryJSON et je ne maintiendrai pas de compatibilité dans le futur.

Sérialiser des classes sans constructeur par défaut

Dans .Net, les constructeurs de classes par défaut (sans paramètres) ne sont pas imposés. Mais presque tous les sérialiseurs en ont besoin.
Et des classes sans constructeur par défaut sont fréquentes dans la plateforme. Exemple: System.Windows.Controls.UIElementCollection.

La solution dans UniversalSerializer est de rechercher les autres constructeurs, et de trouver une correspondance entre leurs paramètres et des champs de la classe, même s’ils sont privés.

Par exemple, dans UIElementCollection, nous trouvons ce constructeur:

public UIElementCollection( UIElement visualParent, FrameworkElement logicalParent )

Et ces champs sont disponibles dans la même classe:

  • private readonly UIElement _visualParent;
  • private readonly FrameworkElement _logicalParent;

Les types sont les mêmes et leur nom très proches. Assez pour que UniversalSerializer essaie de créer une instance avec ces valeurs.
Et ça marche !

Sérialiser des classes ICollection non génériques

Pour d’obscures raisons, l’interface ICollection ne fournit pas de méthodes Add et Insert.
En d’autres termes, elle définit une collection en lecture-seule, au contraire de l’interface générique ICollection<T>.
Normalement, nous ne pourrions pas désérialiser une classe qui implémente ICollection mais pas ICollection<T>.

Heureusement, dans le monde réel, la quasi-totalité de ces classes possèdent une méthode Add ou Insert, au moins pour un usage interne.
UniversalSerializer détecte ces méthodes et les utilise pour désérialiser une instance de classe collection.

Réutiliser les mécanismes existant et les gérer à l’aide de ITypeContainers

Dans certains cas, il est plus efficace ou seulement possible d’utiliser les mécanismes de transcodage existant.

Lorsque j’ai essayé de sérialiser des contrôles WPF, j’ai découvert que:

  • [TypeConverter] permet de transcoder un objet vers des types facilement sérialisables (string, byte[ ], etc.).
    Exemple: System.Windows.Input.Cursor
  • [ValueSerializer] permet de transcoder à partir et vers un string.
    Exemple: System.Windows.Media.FontFamily
  • [Serializable] (et ISerializable) permet d’utiliser BinaryFormatter.
    Exemple: System.Uri

Si vous examinez FontFamily, vous vous apercevrez que la transcoder vers un string est bien plus facile que d’essayer de sauvegarder ses propriétés.
Et plus sûr, car modifier une propriété peut entraîner des conséquences imprévisibles sur des classes inconnues ou complexes.

Pour ces attributs, j’ai créé le mécanisme de ITypeContainer. Un conteneur remplace l’instance source par sa valeur transcodée, en général un string ou un byte[ ].
Un conteneur peut s’appliquer à un ensemble de types, par exemple tous les types affublés d’un certain attribut.

Des exemples et des détails suivent.

Les Filtres

Certains types nécessitent une gestion particulière, qui peut être faite par un ensemble de filtres.

Le filtre de validation de type

Ce filtre permet d’éviter à UniversalSerializer de sérialiser des types problématiques.

Par exemple, j’ai rencontré certaines classes qui utilisent System.IntPtr .
Sérialiser ce type mène droit à des problèmes vu que ses instances sont uniquement utilisées en interne dans les classes, même lorsqu’elles sont stockées dans des propriétés publiques.

Les filtres d’ajout de champs privés

Ce filtre ordonne au sérialiseur d’ajouter certains champs privés aux informations de sérialisation.

Par exemple, System.Windows.Controls.Panel a besoin de _uiElementCollection pour remplir sa propriété Children, car Children est en lecture-seule.
Grâce à ce filtre, la solution est simple. Et tous les types héritant de Panel, comme StackPanel, bénéficient du même filtre.

ForcedParametricConstructorTypes

Ce n’est pas un filtre mais une liste de types.
Lorsqu’un type est dans cette liste, UniversalSerializer ignore son constructeur par défaut (sans paramètres) et recherche un constructeur paramétrique.
Exemple: System.Windows.Forms.PropertyManager . Il est ici bien plus simple d’utiliser son constructeur paramétrique que d’écrire un ITypeContainer pour ce type.

Les références

Examinons ce code:

{ // Two references to the same object.
  var data = new TextBox[2];
  data[0] = new TextBox() { Text = "TextBox1" };
  data[1] = data[0]; // Same reference
  byte[] bytesData = UniversalSerializer.Serialize(data);
  var data2 = UniversalSerializer.Deserialize<TextBox[]>(bytesData);
  data2[0].Text = "New text"; // Affects the two references.
  bool sameReference = object.ReferenceEquals(data2[0], data2[1]);
}
  1. UniversalSerializer sérialise une seule instance du TextBox.
  2. Il désérialise ensuite seulement une seule instance de TextBox, et deux références pointant vers elle.
    Les preuves: sameReference est vrai, et Text est identique dans les deux références.

Des filtres et des ITypeContainer personnalisés

Étant donné que l’objectif majeur de UniversalSerializer est de permettre de sérialiser n’importe quel type, il est essentiel que nous partagions notre expérience.

Par conséquent, j’ai essayé de rendre la création de conteneurs et de filtres aussi facile que possible, pour vous permettre de les essayer dans tous les sens et de partager ensuite vos solutions.

Créer un ITypeContainer

Le but est de remplacer une instance problématique par une instance facile à sérialiser. Le plus souvent, le conteneur contiendra des types très simples (string, int, byte[ ], etc..).

Prenons un exemple:

/// <summary>
/// . No default (no-param) constructor.
/// . The only constructor has a parameter with no corresponding field.
/// . The field ATextBox has no public 'set' and is different type from constructor's parameter.
/// </summary>
public class MyStrangeClassNeedsACustomerContainer
{
    /// <summary>
    /// It is built from the constructor's parameter.
    /// Since its 'set' method is not public, it will not be serialized directly.
    /// </summary>
    public TextBox ATextBox { get; private set; }
    public MyStrangeClassNeedsACustomerContainer(int NumberAsTitle)
    {
        this.ATextBox = new TextBox() { Text = NumberAsTitle.ToString() };
    }
}

Comme noté dans son résumé, cette classe pose quelques problèmes au(x) sérialiseur(s).

Pour régler ce problème, nous créons un conteneur:

class ContainerForMyStrangeClass : UniversalSerializerLib.ITypeContainer
{
#region Here you add data to be serialized in place of the class instance
public int AnInteger; // We store the smallest, sufficient and necessary data.
#endregion Here you add data to be serialized in place of the class instance

public UniversalSerializerLib.ITypeContainer CreateNewContainer(object ContainedObject)
{
MyStrangeClassNeedsACustomerContainer sourceInstance = ContainedObject as MyStrangeClassNeedsACustomerContainer;
return new ContainerForMyStrangeClass() { AnInteger = int.Parse(sourceInstance.ATextBox.Text) };
}
public object Deserialize()
{
return new MyStrangeClassNeedsACustomerContainer(this.AnInteger);
}
public bool IsValidType(Type type)
{
return Tools.TypeIs(type, typeof(MyStrangeClassNeedsACustomerContainer));
}
public bool ApplyEvenIfThereIsANoParamConstructor
{
get { return false; }
}
public bool ApplyToStructures
{
get { return false; }
}
}

Un détail: toutes les méthodes se comportent comme des méthodes statiques (mais sans l’être officiellement), à l’exception de Deserialize().

Voyons-les plus en détail:

  • public int AnInteger

    Ça ne fait pas partie de l’interface ITypeContainer.
    C’est ici que nous stockerons les informations qui serviront plus tard à la désérialisation.

  • ITypeContainer CreateNewContainer(object ContainedObject)

    Utilisé durant la sérialisation.
    C’est un genre de constructeur pour une instance de ce conteneur. Le paramètre sera l’instance de la classe source à sérialiser.

  • object Deserialize()

    Utilisé durant la désérialisation.
    L’instance du conteneur produira une nouvelle instance, une copie de l’instance de la classe source, en utilisant notre champ AnInteger.

  • bool IsValidType(Type type)

    Utilisé durant la sérialisation.
    Renvoie vrai si le type hérite, ou est, du type source.
    C’est un filtre.
    Nous pouvons choisir d’accepter les types hérités ou non, d’accepter des types compatibles, etc..

  • bool ApplyEvenIfThereIsANoParamConstructor

    Utilisé durant la sérialisation.
    Renvoie vrai si ce conteneur s’applique aux types de classes possédant un constructeur par défaut (sans paramètres).
    Peut être utile avec des conteneurs très généraux.

  • bool ApplyToStructures

    Utilisé durant la sérialisation.
    Renvoie vrai si ce conteneur s’applique aux types structure, et pas seulement aux types classe.
    Peut être utile avec des conteneurs très généraux.

Les étapes sont:

  1. Le sérialiseur teste si le type source (MyStrangeClassNeedsACustomerContainer) est géré par ce conteneur.
    Notre classe de conteneur (ContainerForMyStrangeClass) réponds que oui, via IsValidType(), ApplyEvenIfThereIsANoParamConstructor et ApplyToStructures.
  2. Le sérialiseur construit une instance de notre conteneur, via CreateNewContainer().
    CreateNewContainer construits une instance et modifie son champ AnInteger.
  3. Le sérialiseur stocke (sérialise) ce conteneur à la place de l’instance source.
  4. Le sérialiseur retrouve (désérialise) l’instance du conteneur.
  5. Le désérialiseur appelle Deserialize() et obtient une copie de l’instance de la classe source.
    Deserialize() crée cette copie en utilisant son champ AnInteger.

Plus qu’a sérialiser:

/* This example needs a custom ITypeContainer.
Normally, this class can not be serialized (see details in its source).
But thanks to this container, we can serialize the class as a small data (an integer).
*/
var Params = new UniversalSerializerLib.CustomParameters();
Params.Containers = new UniversalSerializerLib.ITypeContainer[] {
new ContainerForMyStrangeClass()
};
var data = new MyStrangeClassNeedsACustomerContainer(123);
byte[] bytesData = UniversalSerializer.Serialize(data, Params);
var data2 = UniversalSerializer.Deserialize<MyStrangeClassNeedsACustomerContainer>(bytesData, Params);
bool ok = data2.ATextBox.Text == "123";

Comme vous pouvez le voir, cette implémentation est très facile.

La classe statique Tools nous offre de l’aide:

  • Type Tools.TypeIs(Type ObjectType, Type SearchedType)

    C’est l’équivalent du ‘is’ en C#, mais pour les Types.
    Par exemple, TypeIs((typeof(List<int>), typeof(List<>)) renvoie true.

  • Type DerivedType(Type ObjectType, Type SearchedType)

    Renvoie le type correspondant à SearchedType qui est hérité par OjectType.
    Par exemple, DerivedType(typeof(MyList), typeof(List<>)) renvoie typeof(List<int>) lorsque MyList est un

    MyList: List<int> { }.

  • FieldInfo FieldInfoFromName(Type t, string name)

    Renvoie le FiedInfo du champ nommé de ce type.
    Nous l’utiliserons dans le prochain chapitre.

Créer un ensemble de filtres

Notez que le mécanisme des filtres est complètement indépendant des ITypeContainers.
On peut les utiliser ensemble, ou séparément.

Prenons un exemple:

public class ThisClassNeedsFilters
{
public ShouldNotBeSerialized Useless;
private int Integer;
public string Value { get { return this.Integer.ToString(); } }
public ThisClassNeedsFilters()
{
}
public ThisClassNeedsFilters(int a)
{
this.Integer = a;
this.Useless = new ShouldNotBeSerialized();
}
}
public class ShouldNotBeSerialized
{
}

Cette classe (ThisClassNeedsFilters) pose quelques problèmes:

  • Elle contient un ShouldNotBeSerialized.
    Nous supposerons que la classe ShouldNotBeSerialized doit être évitée pour certaines raisons, je ne sais pas pourquoi, peut-être qu’elle est empoisonnée !
  • Le champ Integer n’est pas public et donc est ignoré par le(s) sérialiseur(s).
  • Même le nom du paramètre du constructeur est différent d’aucun champ ou propriété.
    De toute façon, le sérialiseur n’a pas besoin du constructeur, puisqu’il y a un constructeur par défaut.

Pour dépasser ces problèmes, nous écrivons un ensemble personnalisé de filtres:

/// <summary>
/// Tells the serializer to add some certain private fields to store the type.
/// </summary>
FieldInfo[] MyAdditionalPrivateFieldsAdder(Type t)
{
if (Tools.TypeIs(t, typeof(ThisClassNeedsFilters)))
return new FieldInfo[] { Tools.FieldInfoFromName(t, "Integer") };
return null;
}
/// <summary>
/// Returns 'false' if this type should not be serialized at all.
/// That will let the default value created by the constructor of its container class/structure.
/// </summary>
bool MyTypeSerializationValidator(Type t)
{
return ! Tools.TypeIs(t, typeof(ShouldNotBeSerialized));
}

Ils parlent d’eux-même:

  • FieldInfo[] MyAdditionalPrivateFieldsAdder(Type t)

    dit au sérialiseur d’ajouter un champ privé (Integer) à chaque instance source de ce type (ThisClassNeedsFilters).

  • bool MyTypeSerializationValidator(Type t)

    empêche le sérialiseur de stocker toute instance de ce type (ShouldNotBeSerialized).
    En conséquence, tout instance de ThisClassNeedsFilters ne fixera pas son champ Useless (il sera null après la désérialisation).

Maintenant on sérialise la classe:

/* This example needs custom filters.
Normally, this class can be serialized but with wrong fields.
Thanks to these filters, we can serialize the class appropriately.
*/
var Params = new UniversalSerializerLib.CustomParameters();
Params.FilterSets = new FilterSet[] {
new FilterSet() {
AdditionalPrivateFieldsAdder=MyAdditionalPrivateFieldsAdder,
TypeSerializationValidator=MyTypeSerializationValidator } };
var data = new ThisClassNeedsFilters(123);
byte[] bytesData = UniversalSerializer.Serialize(data, Params);
var data2 = UniversalSerializer.Deserialize<ThisClassNeedsFilters>(bytesData, Params);
bool ok = data2.Value == "123" && data2.Useless == null;

L’implémentation est même plus facile qu’avec ITypeContainer.

Le code source de UniversalSerializer

Toutes les sources sont en C#.

La solution a été créée avec Visual Studio 2012, mais elle doit être compatible avec VS 2010.
La compilation nécessite seulement .Net 4, aucune DLL tierce, le code source est complet.

Il y a 3 DLLs:

  • UniversalSerializer1.dll
    La DLL principale générale, avec peu de dépendances.
  • UniversalSerializerWPF1.dll
    Une DLL spécialisée pour WPF, qui gère plus de types de WPF.
  • UniversalSerializerWinForm1.dll
    Une DLL spécialisée pour WinForms, qui gère plus de types de WinForms.

De cette façon, votre application n’aura pas de dépendances inutiles.

Dans l’archive, vous trouverez ces solutions pour VS:

  • \UniversalSerializer Tester\UniversalSerializer Tester.sln
    Une application WPF avec de nombreux exemples.
  • \UniversalSerializerWinForm\UniversalSerializerWinForm.sln
    Une application WinForm avec quelques exemples.
  • \UniversalSerializerReleaseLibs\UniversalSerializerReleaseLibs.sln
    Cette solution génère la version Release des trois DLLs.

Quelques points important

  • Le processus de sérialisation/désérialisation  identifie les types par leur nom complet (grâce à type.AssemblyQualifiedName).
    Ce genre de nom dépend de la version de l’assemblage (assembly), alors faites attention aux problèmes de versions pour la plateforme et pour les DLLs !

  • Le format produit actuel est sujet à changements dans le futur.
    C’est pour cela que le nom de la DLL porte un numéro de version.
    Si vous sauvegardez le tableau d’octets dans un fichier, je vous suggère d’ajouter une information de version à votre arbre d’objets.

Le futur

Je voudrais améliorer certains aspects:

  • Ajouter de nouveaux ITypeContainers et filtres.
    Vous pouvez m’aider. Prévenez-moi lorsque des Types ne sont pas sérialisés correctement. Et merci de partager votre solution, telle que des conteneurs et des filtres.
  • Réduire la taille des données produites.
    J’ai besoin de créer un nouveau format binaire.
  • Accélérer le traitement.

Faciliter le travail des sérialiseurs

Cette expérience m’a enseigné quelles difficultés rencontrent les sérialiseurs avec certains types et comment on peut leur faciliter le travail lorsque nous créons une classe.

  1. Écrivez un constructeur par défaut (sans paramètres) lorsque c’est possible.
    Le sérialiseur reconstruira l’instance à partir des champs et des propriétés.
  2. Si vous ne pouvez pas écrire un constructeur par défaut (sans paramètres), écrivez un constructeur paramétrique dont les paramètres correspondent à des champs privés.
    Chaque paramètre doit être du même type que le champ, et leurs noms doivent être semblables. En fait les noms peuvent différer légèrement: Param -> param ou _param ou encore _Param.
  3. Implémentez un ValueSerializerAttribute ou un TypeConverterAttribute lorsqu’une instance peut être construite à partir de quelque chose d’aussi simple qu’un string.
    En particulier lorsque votre classe contient de nombreux champs et propriétés d’optimisation publics.
    Par exemple, FontFamily peut être construit à partir d’un simple string; peu importe s’il contient bien d’autres informations, elles peuvent toutes être retrouvées à partir de ce simple string.
    Je ne sais pas si beaucoup de sérialiseurs prennent ces attributs en compte, mais au moins UniversalSerializer le fait.
  4. Tous les champs et les propriétés d’optimisation devraient être privés, lorsque c’est possible. Voir plus haut.
  5. Lorsque vous créez une collection d’objets, implémentez IList.
      Parce que ICollection ne permet pas d’ajouter des éléments à une collection.
    Lorsque vous créez une collection générique, implémentez ICollection<>.
      Parce que IEnumerable<> ne permet pas d’ajouter des éléments à une collection.

Remerciements

Je remercie Mehdi Gholam pour son fastBinaryJSON.
J’ai appris des choses intéressantes en lisant son code.
Merci pour avoir partagé cela.

 
Poster un commentaire

Publié par le 2013-07-13 dans .Net, Programmation

 

Étiquettes : , , ,

Différence en C# entre new et override lors du remplacement d’une fonction à hériter

Au départ, un débutant en C# se demande quelle peut bien être la différence entre new et override, et lequel choisir lorsqu’il souhaite remplacer une méthode dans une classe héritante.

Vocabulaire utilisé ici

  • Classe à hériter = classe de base.
  • Classe héritante = classe dérivée (elle hérite de la classe de base).

En une phrase

Avec override (et donc virtual), le code de la classe à hériter (la classe de base) appellera soit sa propre méthode virtual, soit la méthode d’une classe héritante (dérivée de cette classe de base) si celle-ci la remplace avec override.

Exemple montrant la différence

Le code:

public class ClasseÀHériter
{
    public string FonctionNormale()
    {
        return "FonctionNormale dans la classe de base";
    }
    public virtual string FonctionExplicitementRemplaçable()
    {
        return "FonctionExplicitementRemplaçable dans la classe de base ";
    }
    public string Résultat()
    {
        return
            this.FonctionNormale()  // Appelle toujours la fonction définie dans cette classe.
            + "\n"
            + this.FonctionExplicitementRemplaçable(); // La fonction appelée a pu être remplacée dans une classe héritante.
    }
}

public class ClasseHéritante : ClasseÀHériter
{
    public new string FonctionNormale()
    {
        return "FonctionNormale dans la classe dérivée";
    }
    public override string FonctionExplicitementRemplaçable()
    {
        return "FonctionExplicitementRemplaçable dans la classe dérivée";
    }
    public string AutreRésultat()
    {
        return
            this.FonctionNormale()
            + "\n"
            + this.FonctionExplicitementRemplaçable();
    }
}

public MainWindow()
{
    InitializeComponent();

    // ------------------

    ClasseÀHériter càh = new ClasseÀHériter();
    string scàh = càh.Résultat();

    ClasseHéritante ch = new ClasseHéritante();

    string schr = ch.Résultat();
    string scàhar = ch.AutreRésultat();
}

Résultats:

Classe utilisée Méthode appelée Texte renvoyé par FonctionNormale() Texte renvoyé par FonctionExplicitementRemplaçable()
ClasseÀHériter Résultat() de ClasseÀHériter FonctionNormale dans la classe de base FonctionExplicitementRemplaçable dans la classe de base
ClasseHéritante Résultat() de ClasseÀHériter FonctionNormale dans la classe de base FonctionExplicitementRemplaçable dans la classe dérivée
ClasseHéritante AutreRésultat() de ClasseHéritante FonctionNormale dans la classe dérivée FonctionExplicitementRemplaçable dans la classe dérivée

Interprétation:

  • Lorsqu’on instancie ClasseÀHériter, elle utilise ses propres fonctions.
  • Lorsqu’on hérite de ClasseÀHériter dans ClasseHéritante, la méthode FonctionNormale() appartenant à ClasseÀHériter utilise la méthode FonctionExplicitementRemplaçable() remplacée par ClasseHéritante avec override.
  • Lorsqu’on hérite de ClasseÀHériter (dans ClasseHéritante), la méthode AutreRésultat() de ClasseHéritante utilise les deux méthodes remplacées (avec new et avec override).

Aspect technique

Alors que new se contente de définir une méthode dont le nom remplace celui de la classe à hériter, override redirige les appels à la méthode remplacée même pour le code de la classe de base à hériter.
Cela implique de placer dans la partie statique de la classe à hériter, et dans toutes ses dérivées, un pointeur (un délégué, si vous voulez) qui permet au code de la classe à hériter de trouver un éventuel remplacement (par override) dans les classes héritantes.

Simulation de la virtualisation

Pour comprendre le mécanisme, voici une sorte d’écriture manuelle d’une méthode virtualisée dans une classe:

public class SimulationDeVirtualisation
{
    public delegate string ModèleDeLaMéthodeÀVirtualiser();

    public ModèleDeLaMéthodeÀVirtualiser RedirecteurVersLaMéthodeÀUtiliser; // En fait devrait être statique.

    public SimulationDeVirtualisation() // Constructeur:
    {
        RedirecteurVersLaMéthodeÀUtiliser = MéthodeDebaseRemplaçable;
    }

    private string MéthodeDebaseRemplaçable()
    {
        return "Méthode dans la classe de base";
    }

    public string DitSiMéthodeRemplacée()
    {
        return RedirecteurVersLaMéthodeÀUtiliser();
    }
}

public class ClasseHéritanteAvecMéthodeVirtualisée : SimulationDeVirtualisation
{
    public ClasseHéritanteAvecMéthodeVirtualisée() // Constructeur:
    {
        RedirecteurVersLaMéthodeÀUtiliser = MéthodeDebaseRemplaçante;
    }

    static private string MéthodeDebaseRemplaçante()
    {
        return "Méthode dans la classe dérivée";
    }
}
   
public MainWindow()
{
    InitializeComponent();

    // ------------------

    SimulationDeVirtualisation sv = new SimulationDeVirtualisation();
    string tsv = sv.DitSiMéthodeRemplacée();    // Renvoie "Méthode dans la classe de base".

    ClasseHéritanteAvecMéthodeVirtualisée chamv = new ClasseHéritanteAvecMéthodeVirtualisée();
    string tchamv = chamv.DitSiMéthodeRemplacée(); // Renvoie "Méthode dans la classe dérivée".
}

Je n’ai pas pu implémenter aussi parfaitement que je l’aurai voulu, à cause de limitations de C#, mais on voit bien que la classe de base utilise soit sa propre fonction virtuelle, soit la fonction remplaçante implémentée dans la classe dérivée.

À noter que RedirecteurVersLaMéthodeÀUtiliser devrait être statique, aussi bien dans la classe de base que dans chaque dérivation de cette classe, et que dans ce cas il est inutile de l’initialiser dans le constructeur de chaque classe.

Bien entendu, le compilateur C# s’occupe de ces détails à notre place lorsqu’on utilise les mots-clés virtual et override.
Mais il est bon de savoir ce qu’implique cette utilisation, en terme de ressources et de performances.

Conclusion(s)

  • Virtual vous est utile lorsque vous écrivez une classe à hériter (une classe de base), et que vous voulez y utiliser des informations qui n’existeront que dans les classes qui en hériteront.
  • L’utilisation de Virtual entraîne l’ajout d’un pointeur caché (au sens du C) dans la partie statique des classes à hériter et des classes héritantes, et une redirection dans le code de la classe à hériter. Soit un peu plus de complication.
    Mieux vaut donc éviter de créer des classes virtuelles et leur préférer des remplacements par new lorsque c’est possible. Surtout pour des petites classes souvent instanciées.
  •  
Poster un commentaire

Publié par le 2012-06-24 dans Programmation

 

Étiquettes : , , , , , , , , , ,