Fermer

août 17, 2022

Assomption difficile avec les fonctionnalités linguistiques

Assomption difficile avec les fonctionnalités linguistiques


Découvrez comment l’évolution d’un langage force le changement dans sa compréhension et son utilisation, et comprenez une nouvelle fonctionnalité C# en préparation : les membres abstraits statiques dans les interfaces.

Le concept et le but d’une interface en C # sont historiquement compris. Cependant, les nouvelles fonctionnalités C#, à la fois livrées et en cours, obligent les développeurs à repenser ce qu’est une interface et à réévaluer les hypothèses existantes. Dans cet article, j’expliquerai ce qu’est une interface, quelles sont ces nouvelles fonctionnalités et où les interfaces (et le système de type en général) peuvent aller à l’avenir.

Introduction

Les langages de programmation sont essentiels au développement de logiciels. Ils permettent à un développeur d’exprimer son intention d’une manière plus simple que d’écrire des applications à l’aide de constructions de niveau inférieur comme le code d’assemblage (ou même le code machine). Il est souhaitable qu’un développeur comprenne comment fonctionnent les fonctionnalités du langage de programmation de son choix. Cette connaissance permet ensuite d’écrire des applications efficaces, évolutives et maintenables.

Inévitablement, les langages de programmation évoluent avec le temps. De nouvelles fonctionnalités peuvent être ajoutées pour faciliter les tâches ou réduire la quantité de code nécessaire dans certaines situations. Par exemple, en C# (un langage que j’utilise depuis plus de 20 ans), le Listing 1 montre comment le « hello world » canonique a été fait pendant de nombreuses années :

Liste 1 : « Hello World » traditionnel en C#

using System;

public static class Program
{
  public static void Main(string[] args)
  {
      Console.WriteLine("Hello world!");   
  }
}

Au fil des ans, des fonctionnalités telles que déclarations de niveau supérieur et instructions globales à l’aide permettre à un développeur de réduire l’implémentation à une seule ligne de code, comme illustré dans le Listing 2 :

Liste 2 : « Hello World » moderne en C#

Console.WriteLine("Hello world!");

Il y a une tension entre la stabilité d’une langue et son évolution dans le temps. Les changements peuvent apporter des avantages, mais ils peuvent également semer la confusion, surtout si ces changements affectent un concept de base qui est en place depuis longtemps. Parfois, il peut même y avoir des modifications avec rupture, lorsqu’une fonctionnalité n’est plus prise en charge ou qu’elle fonctionne d’une manière qui entraînera des échecs de compilation. Traditionnellement, les langages de programmation ont tendance à éviter les changements de rupture, car ils peuvent rendre difficile la mise à niveau du code écrit pour une version antérieure vers la dernière version. Mais, même si une nouvelle fonctionnalité ne casse pas le code, elle peut entraîner des malentendus et des incertitudes.

Dans cet article, je parlerai des interfaces en C#. Je commencerai par expliquer ce qu’ils sont et comment ils sont définis. Ensuite, je vais montrer comment ils ont changé en C # 8 avec les membres d’interface par défaut. Enfin, je montrerai comment la nouvelle fonctionnalité des membres abstraits statiques dans les interfaces (qui arrive dans C# 11) peut potentiellement changer la façon dont les développeurs définissent et interagissent avec les interfaces.

Expliquer les fonctionnalités dans les interfaces

Commençons notre discussion sur les interfaces avec les fonctionnalités de base qui ont été mises en place depuis la première version de C#.

Comment fonctionnent généralement les interfaces

Les interfaces sont principalement utilisées pour définir un contrat, spécifiant ce qui doit être fait mais pas comment cela doit être accompli. Par exemple, le Listing 3 définit une interface appelée IDriveravec une méthode, Drive().

Listing 3 : Définition de l’interface IDriver

public interface IDriver
{
  void Drive();
}

UN class ou struct implémentera l’interface comme bon lui semble. Le Listing 4 montre deux implémentations différentes de IDriverun pour un golfeur et un pour un pilote de course :

Listing 4 : Implémentation de l’interface IDriver

public sealed class Golfer
  : IDriver
{
  public void Drive()
  {
    var value = RandomNumberGenerator.GetInt32(250, 320);
    Console.WriteLine($"{value} yards");
  }
}

