Ce premier article de la série donne un aperçu général des piliers et du point de conception de la plate-forme. Il décrit "ce que vous obtenez" à un niveau fondamental lorsque vous choisissez .NET et est destiné à être un cadre suffisant et axé sur les faits que vous pouvez utiliser pour décrire la plate-forme à d'autres. Des articles ultérieurs traiteront plus en détail de ces mêmes sujets, car cet article ne rend pas justice à toutes ces caractéristiques. Cet article ne décrit pas les outils, comme Visual Studio, ni les bibliothèques de niveau supérieur et les modèles d'application comme ceux fournis par ASP.NET Core.
Le " .NET " dont nous parlons est le .NET Core moderne. Au cas où vous l'auriez manqué, nous avons lancé ce projet en 2014 en tant que projet open source sur GitHub. Il fonctionne sous Linux, macOS et Windows sur Arm64, x64 et d'autres architectures de puces. Il est disponible dans un tas de distros Linux. Il conserve une grande partie de la compatibilité avec .NET Framework, mais constitue une nouvelle direction et un nouveau produit.
Avant d'entrer dans les détails, il convient de parler de l'utilisation de .NET. Il est utilisé par des millions de développeurs pour créer des applications cloud, client et autres sur de multiples systèmes d'exploitation et architectures de puces. Il est également exécuté dans certains endroits bien connus, comme Azure, StackOverflow et Unity. Il est courant de trouver .NET utilisé dans des entreprises de toutes tailles, mais particulièrement dans les plus grandes. Dans de nombreux endroits, c'est une bonne technologie à connaître pour obtenir un emploi.
Voir la réaction de la communauté à cet article : Réflexions sur "Qu'est-ce que .NET et pourquoi le choisir ?
Point de conception .NET
La plate-forme .NET est synonyme de productivité, de performance, de sécurité et de fiabilité. L'équilibre que .NET établit entre ces valeurs est ce qui le rend attrayant.
Nous pouvons nous étendre un peu plus sur le point de la conception :
- La productivité est le principal critère de conception pour l'exécution, les bibliothèques, le langage et les outils.
- Le code sûr est le principal modèle de calcul, tandis que le code non sûr permet des optimisations manuelles supplémentaires
- Le code statique et le code dynamique sont tous deux pris en charge, ce qui permet un large éventail de scénarios distincts.
- L'interopérabilité du code natif et les éléments matériels intrinsèques sont peu coûteux et de haute fidélité (accès brut à l'API et aux instructions).
- Le code est portable sur toutes les plates-formes (système d'exploitation, architecture de puce), tandis que le ciblage de la plate-forme permet la spécialisation et l'optimisation.
- L'adaptabilité entre les domaines de programmation (cloud, client, jeux) est rendue possible par des implémentations spécialisées du modèle de programmation universel.
- Les normes industrielles telles que OpenTelemetry et gRPC sont préférées aux solutions sur mesure.
Le moteur d'exécution, les bibliothèques et les langages sont les piliers de la pile .NET. Les composants de niveau supérieur, tels que les outils .NET et les piles d'applications comme ASP.NET Core, reposent sur ces piliers. Les piliers ont une relation symbiotique, ayant été conçus et construits ensemble par un seul groupe (les employés de Microsoft et la communauté open source), où les individus travaillent sur et informent plusieurs de ces composants.
C# est orienté objet et le runtime supporte l'orientation objet. C# nécessite un ramasse-miettes et le runtime fournit un ramasse-miettes de traçage. En fait, il serait impossible de porter C# (dans sa forme complète) sur un système sans ramasse-miettes. Les bibliothèques (et aussi les piles d'applications) transforment ces capacités en concepts et modèles d'objets qui permettent aux développeurs d'écrire des algorithmes de manière productive dans des flux de travail intuitifs.
C# est un langage de programmation moderne, sûr et polyvalent, qui s'étend des fonctionnalités de haut niveau telles que les enregistrements orientés données aux fonctionnalités de bas niveau telles que les pointeurs de fonction. Il offre le typage statique et la sécurité des types et de la mémoire comme capacités de base, ce qui améliore simultanément la productivité des développeurs et la sécurité du code. Le compilateur C# est également extensible et prend en charge un modèle de plug-in qui permet aux développeurs de compléter le système par des diagnostics supplémentaires et la génération de code à la compilation.
Un certain nombre de fonctionnalités de C# ont influencé ou ont été influencées par des langages de programmation de pointe. Par exemple, C# a été le premier langage grand public à introduire async et await. En même temps, C# emprunte des concepts introduits pour la première fois dans d'autres langages de programmation, par exemple en adoptant des approches fonctionnelles telles que le filtrage et les constructeurs primaires.
Les bibliothèques de base exposent des milliers de types, dont beaucoup s'intègrent au langage C# et l'alimentent. Par exemple, le foreach de C# permet d'énumérer des collections arbitraires, avec des optimisations basées sur des motifs qui permettent de traiter simplement et efficacement des collections comme List<T>. La gestion des ressources peut être laissée au ramasse-miettes, mais un nettoyage rapide est possible grâce à IDisposable et au support direct du langage dans using.
L'interpolation de chaînes en C# est à la fois expressive et efficace, intégrée et alimentée par des implémentations dans les types de base de la bibliothèque comme string, StringBuilder et Span<T>. Enfin, les fonctionnalités de requêtes intégrées au langage (LINQ) sont alimentées par des centaines de routines de traitement de séquences dans les bibliothèques, comme Where, Select et GroupBy, avec une conception extensible et des implémentations qui prennent en charge les sources de données en mémoire et distantes. La liste est longue et ce qui est intégré directement dans le langage ne fait qu'effleurer la surface des fonctionnalités exposées dans le cadre des bibliothèques .NET de base, de la compression à la cryptographie en passant par les expressions régulières. Une pile réseau complète constitue un domaine à part entière, allant des sockets à HTTP/3. De même, les bibliothèques prennent en charge le traitement d'une myriade de formats et de langages tels que JSON, XML et tar.
Le moteur d'exécution .NET était initialement appelé "Common Language Runtime (CLR)". Il continue à prendre en charge de nombreux langages, certains étant maintenus par Microsoft (par exemple C#, F#, Visual Basic, C++/CLI et PowerShell) et d'autres par d'autres organisations (par exemple Cobol, Java, PHP, Python, Scheme). De nombreuses améliorations sont agnostiques du point de vue du langage, ce qui permet d'améliorer la situation.
Ensuite, nous allons examiner les diverses caractéristiques de la plate-forme qu'ils offrent ensemble. Nous pourrions détailler chacun de ces composants séparément, mais vous verrez rapidement qu'ils coopèrent pour répondre aux critères de conception de .NET. Commençons par le système de types.
Système de types
Le système de types de .NET offre un large éventail de possibilités, répondant à peu près également aux besoins de sécurité, de descriptivité, de dynamisme et d'interopérabilité native.
Avant tout, le système de types permet un modèle de programmation orienté objet. Il inclut les types, l'héritage (classe de base unique), les interfaces (y compris les implémentations de méthodes par défaut) et la répartition des méthodes virtuelles afin de fournir un comportement raisonnable pour toutes les couches de types que l'orientation objet permet.
Les génériques sont une fonctionnalité omniprésente qui permet de spécialiser les classes en un ou plusieurs types. Par exemple, List<T> est une classe générique ouverte, tandis que les instanciations comme List<string> et List<int> évitent d'avoir à créer des classes distinctes ListOfString et ListOfInt ou de s'appuyer sur des objets et des castings comme c'était le cas avec ArrayList. Les génériques permettent également de créer des systèmes utiles à travers des types disparates (et de réduire le besoin de beaucoup de code), comme avec Generic Math.
Les délégués et les lambdas permettent de passer des méthodes en tant que données, ce qui facilite l'intégration de code externe dans un flux d'opérations appartenant à un autre système. Ils constituent une sorte de "code colle" et leurs signatures sont souvent génériques pour permettre une large utilisation.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 | app.MapGet("/Product/{id}", async (int id) => { if (await IsProductIdValid(id)) { return await GetProductDetails(id); } return Products.InvalidProduct; }); |
Les types de valeurs et les blocs de mémoire alloués par pile offrent un contrôle plus direct et de bas niveau sur les données et une interopérabilité avec la plate-forme native, contrairement aux types gérés par GC de .NET. La plupart des types primitifs de .NET, comme les types entiers, sont des types de valeur, et les utilisateurs peuvent définir leurs propres types avec une sémantique similaire.
Les types de valeur sont entièrement pris en charge par le système générique de .NET, ce qui signifie que les types génériques comme List<T> peuvent fournir des représentations mémoire plates, sans frais généraux, des collections de type valeur. En outre, les génériques .NET fournissent un code compilé spécialisé lorsque les types de valeur sont substitués, ce qui signifie que ces chemins de code génériques peuvent éviter une surcharge GC coûteuse.
Code : | Sélectionner tout |
1 2 3 | byte magicSequence = 0b1000_0001; Span<byte> data = stackalloc byte[128]; DuplicateSequence(data[0..4], magicSequence); |
Les types et variables Ref sont une sorte de mini-modèle de programmation qui offre des abstractions de plus bas niveau et plus légères sur les données du système de types. Cela inclut Span<T>. Ce modèle de programmation n'est pas d'usage général et comporte des restrictions importantes pour maintenir la sécurité.
Code : | Sélectionner tout |
internal readonly ref T _reference;
Gestion automatique de la mémoire
Le moteur d'exécution .NET assure une gestion automatique de la mémoire par le biais d'un ramasse-miettes (Garbage Collector). Pour tout langage, le modèle de gestion de la mémoire est probablement sa caractéristique la plus déterminante. Ceci est vrai pour les langages .NET.
Les bogues de corruption de tas sont notoirement difficiles à déboguer. Il n'est pas rare que les ingénieurs passent des semaines, voire des mois, à les rechercher. De nombreux langages utilisent un ramasse-miettes comme moyen convivial d'éliminer ces bogues, car le GC garantit la durée de vie correcte des objets. En général, les GC libèrent la mémoire par lots pour fonctionner efficacement. Cela entraîne des pauses qui peuvent ne pas convenir si vous avez des exigences de latence très strictes, et l'utilisation de la mémoire serait plus élevée. Les GC ont tendance à avoir une meilleure localisation de la mémoire et certains sont capables de compacter le tas, ce qui le rend moins sujet à la fragmentation de la mémoire.
.NET dispose d'une GC auto-réglée et traçante. Il vise à fournir un fonctionnement "sans intervention" dans le cas général tout en offrant des options de configuration pour les charges de travail plus extrêmes. Le GC est le résultat de nombreuses années d'investissement, d'amélioration et d'apprentissage à partir de nombreux types de charges de travail.
Allocation par pointeur de bosse - les objets sont alloués en incrémentant un pointeur d'allocation de la taille nécessaire (au lieu de trouver de l'espace dans des blocs libres séparés), de sorte que les objets alloués ensemble ont tendance à rester ensemble. Et comme les objets sont souvent accédés ensemble, cela permet une meilleure localisation de la mémoire, ce qui est important pour les performances.
Collections générationnelles - il est extrêmement fréquent que la durée de vie des objets suive l'hypothèse générationnelle, c'est-à-dire qu'un objet vit très longtemps ou meurt très rapidement. Il est donc beaucoup plus efficace pour un GC de ne collecter que la mémoire occupée par des objets éphémères la plupart du temps (appelé GC éphémères), au lieu de devoir collecter tout le tas (appelé GC complets) à chaque fois qu'il s'exécute.
Compactage - la même quantité d'espace libre dans des morceaux plus grands et moins nombreux est plus utile que dans des morceaux plus petits et plus nombreux. Au cours d'une GC de compactage, les objets survivants sont déplacés ensemble afin de former de plus grands espaces libres. Ce comportement nécessite une mise en œuvre plus sophistiquée qu'un GC sans déplacement car il doit mettre à jour les références à ces objets déplacés. Le GC .NET est réglé de manière dynamique pour effectuer le compactage uniquement lorsqu'il détermine que la mémoire récupérée vaut le coût du GC. Cela signifie que les collections éphémères sont souvent compactées.
Parallèle - Le travail GC peut être exécuté sur un seul ou plusieurs threads. La version Workstation effectue le travail GC sur un seul fil d'exécution, tandis que la version Server l'effectue sur plusieurs fils d'exécution GC, ce qui permet de le terminer beaucoup plus rapidement. Le GC du serveur peut également s'accommoder d'un taux d'allocation plus élevé, car il y a plusieurs tas sur lesquels l'application peut allouer, ce qui est très bon pour le débit.
Concurrent - effectuer le travail GC pendant que les threads de l'utilisateur sont en pause - appelé Stop-The-World - rend l'implémentation plus simple mais la durée de ces pauses peut être inacceptable. .NET offre une saveur concurrente pour atténuer ce problème.
Épinglage - le GC .NET prend en charge l'épinglage des objets, qui permet une interopérabilité sans copie avec le code natif. Cette capacité permet une interopérabilité native haute performance et haute fidélité, avec une surcharge limitée pour le GC.
GC autonome - il est possible d'utiliser un GC autonome avec une implémentation différente (spécifiée via la configuration et répondant aux exigences de l'interface). Cela facilite les recherches et l'essai de nouvelles fonctionnalités.
Diagnostic - Le GC fournit des informations riches sur la mémoire et les collections, structurées d'une manière qui vous permet de corréler les données avec le reste du système. Par exemple, vous pouvez évaluer l'impact du GC sur la latence de votre queue en capturant les événements GC et en les corrélant avec d'autres événements comme les entrées/sorties pour calculer la contribution du GC par rapport aux autres facteurs, afin de diriger vos efforts vers les bons composants.
Sécurité
La sécurité de la programmation a été l'un des thèmes majeurs de la dernière décennie. Il s'agit d'un élément inhérent à un environnement géré comme .NET.
Formes de sécurité :
- Sécurité de type - Un type arbitraire ne peut pas être utilisé à la place d'un autre, ce qui évite un comportement indéfini.
- Sécurité de la mémoire - Seule la mémoire allouée est utilisée, par exemple une variable fait référence à un objet vivant ou est nulle.
- Concurrence ou sécurité des threads - Les données partagées ne peuvent pas être accédées d'une manière qui entraînerait un comportement indéfini.
Remarque : le gouvernement fédéral américain a récemment publié des conseils sur l'importance de la sécurité de la mémoire.
.NET a été conçu comme une plate-forme sûre dès sa conception initiale. En particulier, il devait permettre la mise en œuvre d'une nouvelle génération de serveurs Web, qui doivent par nature accepter des entrées non fiables dans l'environnement informatique le plus hostile du monde (Internet). Il est désormais généralement admis que les programmes Web doivent être écrits dans des langages sûrs.
La sécurité des types est assurée par une combinaison du langage et du moteur d'exécution. Le compilateur valide les invariants statiques, tels que l'affectation de types différents - par exemple, l'affectation de string à Stream - qui produira des erreurs de compilation. Le runtime valide les invariants dynamiques, tels que le casting entre des types différents, qui produira une exception InvalidCastException.
La sécurité de la mémoire est assurée en grande partie par la coopération entre un générateur de code (comme un JIT) et un ramasseur de déchets. Les variables font référence à des objets vivants, sont nulles ou sont hors de portée. La mémoire est auto-initialisée par défaut, de sorte que les nouveaux objets n'utilisent pas de mémoire non initialisée. La vérification des limites garantit que l'accès à un élément d'un tableau dont l'index n'est pas valide ne permettra pas de lire de la mémoire non définie - souvent causée par des erreurs de type off-by-one - mais entraînera une IndexOutOfRangeException.
La gestion des nullités est une forme spécifique de sécurité de la mémoire. Les types de référence nuls sont une fonctionnalité du langage C# et du compilateur qui identifie de manière statique le code qui ne gère pas les null de manière sûre. En particulier, le compilateur vous avertit si vous déréférencez une variable qui pourrait être nulle. Vous pouvez également interdire l'affectation de null, de sorte que le compilateur vous prévient si vous affectez une variable à partir d'une valeur qui pourrait être nulle. Le moteur d'exécution dispose d'une fonction de validation dynamique correspondante qui empêche l'accès aux références nulles en déclenchant l'exception NullReferenceException.
Cette fonctionnalité C# s'appuie sur les attributs nullables de la bibliothèque. Elle repose également sur leur application exhaustive dans les bibliothèques et les piles d'applications (ce que nous avons fait) afin que votre code puisse recevoir des résultats précis de la part des outils d'analyse statique.
Code : | Sélectionner tout |
1 2 | string? SomeMethod() => null; string value = SomeMethod() ?? "default string"; |
Il n'y a pas de sécurité concurrentielle intégrée dans .NET. Les développeurs doivent plutôt suivre des modèles et des conventions pour éviter les comportements non définis. Il existe également des analyseurs et d'autres outils dans l'écosystème .NET qui permettent de mieux comprendre les problèmes de concurrence. De plus, les bibliothèques de base comprennent une multitude de types et de méthodes qui peuvent être utilisés en toute sécurité de manière concurrente, par exemple les collections concurrentes qui prennent en charge un nombre illimité de lecteurs et d'écrivains concurrents sans risque de corruption de la structure de données.
Le runtime expose des modèles de code sûrs et non sûrs. La sécurité est garantie pour le code sûr, qui est le code par défaut, tandis que les développeurs doivent choisir d'utiliser du code non sûr. Le code non sécurisé est généralement utilisé pour interopérer avec la plate-forme sous-jacente, interagir avec le matériel ou mettre en œuvre des optimisations manuelles pour les chemins critiques en termes de performances.
Un bac à sable est une forme spéciale de sécurité qui fournit une isolation et restreint l'accès entre les composants. Nous nous appuyons sur des technologies d'isolation standard, comme les processus (et les CGroups), les machines virtuelles et WebAssembly (avec leurs différentes caractéristiques).
Gestion des erreurs
Les exceptions constituent le principal modèle de gestion des erreurs dans .NET. Les exceptions présentent l'avantage que les informations relatives aux erreurs n'ont pas besoin d'être représentées dans les signatures de méthodes ou traitées par chaque méthode.
Le code suivant illustre un modèle typique :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 | try { var lines = await File.ReadAllLinesAsync(file); Console.WriteLine($"The {file} has {lines.Length} lines."); } catch (Exception e) when (e is FileNotFoundException or DirectoryNotFoundException) { Console.WriteLine($"{file} doesn't exist."); } |
Les exceptions sont lancées à partir du point d'erreur et collectent automatiquement des informations de diagnostic supplémentaires sur l'état du programme. Ces informations sont utilisées pour le débogage interactif, l'observabilité des applications et le débogage post-mortem. Chacune de ces approches de diagnostic repose sur l'accès à des informations riches sur les erreurs et l'état de l'application pour diagnostiquer les problèmes.
Les exceptions sont destinées à des situations rares. Cela est dû, en partie, au fait qu'elles ont un coût de performance relativement élevé. Elles ne sont pas destinées à être utilisées pour le flux de contrôle, même si elles sont parfois utilisées de cette façon.
Les exceptions sont utilisées (en partie) pour l'annulation. Elles permettent d'arrêter efficacement l'exécution et de dérouler une pile d'appels qui avait du travail en cours lorsqu'une demande d'annulation est observée.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 | try { await source.CopyToAsync(destination, cancellationToken); } catch (OperationCanceledException) { Console.WriteLine("Operation was canceled"); } |
La gestion des erreurs, et plus généralement les diagnostics, sont implémentés via des API d'exécution de bas niveau, des bibliothèques de plus haut niveau et des outils. Ces capacités ont été conçues pour prendre en charge les nouvelles options de déploiement telles que les conteneurs. Par exemple, dotnet-monitor peut transmettre des données d'exécution d'une application à un auditeur via un serveur web intégré orienté diagnostic.
Concurrence
La possibilité de faire plusieurs choses en même temps est fondamentale pour pratiquement toutes les charges de travail, qu'il s'agisse d'applications clientes effectuant un traitement en arrière-plan tout en maintenant la réactivité de l'interface utilisateur, de services gérant des milliers et des milliers de requêtes simultanées, de dispositifs répondant à une multitude de stimuli simultanés ou de machines très puissantes parallélisant le traitement des opérations de calcul intensif. Les systèmes d'exploitation prennent en charge cette concurrence par le biais des threads, qui permettent de traiter indépendamment plusieurs flux d'instructions, le système d'exploitation gérant l'exécution de ces threads sur tous les cœurs de processeur disponibles dans la machine. Les systèmes d'exploitation prennent également en charge les E/S, avec des mécanismes permettant d'exécuter les E/S de manière évolutive avec de nombreuses opérations d'E/S "en cours" à un moment donné. Les langages et cadres de programmation peuvent ensuite fournir divers niveaux d'abstraction au-dessus de ce support de base.
.NET fournit une telle prise en charge de la concurrence et de la parallélisation à plusieurs niveaux d'abstraction, à la fois via des bibliothèques et en étant profondément intégré à C#. La classe Thread se situe au bas de la hiérarchie et représente un thread du système d'exploitation, permettant aux développeurs de créer de nouveaux threads et de se joindre à eux. ThreadPool se situe au-dessus des threads, permettant aux développeurs de penser en termes d'éléments de travail qui sont programmés de manière asynchrone pour s'exécuter sur un pool de threads, la gestion de ces threads (y compris l'ajout et le retrait de threads du pool, et l'affectation d'éléments de travail à ces threads) étant laissée à l'exécution. Task fournit ensuite une représentation unifiée pour toutes les opérations effectuées de manière asynchrone et qui peuvent être créées et jointes de multiples façons ; par exemple, Task.Run permet de planifier l'exécution d'un délégué sur le ThreadPool et renvoie une Task pour représenter l'achèvement éventuel de ce travail, tandis que Socket.ReceiveAsync renvoie une Task<int> (ou ValueTask<int> qui représente l'achèvement éventuel de l'E/S asynchrone pour lire les données en attente ou futures d'un Socket. Une vaste gamme de primitives de synchronisation est fournie pour coordonner les activités de manière synchrone et asynchrone entre les threads et les opérations asynchrones, et une multitude d'API de plus haut niveau sont fournies pour faciliter la mise en œuvre des modèles de concurrence courants, par exemple, Parallel.ForEach et Parallel.ForEachAsync facilitent le traitement de tous les éléments d'une séquence de données en parallèle.
La prise en charge de la programmation asynchrone est également une caractéristique de premier ordre du langage de programmation C#, qui fournit les mots-clés async et await qui facilitent l'écriture et la composition d'opérations asynchrones tout en bénéficiant de tous les avantages des constructions de flux de contrôle que le langage a à offrir.
Reflection
Reflection est un paradigme de "programmes en tant que données", permettant à une partie d'un programme d'interroger et/ou d'invoquer dynamiquement une autre partie, en termes d'assemblages, de types et de membres. Elle est particulièrement utile pour les modèles et outils de programmation à liaison tardive.
Le code suivant utilise la réflexion pour trouver et invoquer des types.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | foreach (Type type in typeof(Program).Assembly.DefinedTypes) { if (type.IsAssignableTo(typeof(IStory)) && !type.IsInterface) { IStory? story = (IStory?)Activator.CreateInstance(type); if (story is not null) { var text = story.TellMeAStory(); Console.WriteLine(text); } } } interface IStory { string TellMeAStory(); } class BedTimeStore : IStory { public string TellMeAStory() => "Once upon a time, there was an orphan learning magic ..."; } class HorrorStory : IStory { public string TellMeAStory() => "On a dark and stormy night, I heard a strange voice in the cellar ..."; } |
Reflection est peut-être le système le plus dynamique proposé dans .NET. Il est destiné à permettre aux développeurs de créer leurs propres chargeurs de code binaire et répartiteurs de méthodes, avec une sémantique qui peut correspondre ou diverger des politiques de code statique (définies par le runtime). Reflection expose un modèle d'objet riche, qui est facile à adopter pour des cas d'utilisation restreints, mais qui nécessite une compréhension plus approfondie du système de types .NET lorsque les scénarios deviennent plus complexes.
Reflection permet également un mode distinct dans lequel le code d'octet IL généré peut être compilé en JIT au moment de l'exécution, parfois utilisé pour remplacer un algorithme général par un algorithme spécialisé. Il est souvent utilisé dans les sérialiseurs ou les mappeurs relationnels d'objets une fois que le modèle d'objet et d'autres détails sont connus.
Format binaire compilé
Les applications et les bibliothèques sont compilées en un bytecode standardisé multiplateforme au format PE/COFF. La distribution binaire est avant tout une fonctionnalité de performance. Elle permet aux applications de s'adapter à un nombre toujours plus grand de projets. Chaque bibliothèque comprend une base de données des types importés et exportés, appelée métadonnées, qui joue un rôle important tant pour les opérations de développement que pour l'exécution de l'application.
Les binaires compilés comprennent deux aspects principaux :
- Le bytecode binaire - format laconique et régulier qui évite d'avoir à analyser la source textuelle après la compilation par un compilateur de langage de haut niveau (comme C#).
- Les métadonnées - décrivent les types importés et exportés, y compris l'emplacement du code octet d'une méthode donnée.
Pour le développement, les outils peuvent lire efficacement les métadonnées pour déterminer l'ensemble des types exposés par une bibliothèque donnée et lesquels de ces types mettent en œuvre certaines interfaces, par exemple. Ce processus accélère la compilation et permet aux IDE et autres outils de présenter avec précision des listes de types et de membres pour un contexte donné.
Pour l'exécution, les métadonnées permettent de charger les bibliothèques de manière paresseuse, et les corps de méthode encore plus. Reflection (abordé précédemment) est l'API d'exécution pour les métadonnées et l'IL. Il existe d'autres API plus appropriées pour les outils.
Le format IL est resté rétrocompatible au fil du temps. La dernière version de .NET peut toujours charger et exécuter des binaires produits avec les compilateurs .NET Framework 1.0.
Les bibliothèques partagées sont généralement distribuées via des paquets NuGet. Les paquets NuGet, avec un seul binaire, peuvent fonctionner sur n'importe quel système d'exploitation et n'importe quelle architecture, par défaut, mais peuvent aussi être spécialisés pour fournir un comportement spécifique dans des environnements particuliers.
Génération de code
Le bytecode .NET n'est pas un format exécutable par la machine, mais il doit être rendu exécutable par une forme de générateur de code. Cela peut être réalisé par une compilation en avance sur le temps (AOT), une compilation juste à temps (JIT), une interprétation ou une transpilation. En fait, toutes ces méthodes sont utilisées aujourd'hui dans divers scénarios.
.NET est surtout connu pour la compilation JIT. Les JIT compilent les méthodes (et d'autres membres) en code natif pendant que l'application est en cours d'exécution et uniquement lorsqu'elles sont nécessaires, d'où le nom de "just in time". Par exemple, un programme peut n'appeler qu'une seule méthode parmi plusieurs sur un type au moment de l'exécution. Un JIT peut également tirer parti d'informations qui ne sont disponibles qu'au moment de l'exécution, comme les valeurs des variables statiques initialisées en lecture seule ou le modèle exact de CPU sur lequel le programme s'exécute, et peut compiler la même méthode plusieurs fois afin de l'optimiser à chaque fois pour différents objectifs et avec les enseignements tirés des compilations précédentes.
Les JIT produisent du code pour un système d'exploitation et une architecture de puce donnés. .NET dispose d'implémentations JIT qui prennent en charge, par exemple, les jeux d'instructions Arm64 et x64, ainsi que les systèmes d'exploitation Linux, macOS et Windows. En tant que développeur .NET, vous n'avez pas à vous soucier des différences entre les jeux d'instructions des processeurs et les conventions d'appel des systèmes d'exploitation. Le JIT se charge de produire le code que le CPU veut. Il sait également comment produire un code rapide pour chaque CPU, et les fournisseurs de systèmes d'exploitation et de CPU nous aident souvent à faire exactement cela.
L'AOT est similaire, sauf que le code est généré avant l'exécution du programme. Les développeurs choisissent cette option car elle peut améliorer considérablement le temps de démarrage en éliminant le travail effectué par un JIT. Les applications créées par AOT sont par nature spécifiques à un système d'exploitation et à une architecture, ce qui signifie que des étapes supplémentaires sont nécessaires pour faire fonctionner une application dans plusieurs environnements. Par exemple, si vous souhaitez prendre en charge Linux et Windows ainsi que Arm64 et x64, vous devez construire quatre variantes (pour tenir compte de toutes les combinaisons). Le code AOT peut également fournir des optimisations précieuses, mais pas autant que le JIT en général.
Nous couvrirons l'interprétation et la transpilation dans un post ultérieur, cependant, ils jouent également des rôles critiques dans notre écosystème.
L'une des optimisations du générateur de code est l'intrinsèque. Les intrinsèques matériels sont un exemple où les API .NET sont directement traduites en instructions CPU. Cette méthode a été utilisée de manière omniprésente dans les bibliothèques .NET pour les instructions SIMD.
Interopérabilité
.NET a été explicitement conçu pour une interopérabilité à faible coût avec les bibliothèques natives. Les programmes et les bibliothèques .NET peuvent appeler de manière transparente les API de bas niveau du système d'exploitation ou exploiter le vaste écosystème des bibliothèques C/C++. Le moteur d'exécution moderne de .NET s'attache à fournir des éléments d'interopérabilité de bas niveau tels que la possibilité d'appeler des méthodes natives via des pointeurs de fonction, l'exposition de méthodes gérées en tant que callbacks non gérés ou le casting d'interfaces personnalisées. .NET évolue également en permanence dans ce domaine et, dans .NET 7, a publié des solutions générées par les sources qui réduisent davantage les frais généraux et sont conviviales pour l'AOT.
Le code suivant démontre l'efficacité des pointeurs de fonctions C#.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | // Using a function pointer avoids a delegate allocation. // Equivalent to `void (*fptr)(int) = &Callback;` in C delegate* unmanaged<int, void> fptr = &Callback; RegisterCallback(fptr); [UnmanagedCallersOnly] static void Callback(int a) => Console.WriteLine($"Callback: {a}"); [LibraryImport("...", EntryPoint = "RegisterCallback")] static partial void RegisterCallback(delegate* unmanaged<int, void> fptr); |
Des paquets indépendants fournissent des solutions d'interopérabilité de plus haut niveau spécifiques au domaine en tirant parti de ces blocs de construction de bas niveau, par exemple ClangSharp, Xamarin.iOS & Xamarin.Mac, CsWinRT, CsWin32 et DNNE.
Ces nouvelles fonctionnalités ne signifient pas que les solutions d'interopérabilité intégrées, telles que le marshalling géré/non géré au moment de l'exécution ou l'interopérabilité Windows COM, ne sont pas utiles - nous savons qu'elles le sont et que les gens s'y fient. Ces fonctionnalités qui ont été historiquement intégrées dans le runtime continuent d'être prises en charge telles quelles, uniquement à des fins de rétrocompatibilité, sans qu'il soit prévu de les faire évoluer davantage. Tous les investissements futurs seront axés sur les modules d'interopérabilité et sur les solutions spécifiques au domaine et plus performantes qu'ils permettent.
Distributions binaires
L'équipe .NET de Microsoft maintient plusieurs distributions binaires, prenant plus récemment en charge Android, iOS et WebAssembly. L'équipe utilise une variété de techniques pour spécialiser la base de code pour chacun de ces environnements. La majeure partie de la plate-forme est écrite en C#, ce qui permet de concentrer le portage sur un ensemble relativement restreint de composants.
La communauté maintient un autre ensemble de distributions, largement axées sur Linux. Par exemple, .NET est inclus dans Alpine Linux, Fedora, Red Hat Enterprise Linux et Ubuntu.
La communauté a également étendu .NET pour qu'il fonctionne sur d'autres plateformes. Samsung a porté .NET pour sa plate-forme Tizen basée sur Arm. Red Hat et IBM ont porté .NET sur LinuxONE/s390x. Loongson Technology a porté .NET sur LoongArch. Nous espérons et attendons que de nouveaux partenaires portent .NET sur d'autres environnements.
Unity Technologies a lancé une initiative pluriannuelle pour moderniser son runtime .NET.
Le projet open source .NET est maintenu et structuré pour permettre aux individus, aux entreprises et à d'autres organisations de collaborer ensemble selon un modèle traditionnel en amont. Microsoft est l'intendant de la plate-forme, fournissant à la fois la gouvernance et l'infrastructure du projet (comme les pipelines de CI). L'équipe de Microsoft collabore avec les organisations pour les aider à réussir l'utilisation et/ou le portage de .NET. Le projet a une large politique d'upstreaming, qui inclut l'acceptation des changements qui sont uniques à une distribution donnée.
Le projet source-build, que plusieurs organisations utilisent pour construire .NET selon les règles typiques des distributions, par exemple Canonical (Ubuntu) et Red Hat, est un élément essentiel. Cet aspect s'est développé plus récemment avec l'ajout d'un Virtual Mono Repo (VMR). Le projet .NET est composé de nombreux dépôts, ce qui améliore l'efficacité des développeurs .NET mais rend plus difficile la construction du produit complet. Le VMR résout ce problème.
Résumé
Nous sommes à plusieurs versions de l'ère moderne de .NET, ayant récemment publié .NET 7. Nous avons pensé qu'il serait utile de résumer ce que nous nous sommes efforcés de construire - aux niveaux les plus bas de la plate-forme - depuis .NET Core 1.0. Bien que nous ayons clairement conservé l'esprit de la version originale de .NET, le résultat est une nouvelle plate-forme qui emprunte une nouvelle voie et offre une nouvelle valeur ajoutée considérable aux développeurs.
Reprenons là où nous avons commencé. .NET est synonyme de quatre valeurs : Productivité, Performance, Sécurité et Fiabilité. Nous croyons fermement que les développeurs sont mieux servis lorsque différentes plateformes linguistiques offrent des approches différentes. En tant qu'équipe, nous cherchons à offrir une productivité élevée aux développeurs .NET tout en fournissant une plate-forme leader en termes de performances, de sécurité et de fiabilité.
Source : Microsoft
Et vous ?
Que pensez-vous de .NET ?
Que pensez-vous de cette présentation de .NET par Microsoft ?
Voir aussi :
Azure Functions : apprendre à créer des fonctions durables isolées, un billet blog d'Hinault Romaric
Formation Azure Arc, une série de tutoriels vidéos, proposé par Hinault Romaric
Microsoft met à jour sa stratégie en matière de langages .NET, maintient Visual Basic en vie mais presque gelé