Skip to content

SupportedFeatures fr

Edgar Mesquita edited this page Oct 13, 2026 · 1 revision

Matrice des fonctionnalités C# prises en charge

🌐 Cette page en : English · Português · Español · Français · Deutsch

Ce document fournit une liste complète des fonctionnalités C#, des API .NET et des modèles pris en charge par le compilateur d'eQuantic.UI.

Légende

✅ Prise en charge complète : transpilé en un comportement JavaScript équivalent.

⚠️ Prise en charge partielle : fonctionne avec des réserves ou de légères différences.

❌ Non pris en charge : logique fondamentalement incompatible (p. ex. E/S bloquantes, pointeurs unsafe).


🏗️ Fonctionnalités centrales du langage

Fonctionnalité Statut Notes
Classes et structs ✅ Transpilés en classes ES6. Une classe ordinaire a un module à elle quoi qu'elle déclare, y compris une classe vide sur les membres par défaut d'une interface ou sur une base, et chaque module qui la nomme l'importe ; une classe dont la chaîne de bases atteint un attribut ou un type [ServerOnly] n'en a aucun, à trois niveaux de profondeur comme à un seul (class Underline : Mark sur class Mark : Attribute), y compris une base issue d'une bibliothèque référencée. Une classe d'exception de l'application est une classe, avec un module à elle (voir try / catch / finally). La chaîne se lit par ce dont chaque base dérive et jamais par son nom, donc une classe sur une interface nommée comme un attribut garde son module, et un nom est un module partout où l'une quelconque de ses déclarations en est un. Une classe, une classe statique, un record ou un struct déclaré à l'intérieur d'un autre type est aussi un module à part entière, nommé par les types qui le contiennent et par son propre nom joints par $ (Cart$Item, A$B$C), ce qu'aucun type de premier niveau ne peut prendre : chaque référence à lui, à l'intérieur de son propriétaire ou au dehors, nomme ce jumeau, donc une classe imbriquée à côté d'une classe de premier niveau du même nom n'est jamais prise pour elle, et une classe à l'intérieur d'une classe réservée au serveur ou d'un attribut n'en a aucun, comme son propriétaire n'en a aucun : le code client qui la nomme est refusé (EQ2010), et une page qui y est déclarée n'a pas de route, tandis qu'une classe à l'intérieur d'une classe d'exception traverse comme cette classe (Depuis 0.2.0-preview.61). Un record et un struct ont un jumeau quoi qu'ils déclarent, sauf s'ils sont marqués [ServerOnly], y compris un record sur un record réservé au serveur (Depuis 0.2.0-preview.61).
Interfaces ✅ Servent à la vérification de types de TypeScript (effacées à l'exécution). Un membre par défaut (une propriété, un indexeur ou une méthode avec un corps ; un indexeur depuis 0.2.0-preview.61) atteint le jumeau de chaque classe qui le prend sans le déclarer : converti depuis la source de l'interface, ou, pour les interfaces du SDK lui-même qu'une application implémente (un thème, un langage de code), délégué à la copie du runtime. Une interface de tout autre assembly compilé n'a ni l'un ni l'autre, et le build refuse la classe (EQ1008) (Depuis 0.2.0-preview.59).
Enums ✅ Traversent sous leur nom en camelCase (MainAlign.Start → 'start'), et un enum [Flags] sous son nombre. .ToString(), l'interpolation et la concaténation donnent le nom du membre C# ("Start"), le même texte que celui qu'imprime le serveur, les flags positionnés d'une combinaison de flags ("Read, Write"), une valeur qu'aucun membre ne nomme sous la forme de ses chiffres, et rien pour un enum nullable null. ToString("D"), "X", "F" et "G", le format d'une interpolation et son alignement écrivent ce qu'écrit .NET, Enum.Parse, TryParse, GetName, GetNames, GetValues et IsDefined répondent comme ceux de .NET, et un dictionnaire dont la clé est un enum traverse dans les deux sens, un nom qu'aucun membre ne porte étant refusé. Depuis 0.2.0-preview.15 ; les formats, le texte des flags, les statiques d'Enum et les clés enum depuis 0.2.0-preview.60.
Génériques ✅ Entièrement pris en charge (effacés à l'exécution).
Méthodes d'extension ✅ Résolues via le Semantic Model et transpilées en appels directs.
Async / Await ✅ Correspond à async / await et Promise.
Expressions lambda ✅ Correspondent aux fonctions fléchées () => {}.
Filtrage par motif ✅ Motifs is, motifs de propriété, motifs récursifs. Un type sans rien de lié (o is int or long, int => …, case int:) teste le type, un long comme un BigInt, un entier comme un nombre entier et un char comme une chaîne d'une seule unité de code. Une constante dans un motif, x is Limits.Max compris, est testée par sa valeur : un null correspond à toute absence, un decimal et un NaN se comparent par valeur, et une constante long est un BigInt, comme l'est tout long (Depuis 0.2.0-preview.60). Un motif positionnel sur un type dont l'application a écrit le Deconstruct, y compris celui d'une extension, l'appelle une fois pour chaque valeur qu'un test déconstruit, à travers les branches d'un switch et les alternatives d'un or aussi, comme le fait .NET, et lit les parties qu'il renvoie, comme le fait une déconstruction : il lisait les membres dont les out portent le nom, ce que n'est pas une partie que calcule le Deconstruct (Depuis 0.2.0-preview.61).
Interpolation de chaînes ✅ Correspond aux littéraux de gabarit `${var}`. Un format avec du texte entre guillemets ({date:dd 'de' MMMM}) garde ses guillemets, et une chaîne interpolée brute garde ses accolades ($$$"""{{x}}""" est {{x}}) (Depuis 0.2.0-preview.60). Un alignement est le nombre qu'il est, y compris une constante par son nom ou une expression de constantes ({name,Widths.Name}, {name,-(Widths.Name + 1)}), dans un StringBuilder aussi (Depuis 0.2.0-preview.62).
Littéraux de chaîne et de char ✅ Une chaîne ou un char garde la valeur que lit C#, quelle que soit son écriture : un substitut isolé ("x\uD83D"), un caractère de contrôle, et les échappements que seul C# possède ('\a', '\e', '\x041', '\U00000041'). Un littéral UTF-8 ("ab"u8) n'a pas de jumeau et fait échouer le build (EQ1004) (Depuis 0.2.0-preview.60).
Coalescence nulle ✅ ?? et ??= transposés en leurs équivalents JS.
Initialiseurs d'objet ✅ new Obj { Prop = 1 } sur un record, un struct, une classe ordinaire, une exception ou un type du vocabulaire dont le runtime transpile le jumeau (les modèles du tableur et des formulaires, CellRef, FieldError, SheetController…), appliqué une fois que le constructeur a rendu la main, comme C# l'applique (une classe ordinaire le prenait comme un objet de configuration final, une exception l'abandonnait, et un type du vocabulaire transpilé le remettait à un emplacement que son jumeau n'a pas, avant 0.2.0-preview.61 ; un composant le prend comme ses props) : chaque initialiseur du type s'est exécuté, y compris celui du membre lui-même, et les valeurs sont lues après eux. Un initialiseur de collection imbriqué (Items = { 1, 2 }) ajoute à la collection que détient le membre, via le Add que résout l'arbre lié (celui d'une méthode d'extension via sa classe hôte, et celui d'une liste, d'un ensemble ou d'un membre ICollection<T> comme l'est un appel à celui-ci) ; un initialiseur d'objet imbriqué (Inner = { N = 3 }) affecte dans l'objet que détient le membre ; et une entrée ([key] = value) est écrite via l'indexeur du type. La paire d'un dictionnaire (Map = { { "a", 1 } }) est son Add, qui refuse une clé déjà présente. Chaque partie est évaluée dans la méthode où elle a été écrite, donc un await dans un élément fonctionne, et chaque élément est appliqué avant que les parties du suivant ne soient évaluées, le membre auquel il ajoute ou dans lequel il affecte étant lu avant ses propres parties, comme C# l'applique. Un élément sans abaissement fait échouer le build (EQ1004) (Depuis 0.2.0-preview.61).
field (C# 14) ✅ Propriétés gardées : le jumeau reçoit un emplacement $name (un nom qu'aucun champ C# ne peut prendre), un getter et le corps propre de l'accesseur. Une propriété statique garde son emplacement sur le type, y compris celle d'un record, et l'emplacement commence avec l'initialiseur de la propriété ou la valeur par défaut de son type (Depuis 0.2.0-preview.60). Celle d'instance d'un record et d'un struct exécute les corps de ses accesseurs sur l'emplacement, comme celle d'une classe, et une propriété dont le getter a un corps est un état que son record compare et copie ; elle était conservée sous le nom de la propriété, le corps de son setter étant abandonné, avant 0.2.0-preview.61 (Depuis 0.2.0-preview.61).
Propriétés virtuelles et redéfinies ✅ Une propriété qu'un type dérivé peut redéfinir ou qui en redéfinit une (une auto-propriété déclarée virtual ou override) garde sa valeur dans un stockage à elle ($name) sous des accesseurs sur le prototype du jumeau, dans une classe, un record et un struct de même, donc chaque lecture atteint la redéfinition et aucun initialiseur n'atteint l'accesseur d'un autre niveau : une auto-propriété sur une propriété calculée, ou une propriété calculée sur une auto-propriété, levait une exception au new ou renvoyait la valeur de la base avant 0.2.0-preview.61. Une redéfinition qui ne déclare que le getter d'une propriété dont la base a les deux garde le setter de la base, comme C# en hérite. Un record compare, hache et copie le stockage, comme .NET compare un champ de stockage. Un jumeau avec un stockage l'écrit en JSON sous le nom de sa propriété, donc une Server Action reçoit la propriété sous son nom C#. Une différence demeure : dans un type qui redéfinit une auto-propriété par une autre, base.Name lit la valeur de la redéfinition, là où .NET lit le champ propre de la base, puisque les deux partagent un même stockage (Depuis 0.2.0-preview.61).
Membres qui masquent (new) ✅ Une méthode qui masque un membre hérité, avec new ou avec la même signature et sans override, est atteinte comme C# l'atteint : via le type de base c'est la méthode de la base qui répond, et via le type qui masque, celle qui masque, pour un appel, un groupe de méthodes, un appel base et ?. de même, y compris une méthode générique sur son propre paramètre de type, et il en va de même pour un membre que le runtime lit par son nom .NET, donc ((object)c).GetHashCode() est celui d'object sur un public new int GetHashCode(). Son jumeau la conserve sous un nom à elle (name$1), et une redéfinition d'une méthode new virtual occupe la place de cette méthode. Il en va de même d'une méthode à côté d'une implémentation explicite d'interface de son nom, donc un appel via la classe l'atteint et un appel via l'interface atteint l'implémentation explicite. Une méthode qui masque un membre, ou se tient à côté d'une telle implémentation, et répond à une interface fait échouer le build (EQ1007), et une propriété ou un champ qui en masque un partage encore son nom avec lui (#563, depuis 0.2.0-preview.61).
Accesseurs init ✅ Émis comme un setter JS, parce qu'un corps init est l'endroit où un type énonce son invariant, et l'abandonner ferait perdre la vérification côté client.
Initialiseurs de collection ✅ new List<int> { 1, 2 } correspond à [1, 2]. Une liste construite avec un argument et un initialiseur contient les deux, comme C# la construit : une copie de la source, jamais la source elle-même, ou rien pour une capacité, puis chaque élément dans l'ordre, la source ou la capacité étant lue en premier. Une capacité est quand même évaluée, et une capacité négative est refusée comme le constructeur la refuse. Ainsi new List<int>(source) { 3 } est [...source, 3] et new List<int>(10) { 1, 2 } est [1, 2], écrits en entier ou typés par la cible, et List<int> c = new(source) est une copie. Une capacité abandonnait les éléments, et une source leur était jointe par une virgule, avant 0.2.0-preview.61 (Depuis 0.2.0-preview.61).
IEnumerable<T> à vous ✅ Une classe, un record ou un struct qui implémente IEnumerable<T>, ou seulement IEnumerable, est énuméré comme le dit son propre GetEnumerator(), par foreach, un spread, string.Join et chaque opérateur LINQ, chacun via celui que C# lie pour lui : un foreach sur la classe parcourt son propre GetEnumerator() public (y compris celui d'un énumérateur struct), et LINQ, un spread, string.Join et un foreach sur l'interface parcourent celui d'IEnumerable<T>, explicite ou non, donc les deux peuvent parcourir des séquences différentes, comme dans .NET. C'est la séquence que produit une méthode itérateur, ou un énumérateur à vous, y compris une classe qui est son propre énumérateur, parcouru par son MoveNext et son Current et libéré quand le parcours se termine, break compris. La redéfinition d'une classe dérivée répond, le IEnumerable.GetEnumerator() explicite à côté du générique est la même séquence, et il en va de même d'un Current explicite à côté d'un public. Une classe qui n'a qu'un GetEnumerator() public est aussi parcourue par foreach. Le jumeau a une seule itération et un seul Current, donc un type qui implémente IEnumerable<T>, ou IEnumerator<T>, sur deux types fait échouer le build (EQ1007), de même qu'un IEnumerable.GetEnumerator() ou un Current explicite qui répond autrement que le membre que garde le jumeau, là où un membre qui renvoie la réponse de celui-ci (=> GetEnumerator();, => Current;) est conservé comme un seul. Le propre GetEnumerator() d'une collection .NET (=> items.GetEnumerator()) n'a pas de traduction et fait échouer le build (EQ2004) : produisez plutôt les éléments par yield (#612, depuis 0.2.0-preview.61).
Déconstruction ✅ var (a, b) = tuple correspond à [a, b] = tuple, de même que (var c, var d) = … et les désignations imbriquées. Un record ou un struct se déconstruit via le Deconstruct qu'il appelle, sous toutes les formes : une déclaration, une affectation ((a, b) = point, (var a, b) = point), un foreach (var (a, b) in points) et une déconstruction imbriquée. Celui d'un record lit ses membres, et un Deconstruct écrit par l'application s'exécute. Les récepteurs et les indices des cibles sont évalués avant la valeur, comme C# les évalue, donc (xs[i++], xs[i++]) = point écrit les éléments qu'écrit C# (Depuis 0.2.0-preview.60). Chaque partie atteint sa cible comme C# l'y place : convertie dans le type de la cible (une partie int dans un long), et écrite selon ce qu'est la cible, l'entrée d'un dictionnaire via sa classe, dans une affectation, une déclaration et une boucle (Depuis 0.2.0-preview.60).
Variables d'expression ✅ Ce que déclare un motif, un out var ou une déconstruction est déclaré là où C# le déclare, dans toute sorte de membre : dans le bloc englobant après un if ou une instruction d'expression, à l'intérieur d'une boucle ou d'un using, et neuf à chaque itération de boucle et à chaque appel, donc une fermeture garde sa propre valeur. Un nom que JavaScript réserve (@class) est renommé une fois, à sa déclaration et à chaque référence (Depuis 0.2.0-preview.59).
Fonctions locales ✅ Transpilées en fonctions internes. Un paramètre out ou ref rapporte sa valeur à l'appelant, comme celui d'une méthode (Depuis 0.2.0-preview.60).
Boucles for ✅ 1:1. La variable d'une boucle est unique pour toute la boucle, comme en C# : une fermeture créée dans le corps lit sa valeur actuelle, jamais une copie propre à son itération, et il en va de même de ce que déclare son initialiseur (out var n, var (i, j) = (0, 3)), y compris un en-tête fait d'expressions (for (Seed(out var n); …)) (Depuis 0.2.0-preview.60).
Délégués ✅ Appelés comme des valeurs, par leur nom, via un membre, ou depuis n'importe quelle expression (handlers[0](x), Make()(x), depuis 0.2.0-preview.60).
Records (with) ✅ Une copie du record sur sa propre classe, puis les membres que l'expression nomme : comme la copie de .NET, elle n'exécute aucun initialiseur ni aucun constructeur. Le with d'un struct, et celui des records du moteur de code, passent par la copie propre du jumeau. Un record dont la chaîne déclare un constructeur de copie est copié via celui-ci, comme l'est le with de C# : un constructeur déclaré démarre ses membres à leur zéro, exécute la copie de sa base avec ce que passe : base(…) puis son corps, et un niveau synthétisé copie ses propres membres (Depuis 0.2.0-preview.61).
Constructeurs ✅ Le constructeur jumeau d'un record, d'un struct ou d'une classe ordinaire est le constructeur C# que l'appel lie, chacun atteint par les nombres d'arguments qu'il accepte, ses paramètres optionnels comptés (une classe ordinaire gardait son plus large avant 0.2.0-preview.61). Celui qui fait son propre travail (le principal, ou un explicite qui ne chaîne pas) lie ses paramètres, exécute chaque initialiseur dans l'ordre de déclaration, celui d'un champ et celui d'une propriété de même, ceux d'un type dérivé avant le constructeur de sa base, appelle le constructeur de sa base avec ses propres arguments et exécute son corps ; celui qui chaîne avec : this(…) exécute celui auquel il chaîne puis son propre corps, même quand le corps de celui-ci retourne tôt. Chaque argument atterrit à la place de son paramètre et est évalué dans l'ordre où il est écrit, dans new, : this(…), : base(…) et une clause de base de même : un argument nommé, un optionnel omis, et un tableau params passé en entier (nommé ou non) ou constitué à partir de ses éléments ; une variable qu'un argument déclare (out var n) est déclarée là où il est évalué, et le paramètre d'un constructeur est une variable que son corps peut affecter. Le constructeur de copie d'un record n'est pas une branche. Deux constructeurs qui acceptent le même nombre d'arguments font échouer le build (EQ1009), de même qu'un chaînage vers un constructeur qui chaîne à son tour et : this() vers le constructeur implicite d'un struct à côté de constructeurs propres. Le zéro d'un struct (default, l'emplacement d'un tableau, new S() sans constructeur propre, y compris celui d'un struct générique, qui contient le zéro de chaque argument de type, et celui d'un autre assembly) n'en exécute rien, pas même son constructeur statique. Le constructeur principal d'une classe ordinaire lie ses paramètres comme celui d'un record, et un paramètre qu'un membre lit est conservé sur chaque instance sous son nom, donc un paramètre qui tombe sur le nom d'un autre membre fait échouer le build (EQ1007). Une différence demeure : un constructeur de base qui lit un membre d'une classe dérivée via un membre virtuel le lit non défini, là où .NET le lit initialisé, puisque JavaScript ne laisse rien toucher l'instance avant que super() ne rende la main (Depuis 0.2.0-preview.61).
Indexeurs ✅ Un indexeur d'instance est le item(…) et le setItem(…, value) de son jumeau, et chaque accès à celui-ci les appelle : une lecture, une écriture, une affectation composée, ++ sur une valeur de n'importe quel type, ??=, ?[…], la cible d'une déconstruction, une clé depuis la fin (^1, au compte que nomme son type) et l'entrée d'un initialiseur d'objet, le récepteur et les clés étant évalués une fois chacun, chaque clé à la place de son paramètre (une clé nommée, une clé optionnelle omise, une clé params). Une affectation renvoie la valeur qu'elle a affectée, quoi que fasse le setter de sa copie, et une implémentation explicite de l'indexeur d'une interface répond aux accès de l'interface. Un second indexeur dans un type, ou tout membre nommé Item ou SetItem à côté d'un indexeur, fait échouer le build (EQ1007) (Depuis 0.2.0-preview.61). Un accès via une face de liste (IList<T>, IReadOnlyList<T>, IList) atteint ce que la face contient au moment où il s'exécute, l'élément d'un tableau ou d'une liste et le item et le setItem d'un jumeau de même, sous chacune de ces formes, et son Count compte un type à vous par son propre Count, jamais par un Length à côté : la face lisait un indice et un length, auxquels un jumeau ne répond pas, avant 0.2.0-preview.61 (Depuis 0.2.0-preview.61).
Plages ✅ x[a..b] sur une chaîne, un tableau ou une liste est le slice de JavaScript, et une borne qui peut être ^0 se résout par rapport à la longueur comme le fait Index.GetOffset. Sur un type à vous avec un Length (ou un Count) et un Slice(int start, int length), il appelle ce Slice comme C# abaisse la plage : le récepteur une fois, puis les bornes dans l'ordre où elles sont écrites, puis le compte, lu seulement là où une borne compte depuis la fin ou là où la fin est laissée ouverte. Ainsi strip[1..3] est Slice(1, 2), là où il appelait le slice(1, 3) du jumeau avant 0.2.0-preview.61. Une plage passée à un indexeur qui prend le Range lui-même fait échouer le build (EQ2004), comme le fait un Range stocké comme valeur (Depuis 0.2.0-preview.61).
Initialisation statique ✅ Les statiques d'un type démarrent à leur zéro, puis les initialiseurs s'exécutent dans l'ordre de déclaration, puis le constructeur statique, une fois, la première fois que l'un d'eux est lu ou écrit : static int A = B + 1; static int B = 2; donne à A la valeur 1, comme dans .NET, dans un record, un struct, une classe, une classe statique et un composant. Un type avec un constructeur statique l'exécute avant sa première instance et la première utilisation de n'importe lequel de ses membres statiques, y compris une méthode, une propriété calculée et un opérateur. Les initialiseurs s'exécutent dans l'ordre de déclaration à travers champs, propriétés et événements de type champ, et le corps du constructeur statique après eux, un return qui s'y trouve ne terminant que lui ; celui qui lève une exception fait lever une TypeInitializationException à chaque utilisation du type, comme le fait .NET ; un statique dont C# replie l'initialiseur en constante est sa valeur ; et le constructeur statique d'un composant s'exécute une fois (Depuis 0.2.0-preview.61).
typeof ✅ Correspond à un littéral de chaîne du nom du type.
Types de base ✅ Une classe étend le jumeau de sa base quelle que soit la façon dont la base est écrite : nue, avec son espace de noms, via un alias ou avec global:: (Depuis 0.2.0-preview.60).
Noms de type qualifiés ✅ Un type atteint via son espace de noms, une partie de celui-ci, sa totalité ou global:: est importé comme l'est un type atteint via un using : à l'intérieur de App.Chat, Portal.Fold.Text(n) répond ce que répond Fold.Text(n) sous using App.Portal; (Depuis 0.2.0-preview.61).
Appels base ✅ Correspond au mot-clé super.
using static ✅ Un membre atteint par son nom simple répond ce que répond son écriture qualifiée : PI est Math.PI, Join(…) est string.Join(…), le High d'un enum est Level.High, et Now sous using static System.DateTime est DateTime.Now (Depuis 0.2.0-preview.61 ; jusque-là, un membre .NET en dehors de Math, MathF, string, des constantes et des enums faisait échouer le build même là où son écriture qualifiée se traduisait). Un membre .NET dont l'écriture qualifiée n'a pas non plus de traduction fait échouer le build (EQ2004) au lieu d'atteindre le navigateur (Depuis 0.2.0-preview.60).
Le Count propre d'un type ✅ Un type que déclare l'application ou une bibliothèque qu'elle référence lit son propre Count : le modèle de domaine d'une bibliothèque le lisait comme le length d'un tableau, undefined (Depuis 0.2.0-preview.60).
cast et as ✅ Correspond à un passage direct/une troncature en JS.
sizeof ✅ Correspond aux tailles des primitifs C#.
Méthodes anonymes ✅ delegate(...) { ... } correspond à des fonctions fléchées.
params ✅ Un paramètre REST (...xs). Les deux formes d'appel de C# fonctionnent : des arguments développés et un tableau passé en entier, qui est étalé. Depuis 0.2.0-preview.15.
stackalloc ✅ Correspond aux tableaux typés (p. ex. Int32Array).
yield return ✅ Correspond aux fonctions génératrices JS (function*).
try / catch / finally ✅ Les clauses catch sont UN seul catch JavaScript qui les essaie dans l'ordre de C# : chacune par son type d'exception, y compris un type qui en dérive, et par son filtre when, qui déclare ses propres variables (out var n, un motif) et répond false là où il lève une exception, comme celui de .NET. Une exception qu'aucune clause ne prend poursuit son chemin, et throw; relance l'exception interceptée. Une exception est une Error JavaScript qui porte ses types .NET, y compris une classe d'exception propre à l'application et Exception e = new("…"), donc e is ArgumentException, un switch sur des exceptions et as testent aussi le type, et une exception que lève le runtime est du type que .NET lève à cet endroit (un dépassement, une clé manquante, un mauvais format, un cast refusé) ; un TypeError JavaScript est une NullReferenceException. Une différence demeure, celle de la plateforme : .NET exécute un filtre AVANT les blocs finally situés entre le throw et la clause, et JavaScript après eux. Depuis 0.2.0-preview.60. Le message d'une exception du framework est composé comme .NET le compose : là où aucun message ou un message null a été donné, le texte que .NET écrit pour ce constructeur, lu depuis .NET lui-même, puis (Parameter 'x'), et la valeur réelle et le nom d'un objet libéré sur des lignes à part, les messages internes d'une exception agrégée après le sien et la phrase d'un initialiseur de type, et ParamName, ActualValue, InnerException et TypeName lisent ce que le constructeur a reçu, le InnerException d'une exception agrégée étant sa première exception interne, les propres exceptions d'argument du runtime nommant aussi leur paramètre (Depuis 0.2.0-preview.61). Un argument null est refusé comme .NET le refuse, par une ArgumentNullException qui nomme le paramètre, par Max, Min et ToDictionary de LINQ, par un opérateur LINQ auquel on passe une séquence qui n'est pas une liste (le second de Concat, le first de Zip), par new Guid(text), char.GetUnicodeCategory(text, i), CancellationToken.Register et CreateLinkedTokenSource, et CompareTo(object) répond 1 pour un null, comme celui de .NET. Un membre abaissé vers la méthode propre de JavaScript, dont LINQ sur une liste, lit encore un null à travers null, une NullReferenceException : la suite de conformité appelle chaque membre de la surface traduite avec un null pour chaque paramètre de type référence, et liste ceux-là dans null-argument-gaps.baseline.txt avec leurs raisons (Depuis 0.2.0-preview.61). Une classe d'exception de l'application est une classe sur la base d'exception du runtime, l'Error qui porte ses types : ses champs, ses propriétés, ses constructeurs et ses méthodes sont les siens, et son appel de base transmet ce que transmet un new de sa base .NET, donc : base(message, inner) atteint Message et InnerException, : base("bad", name) sur ArgumentException lit bad (Parameter 'x') et son ParamName, une classe sur InvalidOperationException qui ne donne aucun message lit le texte propre à ce type, un message manquant sur Exception est celui par défaut de .NET (Exception of type 'App.Failure' was thrown.), un membre à elle sous un nom que la base détient (un Name, une redéfinition de Message ou de ParamName) répond à sa place, et une classe générique est interceptée selon sa construction (catch (Failed<int>)) dès l'instant où son constructeur s'exécute, y compris un throw this à cet endroit. Un message manquant nomme le type comme .NET le nomme à l'exécution (App.Failed`1[System.Int32], App.Outer+Inner). Elle est transpilée comme toute classe : une classe que le navigateur ne voit jamais, dont les membres atteignent ce qu'il ne peut pas exécuter, est marquée [ServerOnly], et deux de ses constructeurs qui acceptent autant d'arguments l'un que l'autre sont refusés (EQ1009), sauf celui que seule la sérialisation de .NET appelle, (SerializationInfo, StreamingContext), que le modèle de Visual Studio écrit et que le jumeau laisse de côté, et un appel de celui-ci que ferait le navigateur, un new ou l'appel de base d'un autre constructeur, fait échouer le build (EQ1009). Une exception construite ou testée là où un argument de type qu'elle porte est un paramètre de type fait aussi échouer le build (EQ2014), puisque JavaScript l'efface : new Failed<T>() dans une méthode générique, catch (Failed<T>), new Missing() à l'intérieur de la classe générique qui la déclare. Une exception déclarée à l'intérieur d'un autre type est aussi une classe, son jumeau étant nommé par son propriétaire (Holder$Nested), et il en va de même de ce que déclare une classe d'exception, un record de la raison qu'elle porte (Rejected$Reason). Dans un extrait compilé seul, sans projet où lire ses bases, une classe d'exception de l'application est encore l'Error du runtime avec ses types et aucun de ses membres (#611, depuis 0.2.0-preview.61).
Expressions throw ✅ x ?? throw …, c ? a : throw … et => throw …, l'exception n'étant évaluée que là où elle est levée, y compris un await qu'elle contient. Depuis 0.2.0-preview.60.
lock ⚠️ Transpilé en son seul corps (JS est single-threaded). Son expression s'exécute une fois, devant lui, sauf si elle ne fait que lire une variable, un champ ou this (Depuis 0.2.0-preview.60 ; une expression qui déclare une variable, depuis 0.2.0-preview.59). new object(), la barrière habituelle, est un objet neuf, avec ou sans initialiseur vide.

🔄 Prise en charge de LINQ

Le compilateur inclut des stratégies spécialisées pour presque toutes les méthodes LINQ.

Logique Méthodes Statut
Filtrage Where, OfType ✅
Projection Select, SelectMany, Cast ✅
Partitionnement Skip, Take, SkipWhile, TakeWhile ✅
Tri OrderBy, OrderByDescending, ThenBy, Reverse ✅
Agrégation Count, Sum, Min, Max, Average, Aggregate ✅
Quantificateurs Any, All, Contains ✅
Ensembles Distinct, DistinctBy, Union, Intersect, Except, Concat ✅
Éléments First, FirstOrDefault, Single, Last, ElementAt ✅
Utilitaires SequenceEqual, DefaultIfEmpty ✅
Génération Enumerable.Range, Enumerable.Repeat, Enumerable.Empty ✅
Conversion ToList, ToArray, ToDictionary, ToHashSet ✅
Regroupement/Jointure GroupBy, Join, Zip ✅

Chaque opérateur lit sa source comme la séquence que .NET énumère, quelle que soit la forme sous laquelle le navigateur la détient : un tableau ou une liste tels quels, un HashSet, les paires d'un dictionnaire et l'ensemble trié, le dictionnaire trié, la liste triée, la file, la pile et la liste chaînée du runtime par leur propre énumération (une pile depuis son sommet), et une chaîne par ses chars, les unités de code UTF-16. ToList et ToArray créent toujours un nouveau tableau, donc une copie et sa source sont deux listes. Le foreach et le ToCharArray d'une chaîne donnent aussi des unités de code, une paire de substitution étant deux chars, new string(chars) et new string(chars, start, length) construisent leur texte, et un statique de char passé comme délégué fonctionne, "a1b2".Count(char.IsDigit) et Count(IsDigit) sous using static System.Char de même (Depuis 0.2.0-preview.60).

Enumerable.Range et Repeat évaluent leurs arguments une fois, là où C# le fait, y compris un await parmi eux : Range(Start(), 3) appelle Start une fois, et Repeat(new List<int>(), 3) répète une seule liste. Un compte négatif, ou un Range au-delà de int.MaxValue, lève l'ArgumentOutOfRangeException de .NET (Depuis 0.2.0-preview.60).

Un comparateur passé à ToDictionary, ToLookup, GroupBy, Distinct, ToHashSet ou SequenceEqual franchit la barrière que franchit le constructeur d'une collection. Celui qui demande l'égalité par défaut du type de la clé (EqualityComparer<T>.Default, un comparateur null, ou StringComparer.Ordinal pour une chaîne) répond comme l'opérateur répond sans lui, chaque sélecteur dans son paramètre qu'il soit nommé ou non, et tout autre, comme StringComparer.OrdinalIgnoreCase, fait échouer le build avec EQ2007. ToDictionary et GroupBy refusaient tout comparateur, ToLookup en appelait un comme sélecteur d'élément et Distinct abandonnait tout ce qu'on lui passait, avant 0.2.0-preview.61 (Depuis 0.2.0-preview.61). SequenceEqual renvoyait sa première séquence quand on lui passait un comparateur, avant 0.2.0-preview.62 (Depuis 0.2.0-preview.62).

Chaque surcharge répond comme sa propre surcharge .NET, chaque argument dans le paramètre auquel il se lie, nommé ou non. FirstOrDefault(defaultValue), LastOrDefault(defaultValue) et SingleOrDefault(defaultValue), avec ou sans prédicat, renvoient l'élément quand il y en a un, y compris un élément null, et la valeur passée sinon, et SingleOrDefault refuse un second élément ou une seconde correspondance avec l'InvalidOperationException de .NET. Zip(second) renvoie des paires et Zip(second, third) des triplets, lus comme First, Second et Third ou déconstruits, autant qu'en contient la séquence la plus courte. Average calcule la moyenne dans le type de l'élément : un int ou un long additionné exactement dans un long, une OverflowException au-delà de long.MaxValue, et divisé comme un double, une moyenne de float arrondie à un single, une de decimal comme un decimal, un nullable sur ses seules valeurs et null quand il n'en a aucune, et une séquence vide refusée avec une InvalidOperationException là où la réponse n'est pas nullable. SequenceEqual compare selon l'égalité par défaut du type de l'élément : un long et un decimal par valeur, un record par ses membres, une classe par son propre Equals et par référence là où elle n'en a pas, un tableau par référence, et NaN égal à lui-même. Avant 0.2.0-preview.62, une valeur par défaut était appelée comme un prédicat, la troisième séquence d'un Zip comme un sélecteur de résultat, et une moyenne de longs levait un TypeError (Depuis 0.2.0-preview.62).


📦 Types .NET (BCL)

Nous faisons correspondre les types .NET courants à leurs équivalents JavaScript.

Primitifs

Type .NET Équivalent JavaScript
string String
int, double, float Number
bool Boolean
object Object
dynamic any

System.String

  • ✅ Join, Format. string.Join lit n'importe quelle séquence (un tableau, une liste, un HashSet, une LinkedList, les clés d'un dictionnaire, une séquence derrière une interface) et écrit chaque valeur comme l'écrit le ToString de .NET : un bool comme True, un enum par le nom de son membre, un float par ses propres chiffres, un double dans la notation de .NET (1E+21), un null comme rien ; les valeurs passées une à une sont chacune écrites selon leur propre type, et un séparateur null n'écrit rien (Depuis 0.2.0-preview.61)
  • ✅ IsNullOrEmpty, IsNullOrWhiteSpace
  • ✅ Split, Replace, Substring, Trim. Split répond comme le fait la surcharge de .NET : un ensemble de séparateurs (un tableau de chars ou de chaînes, ou des chars écrits un par un) découpe à n'importe lequel d'entre eux, le premier dans l'ordre de l'ensemble l'emportant à un endroit donné ; un séparateur unique null ou vide ne découpe rien et un ensemble null ou vide découpe aux espaces blancs de .NET ; le compte plafonne les entrées, la dernière prenant le reste ; TrimEntries et RemoveEmptyEntries s'appliquent, écrits en entier ou conservés dans une variable ; et un compte négatif ou une option non définie lève l'exception de .NET (Depuis 0.2.0-preview.62)
  • ✅ ToLower, ToUpper et leur paire invariante changent la casse comme le fait .NET 10 : chaque unité de code, ou paire de substitution, devient exactement une autre (ß et fi restent tels quels, ᾳ passe en majuscule en ᾼ, et un sigma majuscule passe en minuscule en σ où qu'il se trouve), et les deux i sont ceux de la culture : tr et az mettent i en majuscule en İ et I en minuscule en ı, toute autre culture met ı en majuscule en I et İ en minuscule en i, et la culture invariante laisse İ et ı tels quels. Une lettre dont la majuscule est arrivée avec Unicode 16, comme ƛ, garde sa casse, comme .NET 10 la garde sur un serveur Linux ou Windows, dont l'ICU est plus ancien ; .NET sur macOS 26 en change la casse, par l'ICU d'Apple. Un argument de culture traverse sous la forme de CultureInfo.CurrentCulture ou d'un null, qui sont la culture de la page, et de CultureInfo.InvariantCulture ; toute autre fait échouer le build (EQ2108) (Depuis 0.2.0-preview.62, là où le navigateur allongeait ß en SS, lisait un sigma final dans son contexte, ne connaissait aucune culture et abandonnait celle que l'appel nommait)
  • ✅ StartsWith, EndsWith
  • ✅ Equals, StartsWith, EndsWith, Contains, IndexOf, LastIndexOf et Replace auxquels on passe une StringComparison, selon cette comparaison, et CompareTo selon la culture actuelle ; une recherche par une comparaison de culture fait échouer le build (EQ1004) (Depuis 0.2.0-preview.60)

System.DateTime, TimeSpan, DateOnly, TimeOnly et DateTimeOffset

Les types temporels reposent sur des types compat précis au tick (ticks de 100 ns, calendrier grégorien proleptique, et non la correspondance avec perte vers new Date() / des millisecondes numériques). Constructeurs, composantes, Add*, arithmétique (-→TimeSpan), comparaisons et .ToString() correspondent tous à .NET, le texte d'une date étant écrit par un seul formateur dans la culture en vigueur (Depuis 0.2.0-preview.61). Un TimeSpan écrit le format qu'on lui donne, via ToString, un trou d'interpolation et un espace réservé de même : c, t et T sa forme constante, g et G ses formes générales avec le séparateur décimal de la culture, un format personnalisé comme hh\:mm jeton par jeton, et un format que .NET refuse, sa FormatException dans les termes de .NET (Depuis 0.2.0-preview.62, là où chaque format écrivait la forme constante) ; les valeurs traversent le fil SSR sous forme de chaînes ISO-8601 / "c" et sont réhydratées dans le type compat. TryParse(text, out var value) de chacun des cinq renvoie true avec la valeur que lit son Parse, ou false avec la valeur par défaut du type dans value pour un texte que Parse refuse et pour un texte null, comme le fait celui de .NET (Depuis 0.2.0-preview.62, là où quatre d'entre eux faisaient échouer la page et celui de DateOnly renvoyait null). Parse et TryParse lisent dans la culture que nomme l'appel, comme le fait .NET : sans fournisseur, avec CultureInfo.CurrentCulture ou un fournisseur null, la culture de la page, et avec CultureInfo.InvariantCulture, la culture invariante. Ils lisent les formes numériques qu'écrit la culture : une date de trois nombres dans l'ordre de son modèle de date courte (01/02/2024 est le 1er février en pt-BR et le 2 janvier en en-US), ses séparateurs à côté de /, - et ., une heure avec son propre séparateur (le 21.30 de fi-FI, où trois nombres courts joints par des points sont une heure et 10.11.12 est 10:11:12) et ses propres indicateurs AM et PM à côté de AM et PM, avant ou après l'heure, l'heure avant ou après la date, une fraction après les secondes avec un point ou une virgule, un fuseau, et la forme ISO (2024-01-15T09:30:00Z). Un TimeSpan est lu comme .NET en lit un sans format : selon combien de nombres il contient et ce qui se trouve entre eux, par rapport au modèle invariant d.hh:mm:ss.fffffff et à celui de la culture, d:hh:mm:ss et son séparateur décimal, donc 10 vaut dix jours, 1:2 une heure et deux minutes, 1:02:03:04 un jour et deux heures, et 24:00:00, qu'aucune horloge ne lit, vingt-quatre jours ; une partie au-delà de sa borne ou une durée au-delà de la plage du type lève l'OverflowException de .NET dans ses termes, et une fraction au-delà de sept chiffres est arrondie comme .NET l'arrondit (Depuis 0.2.0-preview.62, là où seul [-][d.]hh:mm:ss[.f] était lu et aucune partie n'était vérifiée, donc 00:60:00 valait une heure). Ainsi une valeur écrite avec ToString() se relit, et ce qui est refusé l'est dans les termes de .NET. Toute autre culture fait échouer le build (EQ2108), de même que des styles autres que l'espace blanc et un span (EQ1004) ; ParseExact, TryParseExact et GetDateTimeFormats ne sont pas traduits (EQ2004). Convert.ToDateTime lit un texte comme le fait DateTime.Parse, un texte null comme MinValue, un objet selon ce qu'il contient et une date comme elle-même, et une valeur d'un autre type lève l'InvalidCastException de .NET ; un char contenu dans un objet est lu comme un texte d'un seul caractère, puisqu'une valeur boxée ne porte pas son type (#552) (Depuis 0.2.0-preview.62, là où chaque lecteur lisait les formes invariantes, un fournisseur faisait échouer la page et Convert.ToDateTime renvoyait le texte). Le nom d'un mois, une date de deux nombres, une heure sans date et les calendriers autres que le grégorien ne sont pas encore lus (#807). Un constructeur qui prend un Calendar fait échouer le build (EQ1004), puisque le navigateur n'a aucun des calendriers de .NET, y compris celui de DateOnly (Depuis 0.2.0-preview.62, là où il abandonnait le calendrier). Voir le tableau Couverture et conformité de la BCL .NET ci-dessous pour le détail par type.

System.Collections.Generic

  • ✅ List<T> → tableau JavaScript [] ; Remove renvoie le bool que renvoie .NET et compare comme le fait EqualityComparer<T>.Default, un record par valeur, un tuple élément par élément et NaN par NaN (Depuis 0.2.0-preview.59). IndexOf, LastIndexOf, Contains et Remove, Array.IndexOf et Array.LastIndexOf, et le Contains de LINQ comparent selon cette seule comparaison, choisie d'après le type de l'élément : un NaN, un record, un tuple et un decimal trouvés par valeur, une classe par son propre Equals, le membre tableau d'un tuple par référence, comme le compare ValueTuple.Equals. Sort et Array.Sort sont le tri introspectif de .NET, instable comme l'est celui de .NET, donc les éléments égaux atterrissent là où atterrissent ceux de .NET, selon le comparateur par défaut du type (les NaN d'un double en premier), une Comparison<T>, les six d'un StringComparer, Comparer<T>.Create ou un comparateur à vous, avec les exceptions de .NET ; tout autre comparateur fait échouer le build (EQ2007). BinarySearch renvoie le complément du point d'insertion en cas d'absence, Find et FindLast la valeur par défaut du type de l'élément, RemoveAll combien il en a retiré, CopyTo écrit dans le tableau qu'on lui passe, et les plages de FindIndex, FindLastIndex, IndexOf et LastIndexOf sont vérifiées dans les termes de .NET (Depuis 0.2.0-preview.61)
  • ✅ Le Add et le Clear d'ICollection<T> ajoutent et vident comme le fait la collection derrière l'interface au moment où l'appel s'exécute : une liste ajoute à la fin, un ensemble ajoute une valeur qu'il ne contient pas, une liste chaînée ajoute en dernier, un dictionnaire ajoute la paire et refuse une clé déjà présente dans les termes de .NET, et un type à vous exécute ses propres Add et Clear, y compris l'initialiseur de collection d'un membre (Items = { 1, 2 }). Ils étaient le push et le splice d'un tableau, qui manquent à un ensemble, une liste chaînée et un dictionnaire, avant 0.2.0-preview.61 (Depuis 0.2.0-preview.61)
  • ✅ Dictionary<TKey, TValue>, IDictionary et IReadOnlyDictionary → la classe dictionnaire du runtime ($eq.collections.dictionary), qui détient ses entrées par emplacement comme celle de .NET : elle énumère dans l'ordre d'insertion, et une entrée ajoutée après une suppression prend la place de celle supprimée, la dernière libérée d'abord, donc d[3] = 30; d[1] = 10; énumère 3,1. Chaque clé garde son type (une clé int revient sous forme de nombre, un long sous forme de BigInt, un char sous forme de char) et est trouvée comme la trouve le comparateur par défaut du type de la clé : par valeur pour un record, un struct, un tuple, un decimal ou une date, par identité pour un nombre, une chaîne, un char, un bool, un enum ou un Guid, et par l'égalité propre de la valeur là où le type ne tranche pas (object, une interface, un paramètre de type, une classe), donc deux records égaux sous object sont une seule clé ; l'élément tableau d'une clé tuple par référence (Depuis 0.2.0-preview.61). new Dictionary<K, V>(other) copie, un foreach (var (k, v) in d) qui déconstruit fonctionne sur chaque dictionnaire, y compris les triés, TryAdd, ContainsValue, Remove(key, out value), Keys.Contains et Keys.Count répondent comme ceux de .NET, et ToDictionary construit la même classe. Add, un initialiseur de collection et le constructeur qui copie des paires refusent une clé déjà présente avec l'ArgumentException de .NET, dans les termes de chaque collection, tandis que l'indexeur, le [key] = value d'un initialiseur d'objet et TryAdd remplacent ou déclinent, et new KeyValuePair<K, V>(key, value) et KeyValuePair.Create construisent la paire que produit un dictionnaire (Depuis 0.2.0-preview.60). Une clé ajoutée pendant qu'un foreach parcourt le dictionnaire termine le parcours avec une InvalidOperationException, comme le fait l'énumérateur de .NET, tandis qu'un écrasement, une suppression et Clear laissent le parcours se poursuivre, et un dictionnaire trié refuse toute modification. Un dictionnaire dans l'état rendu par le serveur, dans le résultat ou les arguments d'une Server Action, ou dans la charge utile d'un topic traverse sous forme de ses paires [key, value] et arrive sous forme de la classe, ses clés dans leur type, dans l'ordre où il énumérait, y compris les clés entières : un objet JSON aurait listé celles-ci en premier et par ordre croissant (Depuis 0.2.0-preview.61, #437). Un comparateur qui demande ce que fait déjà le navigateur (null, EqualityComparer<T>.Default, StringComparer.Ordinal) construit le dictionnaire, et un dictionnaire trié garde l'ordre qu'il demande (Depuis 0.2.0-preview.61), tandis qu'un comparateur qui change l'égalité, comme StringComparer.OrdinalIgnoreCase, fait échouer le build avec EQ2007. Depuis 0.2.0-preview.60. Ses Keys et Values sont des vues vivantes, comme celles de .NET : une vue lue après que le dictionnaire a changé montre le changement, une clé ajoutée pendant qu'une vue est parcourue termine le parcours, le Contains propre des clés trouve une clé comme le fait le dictionnaire, et chaque élément revient tel quel, y compris un délégué. Une requête LINQ sur une vue, comme sur toute source, la lit entièrement avant son premier callback (#685). EnsureCapacity renvoie le nombre premier sur lequel .NET s'arrête, un constructeur de capacité et une copie dimensionnent le dictionnaire comme ceux de .NET, et TrimExcess tasse les entrées pour que la clé suivante aille après les autres (Depuis 0.2.0-preview.61).
  • ✅ HashSet<T>, ISet<T> et IReadOnlySet<T> → l'ensemble du runtime ($eq.collections.hashSet), construit partout où l'on en crée un (un constructeur, un initialiseur, une expression de collection, ToHashSet, une valeur qui a traversé depuis le serveur). Il détient ses éléments par emplacement dans la table où le dictionnaire du runtime garde ses clés : un élément ajouté après une suppression prend l'emplacement de celui supprimé, le dernier libéré d'abord, donc { 3, 1 } moins 3, plus 5, énumère 5,1, et chaque opération ensembliste (UnionWith, IntersectWith, ExceptWith, SymmetricExceptWith, RemoveWhere), une copie et TrimExcess laissent les emplacements là où les laissent ceux de .NET. Un élément est trouvé comme le trouve le comparateur par défaut de son type, la règle que suivent les clés d'un dictionnaire : une date, un decimal, un tuple et un record par valeur, NaN égal à NaN. Add indique si l'élément était nouveau, les prédicats d'ensemble, CopyTo et EnsureCapacity répondent comme ceux de .NET, et un élément ajouté pendant un foreach termine le parcours avec une InvalidOperationException. TryGetValue n'est pas encore pris en charge et fait échouer le build (EQ1004) ; un comparateur qui change l'égalité le fait aussi échouer (EQ2007) (Depuis 0.2.0-preview.61).
  • ✅ ContainsKey / TryGetValue ne répondent jamais pour "constructor", "toString" ni pour quoi que ce soit d'autre sur Object.prototype. Depuis 0.2.0-preview.15 ; GetValueOrDefault aussi, depuis 0.2.0-preview.58.
  • ✅ TryGetValue et GetValueOrDefault répondent à une absence par default(TValue) sur chaque dictionnaire, y compris les triés : 0 pour un int, false pour un bool, null pour une référence, donc d.TryGetValue(k, out var n); d[k] = n + 1; compte. Le récepteur, la clé et une valeur par défaut explicite sont chacun évalués une fois, dans l'ordre d'écriture ; un out _ ne reçoit rien, et un champ ou un élément out est écrit là où C# l'écrit. La forme statique, CollectionExtensions.GetValueOrDefault(d, key), lit le dictionnaire que passe son premier argument. Depuis 0.2.0-preview.58.
  • ✅ Queue<T>, Stack<T>, LinkedList<T>, SortedSet<T>, SortedDictionary<K,V>, SortedList<K,V> → collections compat du runtime sous $eq.collections.*. Une collection triée ordonne comme Comparer<T>.Default ordonne son type d'élément : une chaîne dans la culture actuelle, un double avec son NaN en premier, un enum par sa valeur, un decimal, une date et un type à vous qui implémente IComparable par son CompareTo, et StringComparer.Ordinal par unité de code. Le Contains d'une file et d'une pile trouve un decimal, une date ou un record par sa valeur (Depuis 0.2.0-preview.60). Chacune écrit le JSON qu'écrit .NET, un tableau dans l'ordre où elle énumère, une pile depuis son sommet, donc un argument de Server Action atteint le serveur sous forme de la collection (Depuis 0.2.0-preview.61). Le CopyTo d'une file et d'une pile, et les CopyTo, UnionWith, IntersectWith, ExceptWith, SymmetricExceptWith, IsSubsetOf, IsProperSubsetOf, IsSupersetOf, IsProperSupersetOf, Overlaps, SetEquals et RemoveWhere d'un ensemble trié répondent comme ceux de .NET, leurs refus dans les termes de .NET, et de deux éléments qu'un ensemble trié trouve égaux (1.0m et 1.00m) une union et une intersection gardent celui que garde .NET. Une source null passée au constructeur d'une liste, d'une file, d'une pile, d'une liste chaînée, d'un ensemble trié ou de n'importe quel dictionnaire lève l'ArgumentNullException que lève .NET, en nommant collection ou dictionary, là où le navigateur en construisait un vide (une liste levait un TypeError), une capacité négative d'une file ou d'une pile est refusée, et une chaîne copiée dans l'une d'elles est lue par ses chars UTF-16 (Depuis 0.2.0-preview.62, #723, #726). Un foreach sur une file, une pile, une liste chaînée ou un ensemble trié s'arrête à l'étape qui suit la modification de la collection, y compris celle qui suit le dernier élément, avec l'InvalidOperationException que lève .NET, dans les termes de .NET : une modification est ce que .NET compte comme telle, y compris le Add par un ensemble trié d'un élément qu'il contient et son Remove d'un élément qu'il ne contient pas, et une valeur écrite dans le nœud d'une liste chaînée n'en est pas une. Le Reverse() d'un ensemble trié le parcourt depuis son plus grand élément et est refusé de même (Depuis 0.2.0-preview.62, #765). Le TryDequeue et le TryPeek d'une file, le TryPop et le TryPeek d'une pile et le TryGetValue d'un ensemble trié répondent via leur out comme ceux de .NET, la valeur par défaut du type là où il n'y a rien ; le Min et le Max d'un ensemble trié vide sont la valeur par défaut de son type d'élément, comme ceux de .NET ; les Capacity, EnsureCapacity et TrimExcess d'une file et d'une pile gardent la capacité de .NET, augmentée selon la règle propre de chacune et dimensionnée à partir d'une séquence comme .NET la dimensionne, au compte d'une ICollection<T> et en doublant à partir de quatre pour tout le reste ; les Find, FindLast, AddAfter, AddBefore et CopyTo d'une liste chaînée, et les surcharges qui prennent un nœud, lient comme ceux de .NET, un nœud retiré ou vidé n'appartenant à aucune liste, avec les refus de .NET dans ses termes ; et new LinkedListNode<T>(value) construit un nœud. Tout autre membre des cinq, dont le Comparer et le GetViewBetween d'un ensemble trié et un GetEnumerator explicite, fait échouer le build (EQ2004), là où chaque membre compilait et ceux qui manquaient au runtime étaient un TypeError dans le navigateur. Une file ou une pile construite à partir d'une requête LINQ prend le compte de la requête comme capacité, puisque la requête a été lue entièrement (#685) (Depuis 0.2.0-preview.62, #866). Le RemoveWhere d'un ensemble trié interroge son prédicat dans l'ordre de l'ensemble, là où .NET parcourt son arbre en largeur d'abord (#766).

System.Threading.Tasks

  • ✅ Task, Task<T> → Promise.
  • ✅ Task.Delay → enveloppe de setTimeout.
  • ✅ Task.WhenAll, Task.WhenAny.
  • ⚠️ Task.Run s'exécute sur le thread principal (microtâche), PAS sur un thread d'arrière-plan.

Autres utilitaires

  • ✅ Console.WriteLine → console.log.
  • ✅ Math.* (Min, Max, Abs, Round, etc.) → Math.*.
  • ✅ Guid (NewGuid, Empty, Parse, TryParse, new Guid(text)) → crypto.randomUUID() pour un nouveau, et son texte canonique, le format D en minuscules, pour un Guid lu depuis un texte dans n'importe quel format que lit .NET, donc deux écritures d'un même Guid sont une seule valeur pour ==, un dictionnaire et un ensemble (Depuis 0.2.0-preview.60).
  • ✅ Regex → RegExp JavaScript.

🌐 Paquets de l'écosystème

eQuantic.UI.Lucide / Heroicons / RadixIcons / TablerIcons / Phosphor / SimpleIcons / BootstrapIcons / Iconoir / ...

Objectif : jeux d'icônes complets. Contenu :

  • Logique de résolution SVG
  • Composants d'icône spécialisés
  • Un IconGlyph généré par icône, sous forme de propriété pour que le trimmer puisse abandonner ceux que personne ne nomme
Paquet Objectif Statut
eQuantic.UI.Charts Graphiques écrits une fois, dessinés par chaque réalisateur. ✅
eQuantic.UI.Lottie Animations haute performance. ✅
eQuantic.UI.Image Optimisée (Lazy/Blur/Priority). ✅

🧩 Modèles du framework

Modèle Interface Description Statut
Gestion des métadonnées IHandleMetadata Balises SEO et éléments du head. ✅
Gestion des assets IRequireAssets Injection dynamique de script/style. ✅
Server Actions [ServerAction] RPC sécurisé du client vers le serveur. ✅
Composants composés N/A Modèles sémantiques de sous-composants. ✅
Bibliothèque de classes partagée ProjectReference Un type d'un projet référencé qu'une page utilise est un module du bundle : une classe statique, un record, un composant et ses chaînes. Voir Flux de build (Depuis 0.2.0-preview.62). ✅

⚠️ Limitations et réserves

  1. Code bloquant :

    • ❌ .Wait(), .Result sur les Tasks ne sont PAS pris en charge. Vous devez utiliser await. Bloquer le thread principal fige l'interface du navigateur.
  2. Réflexion :

    • ❌ System.Reflection n'est en grande partie pas pris en charge.
    • ✅ typeof(T).Name et nameof(...) sont des constantes prises en charge.
  3. Système de fichiers :

    • ❌ System.IO (File, Directory) dans un composant client est une erreur de build (EQ2112) : le navigateur n'a pas de système de fichiers à lui donner.
    • Utilisez les Server Actions pour gérer les opérations sur les fichiers.
  4. Nombres :

    • ✅ int/double/float correspondent aux nombres JS. La division entière tronque (Math.trunc), et une division ou un reste par zéro, ou MinValue par -1 sur int et long, lève l'exception que lève .NET (Depuis 0.2.0-preview.58) ; un float est arrondi en simple précision partout où il est produit, exactement comme RyuJIT l'arrondit, y compris l'affectation composée et l'incrément d'une cible float? (Depuis 0.2.0-preview.58), et Math.Round est celui de .NET : un point milieu exact, chaque mode MidpointRounding.
    • ✅ decimal est exact de bout en bout : compilé vers le type compat Decimal du runtime (mantisse BigInt + échelle), donc 0.1m + 0.2m == 0.3m est true. Les decimals traversent aussi le fil sous forme de chaînes JSON (EqJson) et sont réhydratés en Decimal côté client (la valeur par défaut Decimal du champ pilote une coercition qui préserve le type, voir hydrate-value.ts), donc les decimals fournis par le serveur gardent leurs 28 chiffres au lieu d'être arrondis via un double. Un decimal lu depuis un texte (decimal.Parse/TryParse, Convert.ToDecimal("…")) suit la grammaire de .NET sous le NumberStyles que nomme l'appel et arrondit comme arrondit l'analyseur de .NET. Il est lu dans la culture invariante, et l'appel le dit avec CultureInfo.InvariantCulture : sans culture le build avertit (EQ2110) et avec une autre il échoue (EQ2108), puisque le navigateur ne lit le texte d'aucune autre culture. Un double ou un float converti en decimal garde les 15 ou 7 chiffres significatifs de .NET : (decimal)(0.1 + 0.2) est 0.3. Depuis 0.2.0-preview.58.
    • ✅ Une constante est sa valeur dans son type C# : decimal.MaxValue, MinValue, Zero, One et MinusOne sont des decimals exacts, écrits decimal ou Decimal, et une constante long comme TimeSpan.TicksPerSecond est un BigInt, donc elle rencontre n'importe quel autre long. Un littéral decimal est sa valeur, donc 1_000.5m fonctionne, et la valeur par défaut d'un paramètre, remplie pour un argument qu'un argument nommé saute, garde son type : un decimal, un long, un char, une chaîne contenant un guillemet. Une constante atteinte par son nom nu sous un using static est écrite de la même façon. Une constante d'un type enum est une valeur de cet enum, pas son nombre, et une constante decimal correspond par valeur dans un motif, une branche de switch ou un case (d is 1m, case decimal.One:). Depuis 0.2.0-preview.60.
    • ✅ Tout autre type numérique lit aussi le texte comme le fait .NET : les int, uint, long, ulong, short, ushort, byte, sbyte, double et float Parse/TryParse, et Convert.ToInt32("…") et ses frères, lisent le texte entier sous le NumberStyles propre au type (Integer, ou Float | AllowThousands pour un réel) ou celui que nomme l'appel. Un texte qui n'est pas un nombre lève une FormatException ("12abc", "1e3" comme int, "0x1F" sans HexNumber), une valeur que le type ne peut pas contenir une OverflowException, un TryParse échoué laisse 0, un style hex ou binaire lit les bits du type ("FFFFFFFF" est -1 pour un int), le texte d'un long est un BigInt, et un float est arrondi une fois, à partir des chiffres. La culture est celle de decimal : EQ2110 sans culture, EQ2108 avec toute autre que CultureInfo.InvariantCulture. Depuis 0.2.0-preview.58.
    • ✅ Le texte d'un double et d'un float est celui de .NET : les chiffres les plus courts qui se relisent comme la valeur, notation fixe de 1e-4 jusqu'à 1e16 pour un double et 1e8 pour un float, scientifique au-delà (1E+17, 1E-05), -0 pour un zéro négatif et Infinity pour un infini, de même via ToString(), ToString(CultureInfo.InvariantCulture), une concaténation et une interpolation ; un float? ou un double? null n'écrit rien. Depuis 0.2.0-preview.58.
    • ✅ Le texte d'un nombre sans spécificateur est celui de la culture, partout où C# l'écrit : une concaténation, un trou d'interpolation simple ou aligné, ToString(), Convert.ToString, string.Format("{0}"), string.Concat, string.Join, le Append et l'Insert d'un StringBuilder, et le texte d'un record, comme .NET les écrit dans la culture en vigueur : -1,5 en pt-BR, un signe moins que la culture écrit à sa façon (le −5 de sv-SE), un float avec ses propres chiffres, un decimal avec son échelle, et jamais -0 pour un entier que JavaScript détient comme un zéro négatif. Une valeur écrite pour une machine le dit avec ToString(CultureInfo.InvariantCulture), qui garde sa forme dans chaque culture. Depuis 0.2.0-preview.61.
    • ✅ Via le formateur aussi : un float formaté avec un alignement ou un spécificateur ($"{f,8}", $"{f:G}"), ToString("G"), ToString("R") et un float passé à string.Format écrivent ses propres chiffres, G et R dans la notation de .NET (1E+21). Un espace réservé de string.Format s'aligne ({0,5}, {0,-5}), un null dans un trou aligné est complété, et un tableau params passé en entier, y compris un string[] ou une expression de collection, vaut ses valeurs. Le fournisseur de string.Format suit la politique de ToString : CultureInfo.InvariantCulture formate de façon invariante, y compris les modèles d'une date et le ¤ générique d'une devise, CultureInfo.CurrentCulture ou un null comme le fait un appel sans fournisseur, et tout autre est EQ2108. Le ToString() d'un bool est True ou False. Une limite : un float boxé dans un object à travers lequel le compilateur ne peut pas voir (object o = 0.1f; $"{o}", ou un object[] construit à l'exécution) est le simple nombre sous-jacent. Depuis 0.2.0-preview.58.
    • ✅ Un nombre s'imprime via son spécificateur comme .NET l'imprime : E et e écrivent les chiffres de la mantisse, le signe de l'exposant et au moins trois chiffres d'exposant ((12345.0).ToString("E2") est 1.23E+004). F, N, P, C, G et un format personnalisé arrondissent la valeur EXACTE du nombre, y compris la valeur binaire d'un double ((0.1).ToString("F20") est 0.10000000000000000555) et chaque chiffre d'un long et d'un decimal (9007199254740993L.ToString("N0") est 9,007,199,254,740,993), une moitié exacte étant arrondie au pair pour un double et un float et en s'éloignant de zéro pour un decimal et un entier ((2.5).ToString("F0") est 2, (2.5m).ToString("F0") est 3, 125.ToString("E1") est 1.3E+002). N, F, C et P sont mis en forme à partir du propre NumberFormatInfo de la culture, qui voyage avec chaque page : ses modèles, symboles, séparateurs, tailles de groupe et chiffres par défaut ((1234.5).ToString("N") est 1,234.500 en en-US sur un serveur dont le .NET lit ICU), en chiffres ASCII dans chaque culture, y compris celle d'ar-EG, comme .NET les écrit (Depuis 0.2.0-preview.61). Une culture vers laquelle on bascule sans serveur à interroger est mise en forme par Intl.NumberFormat v3 (ES2023) : Chrome et Edge 106, Firefox 116 et Safari 15.4 l'ont. X et B (binaire) écrivent un entier négatif à la largeur de son type (((short)-1).ToString("X") est FFFF, celui d'un int est FFFFFFFF). Un format personnalisé est dessiné comme .NET le dessine : sections (0.00;(0.00);'zero'), texte entre guillemets et échappé, % et ‰, exposants (0.00E+00), et les virgules avant le point qui divisent par mille (#,##0,). Une précision peut dépasser 100 chiffres, comme .NET le permet. Les nombres sont groupés comme .NET les groupe (es-ES écrit 1.234) et signés avec le moins de la culture (le − de sv-SE), et un spécificateur que le type n'accepte pas lève une exception, comme dans .NET ((2.5).ToString("D")). Depuis 0.2.0-preview.59. Un double sous D, X ou B lève une exception quoi qu'il contienne, nint et nuint sont des entiers 64 bits, et le signe pour mille (le ؉ d'ar-SA), les signes d'un exposant et le signe pour cent sont ceux propres à la culture. Depuis 0.2.0-preview.61.
    • ✅ long/ulong sont désormais exacts : compilés en BigInt JS via le helper compat long. 9007199254740993L + 1L vaut 9007199254740994 (un simple nombre JS arrondirait à …992). Les littéraux deviennent des BigInt (5L → 5n), et de même un littéral sans suffixe que seul un long peut contenir (637000000000000000 → 637000000000000000n, Depuis 0.2.0-preview.58) ; les opérandes arithmétiques/de comparaison sont enveloppés dans long() (qui convertit number/string → bigint) pour que les expressions mixtes ne lèvent jamais d'exception. Sur le fil, les entiers 64 bits traversent sous forme de chaînes JSON (Server Actions + état SSR, via EqJson) pour que les valeurs au-delà de 2^53 survivent à l'aller-retour. Les autres suffixes de type numérique (1.5f, 100u) sont supprimés.
  5. Sécurité des threads :

    • Comme JS est single-threaded, les instructions lock disparaissent à la compilation (ignorées).
    • Thread.Sleep n'est pas pris en charge (utilisez Task.Delay).

🧪 Couverture et conformité de la BCL .NET

La fidélité de la transpilation est garantie par un harnais de conformité (tests/eQuantic.UI.Conformance.Tests) : chaque cas exécute la même expression C# de deux façons, transpilée en JS (exécutée via le Bun embarqué) et évaluée directement dans .NET (Roslyn scripting), et vérifie que les résultats sont identiques. La suite compte ses cas (plus d'un millier au moment de la rédaction, et en hausse à chaque construction) ; tous sont au vert.

(Le plus récent : chaque dictionnaire via la classe dictionnaire du runtime, $eq.collections.dictionary.) Cela couvre à la fois les expressions et les blocs d'instructions (flux de contrôle : if/for/foreach/while/switch/ try-catch-finally/fonctions locales ; le bloc s'exécute dans une IIFE et sa valeur renvoyée est comparée).

Chaque construction se résout par l'un de trois mécanismes (voir docs/DOTNET-COVERAGE-PROGRAM.md) :

  1. Stratégie native : du JS idiomatique quand le runtime a un équivalent.

  2. Helper compat .NET du runtime : une sémantique .NET fidèle là où JS n'en a pas. Le transpileur émet ces helpers sous un unique espace de noms $eq (organisé par domaine), apporté par un seul import par module, import { $eq } from "@equantic/runtime" (résolu par l'import map de la page), au lieu de N imports de helpers épars, et $eq.* ne peut jamais entrer en collision avec un identifiant de l'utilisateur dans la portée générée :

    • $eq.num : dec (Decimal exact), decParse/decTryParse/decConvert (un decimal depuis un texte ou Convert), intParse/intTryParse/intConvert et realParse/realTryParse/realConvert (un entier, un double ou un float depuis un texte ou Convert), decFromDouble/decFromSingle (un nombre binaire vers un decimal), long (Int64 via BigInt), double/single (le texte d'un nombre)
    • $eq.math : round (arrondi bancaire)
    • $eq.convert : toBase64String, toBase64CharArray, toHexString, toHexStringLower (l'encodage d'un tableau, comme l'écrit le Convert de .NET)
    • $eq.text : format (mise en forme de nombres/chaînes), stringBuilder
    • $eq.time : dateTime, timeSpan, dateOnly, timeOnly, dateTimeOffset
    • $eq.enums : text, parse, tryParse, zero, names, values, isDefined (un enum tel que .NET le lit, à partir de la forme qu'écrit eqc à chaque appel)
    • $eq.collections : queue (FIFO), stack (LIFO), dictionary (détenu par emplacement comme celui de .NET, clés trouvées par identité ou par valeur), linkedList, sortedSet/sortedDictionary/sortedList (triés par clé)
    • $eq.nullable : arith, cmp, unary (opérateurs Nullable<T> levés : arithmétique et opérateurs unaires qui propagent null, relationnels faux sur null)
    • $eq.equals : égalité structurelle (par valeur) pour les records/structs/tuples (sous-tend ==, .Equals, Contains, Distinct)
    • $eq.hash : of (GetHashCode(), qui s'accorde avec $eq.equals, donc des valeurs que Equals trouve égales ont des hash égaux, y compris un type valeur du vocabulaire comme Point ou TypeStyle, et un type qui le redéfinit répond le sien), instance (le même appel sur une référence, refusé quand elle est null comme .NET lève NullReferenceException, tandis qu'un Nullable<T> vide répond 0), group (le groupe de méthodes value.GetHashCode, qui prend son récepteur une fois et refuse un récepteur null là où le délégué est créé), combine (HashCode.Combine, par ses paramètres même quand ils sont nommés) et identity (le object.GetHashCode d'une classe qui ne le redéfinit pas, et celui d'un tableau, quelle que soit la façon dont ses éléments changent). Les propres nombres de .NET ne sont pas stables d'un processus à l'autre, donc le navigateur garde le contrat, jamais le nombre du serveur (Depuis 0.2.0-preview.60)
    • $eq.css : styleBuilder, classBuilder, joinClasses, whenClass (le sous-système de style)
  3. Échec sur le non pris en charge : une construction sans représentation JS possible lève désormais une erreur de build (avec un code EQ stable) au lieu d'être émise telle quelle en silence. Deux couches :

    • UnsupportedConstructStrategy (EQ2001) : les intrinsèques de référence typée (__makeref, __refvalue, __reftype), les types pointeur, les pointeurs de fonction.
    • goto/goto case/goto default (EQ2002) : pas d'équivalent JS ; restructurez avec des boucles/conditions. (Les blocs unsafe/fixed/lock se déroulent en leur corps, lock étant un no-op single-threaded, et une étiquette nue se réduit à son instruction interne.)
    • La frontière client/serveur SemanticValidator (EQ21xx) : les appels à System.IO, System.Net.Http, System.Net.Sockets, EF Core / System.Data, le threading de l'OS (Thread/Monitor/Mutex), Process, InteropServices (P/Invoke), Reflection.Emit depuis un composant client. (System.Threading.Tasks n'est pas interdit : async correspond à Promise.) La correction consiste à déplacer l'appel dans un [ServerAction].

    Toute autre construction qui ne rencontre aucune stratégie est une erreur (EQ1001 pour une expression, EQ1002 pour une instruction, EQ1003 pour tout le reste) et rien n'est émis pour elle. Elle passait auparavant telle quelle avec un avertissement, ce qui signifiait un build qui réussissait et une page qui levait une exception — refuser ce qu'il ne peut pas représenter est tout l'intérêt. Les diagnostics s'impriment sous la forme canonique de MSBuild, donc ils font échouer dotnet build et l'IDE en fait des liens. La liste complète se trouve dans Diagnostics.

Domaine Statut Notes
Arithmétique / bit à bit / comparaison ✅ la division entière tronque ; %, les décalages, `&
Math.* / MathF.* ✅ la même table que double.* / float.*, avec les compositions propres de .NET ; Round est celui de .NET (point milieu exact, chaque mode)
decimal ✅ exact via Decimal (littéraux + + - * / % == != < > <= >=, le reste avec le signe du dividende, Depuis 0.2.0-preview.58 pour %) ; les constantes MaxValue, MinValue, Zero, One et MinusOne (Depuis 0.2.0-preview.60) ; decimal.Parse/TryParse selon la grammaire et l'arrondi de .NET, y compris le NumberStyles que nomme un appel ; Convert.ToDecimal et les casts depuis double/float aux 15 et 7 chiffres de .NET
Constantes numériques ✅ int.MaxValue, double.Epsilon, … → littéraux
Analyse / Convert.* ✅ Parse/TryParse sur chaque type numérique, et Convert.ToInt32/ToInt64/ToDouble/… d'un texte, selon la grammaire de .NET (Depuis 0.2.0-preview.58) ; bool.Parse/TryParse et Convert.ToBoolean d'un texte comme le lit le Boolean.TryParse de .NET : "True" ou "False" dans n'importe quelle casse, débarrassé des espaces blancs et des NUL, et les exceptions de .NET, et Convert.ToBoolean d'un nombre selon qu'il n'est pas nul, d'un bool comme lui-même, et d'un char ou d'une date comme l'InvalidCastException de .NET (Depuis 0.2.0-preview.59) ; Convert.ToString/… ; Convert.ToDateTime d'un texte comme le lit DateTime.Parse dans la culture que nomme l'appel, un texte null comme MinValue (Depuis 0.2.0-preview.62, là où il renvoyait le texte) ; Convert.ToBase64String, ToBase64CharArray, ToHexString et ToHexStringLower écrivent l'encodage de .NET d'un byte[], y compris son décalage, sa longueur et ses sauts de ligne tous les 76 caractères, et refusent ce que .NET refuse dans ses termes ; un span qui n'est pas une expression de collection fait échouer le build (EQ1004) (Depuis 0.2.0-preview.62, là où chacun écrivait le 1,2,255 du tableau)
Chaînes ✅ Substring/IndexOf/Replace/Split/Pad/Trim/Trim(char)/Concat/Format/Join/IsNullOrEmpty/IsNullOrWhiteSpace ; Trim(chars) et ses deux moitiés lisent la chaîne puis les caractères une fois, y compris un await parmi eux, et l'absence de caractères (un tableau vide ou null) supprime les espaces blancs, comme le fait .NET (Depuis 0.2.0-preview.60) ; Trim(), ses deux moitiés, IsNullOrWhiteSpace et un Split() nu lisent les espaces blancs de .NET, là où ceux de JavaScript laissent U+0085 et prennent U+FEFF (Depuis 0.2.0-preview.58) ; les propres Equals, StartsWith, EndsWith, Contains, IndexOf et LastIndexOf (avec un début et un compte) d'une chaîne et Replace auxquels on passe une StringComparison répondent selon cette comparaison, écrite en entier ou conservée dans une variable : OrdinalIgnoreCase garde la casse de .NET (le signe Kelvin, le s long et le i sans point ne sont pas k, s et I, et une paire de substitution est lue comme son point de code), un début ou un compte hors de la chaîne lève l'exception de .NET, Replace écrit son remplacement comme du texte, et CompareTo ordonne selon la culture actuelle, comme le fait string.Compare (Depuis 0.2.0-preview.60). IndexOf et LastIndexOf d'une chaîne à partir d'un début, et pour un compte, sans comparaison, s'arrêtent à leur compte, lisent le début de LastIndexOf comme le dernier char qu'une correspondance peut couvrir, et lèvent l'exception de .NET pour une plage hors de la chaîne ou une valeur null (Depuis 0.2.0-preview.62, là où le compte était abandonné et LastIndexOf("bc", 4) renvoyait la correspondance commençant à 4). IndexOf(char, startIndex[, count]) et LastIndexOf(char, startIndex[, count]) cherchent dans cette plage et lèvent l'exception de .NET pour un début ou un compte hors de la chaîne : le début de LastIndexOf se tient sur un char de la chaîne, et une chaîne vide renvoie -1 (Depuis 0.2.0-preview.61, là où le début était borné et le compte abandonné). Une RECHERCHE par une comparaison de culture, ou avec une CultureInfo, fait échouer le build (EQ1004), parce que le navigateur n'a pas de recherche sensible à la culture : cherchez par Ordinal ou OrdinalIgnoreCase, ou comparez des chaînes entières avec Equals ou string.Compare. Les surcharges sans comparaison cherchent de façon ordinale dans le navigateur et selon la culture actuelle dans .NET (#532).
char.* ✅ ToUpper/ToLower/IsDigit/IsLetter/IsWhiteSpace/… (tenant compte d'Unicode ; IsWhiteSpace est l'ensemble de .NET, U+0085 dedans et U+FEFF dehors, Depuis 0.2.0-preview.58 ; ToUpper, ToLower et leur paire invariante changent la casse d'un char comme celle d'une chaîne d'un caractère, dans la culture que nomme l'appel, et une culture null lève l'ArgumentNullException de .NET, Depuis 0.2.0-preview.62)
StringBuilder ✅ type compat : Append(y compris bool→"True"/"False")/AppendLine("\n")/Insert/Remove/Replace/Clear/Length/ToString. Les surcharges qui comptent ou prennent une plage écrivent ce qu'écrit .NET, Append('x', 3), Append(text, start, count), Insert(i, text, count), Replace sur une plage et ToString(start, length), un char[] comme ses caractères et un null comme rien, et chaque refus est celui de .NET (Depuis 0.2.0-preview.61). AppendFormat et AppendJoin ajoutent ce qu'écrivent string.Format et string.Join, Capacity suit les blocs que .NET alloue à travers chaque modification, MaxCapacity et EnsureCapacity répondent, l'indexeur Chars lit et écrit un char, Length coupe le texte ou le complète avec \0, Equals(StringBuilder) compare le texte et CopyTo le copie, chacun refusant ce que .NET refuse ; un Append($"…") ou AppendLine($"…") interpolé ajoute chaque partie à son tour, comme le fait le handler de .NET, donc un trou qui lit le builder voit les parties qui le précèdent, un argument nommé est lié à son paramètre, et un texte plus long que ce que peut contenir la chaîne du navigateur est l'OutOfMemoryException de .NET ; un fournisseur autre que la culture actuelle sur Append, GetChunks et les surcharges span font échouer le build (Depuis 0.2.0-preview.61)
CancellationTokenSource / CancellationToken ✅ ceux du runtime, comme ceux de .NET : une source (avec ou sans délai), son Token, IsCancellationRequested, Cancel, CancelAfter, Dispose et CreateLinkedTokenSource ; le None d'un jeton (et default, et new CancellationToken(bool)), IsCancellationRequested, CanBeCanceled, ThrowIfCancellationRequested (une OperationCanceledException, "The operation was canceled.") et le Register d'un callback, dont l'enregistrement répond à Dispose et à Unregister. Les callbacks s'exécutent une fois, le dernier enregistré en premier, un callback enregistré tardivement s'exécute aussitôt et reçoit en retour l'enregistrement par défaut (son jeton est None), et ceux qui lèvent une exception sont rassemblés dans une AggregateException, comme dans .NET. Une méthode des trois passée comme callback (token.Register(source.Cancel)) garde son récepteur, et une source liée, une fois libérée, lâche les jetons qu'elle suit. Une source libérée lâche aussi ses callbacks, un enregistrement se désinscrivant encore une fois après elle, et new CancellationTokenSource(delay, timeProvider) fait échouer le build avec EQ2004 : la source du runtime est minutée par l'horloge réelle. Un délai est accepté jusqu'à 4294967294 ms, comme .NET l'accepte, et un mois est attendu jusqu'au bout bien que le minuteur d'un navigateur tienne environ 24,8 jours. default(CancellationTokenRegistration) est l'enregistrement de rien, dont le jeton est None. Tout autre membre des trois fait échouer le build avec EQ2004 (Depuis 0.2.0-preview.61)
LINQ ✅ Where/Select/SelectMany/Where-Select(indexé)/OrderBy/Distinct(By)/GroupBy/ToDictionary/ToLookup/Zip/Chunk/MinBy/MaxBy/Take(While)/Skip(While)/Aggregate/Sum/Min/Max/Average/Count/Any/All/First/Last/Concat/Reverse/Join/GroupJoin/ThenBy/ThenByDescending (Join/GroupJoin = jointure par hachage qui préserve l'ordre sur des clés primitives ; OrderBy+ThenBy = un seul tri composite stable, source copiée). L'IGrouping de GroupBy/ToLookup est utilisable comme une séquence (itérer, g.Select/g.Sum/g.Count()) et expose g.Key ; l'indexeur [key] d'ILookup n'est pas modélisé. Un comparateur qui demande la valeur par défaut passe ToDictionary, ToLookup, GroupBy, Distinct et ToHashSet, et tout autre fait échouer le build (EQ2007) (Depuis 0.2.0-preview.61).
Collections ✅ List, Dictionary, HashSet (y compris les initialiseurs et .Count ; un ensemble est l'ensemble du runtime, détenu par emplacement et trouvant ses éléments par valeur là où son type d'élément le fait, Depuis 0.2.0-preview.61) ; Queue/Stack/LinkedList compat ; famille triée SortedSet/SortedDictionary/SortedList (énumération triée par clé). Chaque dictionnaire est la classe dictionnaire du runtime, $eq.collections.dictionary, qui énumère par emplacement comme celle de .NET, une clé record/struct/tuple étant trouvée par valeur (construction et copie, d[k] en lecture/écriture, ContainsKey/Add/Remove/Clear/TryGetValue/GetValueOrDefault/TryAdd/ContainsValue, Keys/Values/Count, foreach) (Depuis 0.2.0-preview.60). Lire une clé manquante lève KeyNotFoundException, de même qu'une affectation composée et un incrément sur une telle clé (Depuis 0.2.0-preview.58 pour l'incrément). L'indexeur ILookup[key] renvoie le groupe (ou un groupe vide pour une clé absente). Le Add et le Clear d'ICollection<T> atteignent la collection derrière l'interface, et IList<T> et IReadOnlyList<T> lisent et écrivent un type à vous via son indexeur et le comptent par son Count (Depuis 0.2.0-preview.61).
enum ✅ chaîne du nom du membre, un enum [Flags] son nombre (égalité/switch/ternaire, texte et formats, les statiques d'Enum)
long/ulong ✅ exact via BigInt (helper long) ; littéraux 5L→5n, et les constantes aussi (TimeSpan.TicksPerSecond → 10000000n, Depuis 0.2.0-preview.60), sur le fil sous forme de chaîne JSON
DateTime ✅ type compat DateTime précis au tick : constructeurs, composantes, Add* au tick près là où .NET 7+ y aboutit (une fraction tronquée, un compte ou un résultat hors plage refusé dans les termes de .NET, y compris AddMicroseconds, Depuis 0.2.0-preview.59), -→TimeSpan, comparaisons, .ToString()/format ; fil ISO-8601 + hydratation. ToString(), ToString(format) et ToString(format, provider) impriment comme .NET les imprime (pas de spécificateur, ou un spécificateur null ou vide, est le G de la culture actuelle, et aucun fuseau horaire ne déplace les parties de la valeur sauf celui de U) : chaque spécificateur standard d'une lettre (d à Y, s, u et R à partir des propres parties de la valeur, o à partir de celles-ci avec le fuseau que nomme son kind, U ramené en UTC sauf s'il est déjà en UTC), une lettre qui n'est pas un spécificateur standard refusée comme .NET la refuse, un format personnalisé jeton par jeton avec sa fraction de seconde (HH:mm:ss.fff), les séparateurs de la culture pour / et : et l'ère pour g (Depuis 0.2.0-preview.61), les modèles de la culture invariante pour CultureInfo.InvariantCulture ou en l'absence de culture en vigueur, et une culture nommée refusée au build (EQ2108) ; ToShortDateString() et ses frères sont d, D, t et T (Depuis 0.2.0-preview.60). Add, Subtract, AddMonths, AddYears et les opérateurs + et -, y compris les formes composées, refusent un résultat hors du calendrier dans les termes de .NET, les opérateurs nommant leur paramètre t (Depuis 0.2.0-preview.60). Chaque constructeur que laisse passer le compilateur construit la valeur que construit .NET, chaque argument à la place de son paramètre, y compris un argument nommé : les composantes avec une milliseconde, une microseconde et un kind, les ticks avec un kind, et un DateOnly avec un TimeOnly, une composante, un kind ou des ticks hors plage étant refusés dans les termes de .NET, et une surcharge qui prend un Calendar refusée au build (EQ1004). Une valeur porte son Kind (Now et Today locaux, UtcNow UTC, SpecifyKind), que l'arithmétique garde et que l'égalité, l'ordre et le hash ignorent ; ToLocalTime() et ToUniversalTime() convertissent selon lui dans le fuseau horaire du navigateur, Microsecond et Nanosecond répondent, et le JSON écrit une valeur UTC avec Z et une valeur locale avec son décalage et les relit comme le fait System.Text.Json (Z une heure UTC, un décalage ramené à l'heure locale du navigateur), tandis que DateTime.Parse ramène un fuseau écrit à l'heure locale. Une heure locale issue d'un instant garde laquelle de deux heures répétées elle est, comme .NET le garde à côté du kind, à travers l'arithmétique jusqu'à SpecifyKind, donc son retour vers UTC est cet instant, et le texte d'une date écrit son kind : o et K terminent une heure UTC par Z et une heure locale par son décalage, et z, zz et zzz écrivent le décalage du fuseau pour tout ce qui n'est pas UTC (Depuis 0.2.0-preview.61)
TimeSpan ✅ type compat TimeSpan précis au tick : From*, constructeurs, composantes/totaux, + -, comparaisons, .ToString() "c" de .NET, et ses formats standard et personnalisés (Depuis 0.2.0-preview.62) ; Parse et TryParse lisent comme .NET lit sans format, chaque partie par rapport à sa borne (Depuis 0.2.0-preview.62) ; fil "c" + hydratation
DateOnly / TimeOnly ✅ types compat (.NET 6+) : constructeurs, composantes, Add*(+ bouclage de TimeOnly, un produit converti comme .NET 9+ convertit un double, Depuis 0.2.0-preview.59, un DateOnly sortant du calendrier refusé dans les termes de .NET, Depuis 0.2.0-preview.60), comparaisons, ToString(), ToString(format) et ToString(format, provider) comme .NET les imprime dans la culture en vigueur : d et t sans format, les spécificateurs standard que chaque type accepte et un format personnalisé de ses propres parties, toute autre chose étant la FormatException de .NET, et ToShortDateString(), ToLongDateString(), ToShortTimeString() et ToLongTimeString() comme d, D, t et T (Depuis 0.2.0-preview.61) ; fil ISO + hydratation
DateTimeOffset ✅ type compat précis au tick (heure d'horloge murale + décalage, comparé par l'instant) : constructeurs, composantes, MinValue/MaxValue (Depuis 0.2.0-preview.58), Offset/UtcDateTime/LocalDateTime, ToOffset, Add* au tick près avec AddMilliseconds/AddMicroseconds, l'heure d'horloge puis l'heure UTC refusées dans les termes de .NET (Depuis 0.2.0-preview.59), y compris AddMonths et AddYears (Depuis 0.2.0-preview.60), From/ToUnixTime* (ToUnixTimeSeconds et ToUnixTimeMilliseconds un long, arrondis vers le bas avant 1970 comme .NET les arrondit, Depuis 0.2.0-preview.61), - →TimeSpan, comparaisons d'instants, ToString() et ses surcharges comme .NET les imprime dans la culture en vigueur : la date courte, l'heure longue et le décalage sans format, chaque spécificateur standard sauf U, o et K écrivant le décalage et u et R convertissant selon lui (Depuis 0.2.0-preview.61) ; fil ISO+décalage + hydratation. Chaque constructeur construit la valeur que construit .NET : les composantes avec une milliseconde et une microseconde, les ticks, un DateOnly avec un TimeOnly, et un DateTime, qui sans décalage prend celui du navigateur pour une heure qui n'est pas UTC ; un décalage qui n'est pas en minutes entières, dépasse quatorze heures, ou n'est pas nul pour un DateTime UTC est refusé dans les termes de .NET. Now est l'instant au décalage du navigateur, LocalDateTime et ToLocalTime() lisent le fuseau horaire du navigateur, et Microsecond et Nanosecond répondent (Depuis 0.2.0-preview.61)
record / struct / tuple valeur ✅ Sémantique de valeur. Les records/structs sont des instances de leurs classes jumelles (le new Point(1,2) positionnel appelle le constructeur qu'appelle C#, et un initialiseur d'objet est appliqué après lui), les tuples sont des tableaux avec accès aux éléments par position (t.Item1) et par nom déclaré ((int X, int Y).X) → index. ==/!=, .Equals, Distinct comparent structurellement via $eq.equals, et Contains, IndexOf et un ensemble selon le comparateur par défaut du type de l'élément, le membre tableau d'un tuple par référence (Depuis 0.2.0-preview.61) ; with copie-et-remplace. Un struct mutable et un tuple valeur sont copiés là où C# les copie, donc une écriture via un nom ne transparaît jamais via un autre nom auquel la valeur a été affectée, passée, renvoyée ou boxée : le jumeau copie à l'écriture, et copie this là où il quitte le membre de son struct (Depuis 0.2.0-preview.61). La déconstruction var (a, b) = … fonctionne pour les tuples (déstructuration de tableau, trous d'éléments ignorés) et les records (déstructuration d'objet selon l'ordre de Deconstruct). Les records sont émis comme des classes JS nommées portant leurs méthodes d'instance utilisateur + un equals structurel, un with qui préserve le prototype, et un toString .NET ; la sémantique de valeur est inchangée. Le equals d'un record compare son type d'exécution (un Dog n'est jamais égal à un Animal), ce que compare sa base et chaque champ qu'il déclare, y compris un champ privé, et son texte nomme les membres qu'écrit le PrintMembers de .NET, dans son ordre : d'abord les membres de sa base, puis les siens, y compris une propriété calculée et une propriété dont le getter a une accessibilité propre, une redéfinition étant laissée à la base, chacun une fois, et Name { } quand il n'y en a aucun. Deux membres d'un record ou d'un struct qui tombent sur un même nom du jumeau (struct S(int x) { public int X … }, un champ celsius à côté de Celsius) font échouer le build (EQ1007) ; un paramètre de constructeur principal lu seulement par un initialiseur (struct Point(int x) { public int X { get; } = x; }) appartient au constructeur, et compile ; une propriété abstraite ne contient rien. Chaque valeur est écrite comme .NET l'écrit : un bool comme True, un enum par le nom de son membre, un null comme rien et un nombre dans la culture en vigueur, et une classe ordinaire ou un struct qui n'écrit aucun texte propre écrit le nom complet de son type (My.App.Outer+In), là où le navigateur écrivait [object Object] ou le texte d'un record ; un type générique ne le fait pas encore (Depuis 0.2.0-preview.61). Un paramètre positionnel dont le record déclare la propriété (record Box(int X) { public int X { get; set; } = X; }) est un seul membre, celui déclaré (Depuis 0.2.0-preview.61). Un membre démarre comme le dit sa déclaration, quelle que soit l'expression : un paramètre positionnel, l'initialiseur d'une propriété ou d'un champ (= 1.5m, = 5 sur un long, = new(), = "#" + Id), écrit une fois, dans le constructeur de la classe (Depuis 0.2.0-preview.59), qui exécute chaque initialiseur à chaque construction, quoi que fixe ensuite un initialiseur d'objet (depuis 0.2.0-preview.61 ; voir Constructeurs ci-dessus). Les membres d'un record et d'un struct sont abaissés comme ceux d'une classe : une méthode async, un itérateur, un paramètre out ou ref, et la variable que lie le motif d'un corps d'expression (o is P p && p.X == X) fonctionnent dans leurs méthodes, opérateurs, conversions et propriétés calculées, les leurs et ceux par défaut que fournit une interface, et un getter qui fait un yield renvoie sa séquence dans une classe aussi (Depuis 0.2.0-preview.60). La pipeline de build découvre les records par balayage et émet chacun comme son propre module ; les composants qui référencent un record l'importent automatiquement (réactif, pas de liste codée en dur). Les valeurs SSR sont réhydratées dans la classe du record côté client (récursivement, en restaurant les records imbriqués et les membres compat), donc les méthodes/instanceof survivent. Couvre les records positionnels et à corps et les structs ordinaires, plus l'héritage de records (extends + super) et les records génériques, dont l'égalité compare le type fermé sous lequel une valeur a été construite : un Box<int> n'est jamais égal à un Box<double>, et une valeur construite à l'intérieur de code générique, dont le build ne peut pas connaître les arguments de type, n'est pas prise pour un autre type (Depuis 0.2.0-preview.61). Les dictionnaires à clé record (Dictionary<RecordKey, V>) trouvent une clé par valeur, donc des clés égales par valeur entrent en collision comme dans .NET. Chaque membre se compare comme EqualityComparer<T>.Default compare son type, donc un tableau, une liste ou l'interface d'une collection (IReadOnlyList<int>) par référence, et le Equals d'un tuple compare ses éléments de la même façon, un tuple d'une autre arité étant inégal et une paire null égale seulement à une autre ; un tuple vu seulement comme object, ou comme une interface qu'implémente un tuple, est un tableau sur le web et se compare par ses éléments (Depuis 0.2.0-preview.61).
Nullable<T> (T?) ✅ HasValue/Value, GetValueOrDefault() (valeur par défaut selon le type : 0/false/$eq.num.dec(0)/membre zéro de l'enum/…) et GetValueOrDefault(fallback), ?? ; opérateurs levés via $eq.nullable.* : l'arithmétique propage null, le relationnel (< > <= >=) est faux quand l'un des côtés est null (pas une coercition numérique) ; une affectation composée (+=, *=, …), un incrément et un -, ~ ou + unaire sur un nombre nullable sont levés aussi, un null restant null et une valeur suivant la règle de son type (un float? arrondit, un byte? boucle, un decimal? reste exact ; Depuis 0.2.0-preview.58). Le GetValueOrDefault() sans argument sur DateTime?/Guid?/struct produit null, donc utilisez là la forme avec valeur de repli.
Guid ✅ Guid.NewGuid()→crypto.randomUUID(), Guid.Empty, Guid.Parse/TryParse/new Guid(text) → le texte D canonique en minuscules (Depuis 0.2.0-preview.60) ; fil en chaîne

Clone this wiki locally