public sealed class Racer
  : IDriver
{
  public void Drive()
  {
    var value = RandomNumberGenerator.GetInt32(120, 220);
    Console.WriteLine($"{value} MPH");
  }
}

Comme vous pouvez le voir, les implémentations entre ces deux sont différentes. La « conduite » pour un golfeur (qui est mesurée en yards) fait quelque chose qui n’a aucun rapport avec ce que ferait un pilote de voiture de course (qui est mesuré en MPH). Cependant, les deux implémentent la même interface, ils sont donc utilisés de la même manière. Par exemple, le Listing 5 montre une méthode qui prend une liste d’objets qui implémentent IDriver et dites-leur à tous de conduire :

Listing 5 : Conduite d’objets basés sur IDriver

public static void Drive(IEnumerable<IDriver> drivers)
{
  foreach(var driver in drivers)
  {
    driver.Drive();
  }
}

Drive(new IDriver[] { new Golfer(), new Racer() });

Si vous exécutiez ce code, vous verriez quelque chose comme ceci dans la fenêtre de la console :

Conduire 268 mètres, 215 mph

Comment chaque objet « drives » est sans importance pour la mise en œuvre de cette méthode. Ce code ne s’intéresse qu’aux objets implémentant le IDriver Contrat.

Il existe d’autres détails liés à une interface, tels que implémentation d’interface explicite, mais la principale conclusion est que les interfaces ont toujours consisté à définir le contrat entre un utilisateur et un implémenteur. Les membres de l’interface n’avaient pas d’implémentations.

Fournir des implémentations par défaut

En C# 8, le paysage de l’interface a considérablement changé. Une modification était que la mise en œuvre des membres pouvait avoir lieu dans une interface. Cette fonction s’appelle membres d’interface par défaut, ou DIM. La principale motivation des DIM est que les interfaces peuvent être mises à jour sans interrompre les classes qui les implémentent. Par exemple, dans le Listing 6, le IDriver l’interface est mise à jour pour avoir un Stop() méthode:

Listing 6 : Mettre à jour une interface

public interface IDriver
{
  void Drive();

  void Stop();
}

Si une classe implémente IDriveril doit maintenant implémenter à la fois Drive() et Stop(). Cela oblige les développeurs à comprendre ce qu’est un Stop() la mise en œuvre ferait dans leur application. Ce n’est pas facultatif pour un développeur. Ils doivent ajouter une implémentation pour
Stop(). Vous ne pouvez pas avoir une classe qui n’implémente pas de membres abstraits.

Cependant, avec un DIM, vous pouvez versionner l’interface et ne pas casser les implémentations existantes. Ceci est montré dans le Listing 7 :

Listing 7 : Ajouter une implémentation à un membre d’interface

public interface IDriver
{
  void Drive();

  void Stop() { }
}

Dans ce cas, Stop() ne fera rien sauf autoriser existant IDriver implémentations au travail. Autrement dit, un développeur n’a pas à mettre à jour chaque définition de classe avec un Stop() mise en œuvre lorsqu’ils obtiennent la nouvelle version de IDriver.

Un autre changement apporté aux interfaces permettait aux membres statiques d’être définis sur l’interface. Nous pourrions reprendre cette méthode de la section précédente qui énumère IDriver objets et placez-les dans IDriver interface elle-même en tant que membre statique, comme indiqué dans le Listing 8 :

Listing 8 : Ajouter des membres statiques à une interface

public interface IDriver
{
  public static void Drive(IEnumerable<IDriver> drivers)
  {
    foreach(var driver in drivers)
    {
      driver.Drive();
    }
  }
    
  void Drive();

  void Stop() { }
}

Bien que j’aie personnellement une réponse positive à ces changements, ils ont provoqué un certain émoi chez certains développeurs C #, en particulier ceux qui utilisaient le langage depuis longtemps. D’un point de vue puriste, les membres d’interface n’ont pas d’implémentations, et ils ne devraient pas non plus avoir de membres statiques, car leurs membres ont toujours été implémentés pour les instances d’un type. Bien qu’il y ait eu des arguments rationnels pour l’ajout de ces fonctionnalités de la part de l’équipe C #, pour certaines personnes, cela ressemblait à une violation de ce qu’une interface devrait (ou ne devrait pas) prendre en charge. En fin de compte, qu’un développeur ait apprécié les mises à jour de l’interface, il était clair que les aspects fondamentaux du langage étaient sujets à modification.

Définition des membres abstraits statiques (y compris les opérateurs)

C# 11 est actuellement en mode aperçu, sa sortie étant prévue pour novembre 2022. L’une des nombreuses fonctionnalités de cette version s’appelle membres abstraits statiques dans les interfaces. Cela permet à un développeur de définir un membre statique dans une interface qui doit être implémentée dans une classe.

Remarque : Si vous souhaitez essayer des membres abstraits statiques dans les interfaces, ajoutez les valeurs de propriété suivantes à votre fichier de projet C# :

  • <EnablePreviewFeatures>true</EnablePreviewFeatures>
  • <LangVersion>preview</LangVersion>

Par exemple, si nous revenons au IDriver scénario, créons une toute nouvelle interface : IModernDriver. Ceci est montré dans le Listing 9 :

Listing 9 : Ajout d’un membre abstrait statique à IDriver

public interface IModernDriver<TSelf>
  where TSelf : IModernDriver<TSelf>
{
  static abstract void Drive(IEnumerable<TSelf> modernDrivers);

  void Drive();
  
  void Stop() { }
}

Notez que Drive() est statique et abstrait. Cela signifie que la classe qui l’implémente doit avoir une implémentation pour cette méthode, comme démontré dans le Listing 10 :

Listing 10 : Fournir une implémentation pour un membre abstrait statique

public class ModernGolfer
  : IModernDriver<ModernGolfer>
{
  public static void Drive(IEnumerable<ModernGolfer> modernGolfers)
  {
    foreach(var modernGolfer in modernGolfers)
    {
      modernGolfer.Drive();
    }
  }

  public void Drive()
  {
    var value = RandomNumberGenerator.GetInt32(100, 150);
    Console.WriteLine($"Club head speed: {value} MPH");    
  }

  public void Stop()
  {
    var value = RandomNumberGenerator.GetInt32(250, 320);
    Console.WriteLine($"{value} yards");    
  }
}

La contrainte générique sur TSelf rend le type sur lequel l’énumérable va itérer identique au type qui implémente l’interface.

Vous pouvez définir des propriétés statiques et abstraites, et vous pouvez également définir des opérateurs sur une interface si vous le souhaitez. Cela permet à C# de gérer des types de données primitifs tels que int et double de façon générique. Je recommande fortement la lecture Cet article pour plus d’informations sur l’utilisation des opérateurs dans les interfaces.

Hypothèses et ramifications

Comme l’a montré la section précédente, les interfaces ont acquis des capacités substantielles ces dernières années. Ils facilitent la gestion des versions et fournissent des techniques pour utiliser les types de manière entièrement nouvelle. Cependant, ils peuvent également défier les développeurs de manière intéressante.

Par exemple, en C#, il est possible d’appeler l’implémentation d’un membre dans une classe de base. Ceci est démontré dans le Listing 11 :

Listing 11 : Appeler un membre de base

public abstract class Driver
{
  protected Driver() { }

  public abstract void Drive();

  public virtual void Stop() { }
}

public sealed class GolferDriver
  : Driver
{
  public override void Drive()
  {
    var value = RandomNumberGenerator.GetInt32(100, 150);
    Console.WriteLine($"Club head speed: {value} MPH");
  }

  public override void Stop()
  {
    base.Stop();

    var value = RandomNumberGenerator.GetInt32(250, 320);
    Console.WriteLine($"{value} yards");
  }
}

Maintenant notre type de base Driver n’est plus une interface. Au lieu de cela, c’est une classe abstraite. Les classes qui dérivent de Driver encore faut-il fournir une implémentation pour Drive()mais Stop() a déjà une implémentation. Cependant, le Golfer La classe peut décider d’appeler l’implémentation de base si elle le souhaite.

Il peut sembler raisonnable qu’il y ait un moyen de le faire avec les DIM. En effet, selon le «Appels d’interface de base » section de La documentationun développeur pourrait penser qu’il pourrait appeler Stop() sur le IDriver interface comme indiqué dans le Listing 12 :

Listing 12 : Essayer d’appeler un DIM

public sealed class Golfer
  : IDriver
{
  public void Drive()
  {
    var value = RandomNumberGenerator.GetInt32(250, 320);
    Console.WriteLine($"{value} yards");
  }

  public override void Stop()
  {
    
    IDriver.base.Stop();

    var value = RandomNumberGenerator.GetInt32(250, 320);
    Console.WriteLine($"{value} yards");
  }
}

Malheureusement, cela ne fonctionne pas. Cette fonctionnalité était une proposition mais n’a pas été implémentée à partir de C # 10, et il n’est pas clair si elle sera ajoutée au langage à l’avenir. Cela m’a fait perdre du temps sur un projet OSS personnel sur lequel j’ai travaillé appelé Rochers. J’essayais de comprendre s’il y avait un moyen d’appeler une implémentation de base à partir d’une interface, et quand j’ai lu la documentation liée ci-dessus, j’ai pensé que ce serait simple à ajouter. Étant donné que cette fonctionnalité n’est pas en C #, j’ai dû essayer de trouver un autre moyen. Heureusement, cela s’est avéré possible, mais c’est un peu compliqué. Lis ce problème GitHub pour voir ce qu’il fallait pour que ça marche.

Un autre exemple est celui des générateurs de code. Disons que vous avez construit un outil qui génère une classe basée sur la définition d’une interface. Il y en a un dans Visual Studio (VS) qui le fait déjà, comme illustré à la figure 1 :

Figure 1 : Implémentation d’une interface à l’aide d’un outil de refactorisation de Visual Studio

Affiche la note : 'Golfer n'implémente pas le membre d'interface 'IDriver.Drive()'.  Le code montre... public void Drive()...

Pour C# 11, des outils comme celui-ci doivent considérer que les membres d’une interface peuvent être statiques et doivent avoir une implémentation. Heureusement, comme vous pouvez le voir sur la capture d’écran de la figure 2, la version d’aperçu de VS 2022 a déjà mis à jour le Interface d’outil refactoring pour gérer cette nouvelle fonctionnalité :

Figure 2 : Outil de refactorisation Visual Studio mis à jour pour les membres abstraits statiques

Le code contient maintenant 'static' : ...public static void Drive()...

Si vous avez des outils qui parcourent la structure d’une interface, vous devrez peut-être revoir leur fonctionnement. Supposez-vous que chaque membre d’une interface sera un membre d’instance ? Vous recherchez des membres statiques ? Sinon, lorsque vous passez à C# 11, ces outils peuvent générer du code non valide qui ne se compilera pas. Prenez le temps d’examiner les outils existants et leurs comportements afin d’être prêt lorsqu’ils seront exécutés sur des bases de code C# modernes.

Conclusion

Le changement est inévitable dans la vie, même dans les langages de programmation. Un développeur de logiciels doit se tenir au courant de l’évolution d’un langage. Ces ajouts peuvent faciliter la réalisation de certains scénarios de développement et provoquer des comportements inattendus basés sur des hypothèses bien fondées. Lorsqu’une nouvelle version s’accompagne d’une langue que vous utilisez, je vous recommande fortement d’examiner toutes les modifications associées à cette nouvelle version, même si vous n’avez pas l’intention de mettre à niveau dans un avenir immédiat.

Je crois aussi qu’il faut regarder la feuille de route d’une langue pour voir ce qui pourrait arriver. Par exemple, il y a une longue discussion sur une idée appelée « Rôles, interfaces d’extension et membres d’interface statique” pour C#. Cela aurait un impact substantiel sur la compréhension d’un développeur du système de type – en fait, la troisième partie, les membres de l’interface statique, a déjà été livrée. Je vous encourage à parcourir le contenu de la Discussions C# page, car vous ne savez jamais quand une fonctionnalité fera son chemin dans la langue !

Le code de cet article se trouve dans le référentiel suivant : https://github.com/JasonBock/ChallengingAssumptionsWithLanguageFeatures.




Source link