Repository navigation
Compiler fr
🌐 Cette page en : English · Português · Español · Français · Deutsch
Le compilateur est le composant central qui rend possible la magie d'eQuantic.UI. Il transforme la sémantique de C# en code TypeScript efficace et lisible.
La justesse est tenue par une suite différentielle de conformité : le même C# est évalué dans .NET et dans le Bun embarqué, et les deux réponses doivent s'accorder. Tout ce que le compilateur ne peut pas traduire fidèlement est une erreur de compilation avec un emplacement (les diagnostics EQ2xxx), jamais une fausseté silencieuse.
Pour garder le code du navigateur honnête, le compilateur impose des frontières strictes, inspirées de Next.js (la séparation serveur/client) et de Flutter (les contraintes), et les valide avant d'émettre du JS :
-
Composants client (
StatefulComponent/StatelessComponent) : logique d'UI, gestion d'état,System.Linq, types de base (string,int,DateTime). -
Interdit sur le client :
System.IO,System.Net.Httpdirect, un.Wait()bloquant.File.ReadAllText()dans le corps d'un composant est une erreur de compilation. -
Le pont : la récupération de données passe par des méthodes annotées
[ServerAction](style RPC). Voir Sécurité et Server Actions.
Le TypeScriptEmitter est le point d'entrée pour générer des fichiers .ts. Il organise les imports, définit les classes et utilise le CSharpToJsConverter pour convertir les corps des méthodes.
Il répartit chaque nœud Roslyn vers une stratégie, une par construction (BinaryExpressionStrategy,
IfStatementStrategy, …), et renvoie de l'IR, pas du texte. Les instructions bâtissent toujours
un JsStatement. Une stratégie d'expression qui a déjà traversé bâtit un JsExpr
(IExpressionIrStrategy) ; une qui renvoie encore du texte est greffée comme un nœud opaque,
identique octet pour octet à ce qu'elle a toujours produit. Cette frontière est ce qui laisse la
migration avancer une stratégie à la fois, et
tests/eQuantic.UI.Compiler.Tests/Coverage/ir-migration.baseline.txt est la liste des stratégies de
texte : elle ne fait que rétrécir, et une nouvelle stratégie naît sur l'IR.
SourceMapGenerator écrit une carte V3 standard (VLQ en Base64) depuis chaque module TypeScript
qu'eqc émet vers les lignes .cs dont il est venu, en nommant chaque fichier depuis lequel le module
a été écrit (une classe qui prend le default d'une interface mène ce default vers le fichier de
l'interface), chacun dans le projet, et avec le C# lui-même dedans seulement quand la carte est
full. Bun mappe ensuite le JavaScript vers ce TypeScript, et SourceMapComposer compose les deux,
en C# et sans aucun paquet npm, en UNE carte du JavaScript que le navigateur exécute jusqu'au C#, en
gardant le debugId de bun. Les deux cartes passent par un seul écrivain de cartes, et cet écrivain,
comme chaque fichier JSON que la compilation écrit, par un seul écrivain JSON dont l'échappement est
celui de System.Text.Json, donc un fichier C# qui contient un caractère de contrôle a quand même une
carte. Ce que porte une carte (EQuanticSourceMaps : full en Debug, none ailleurs, external
sur demande) est sur Débogage.
Le convertisseur interroge le modèle sémantique avant de deviner. Un membre, une locale, un
paramètre, un statique : chacun émet depuis son symbole (this.name, Counter.name, un nom nu), et
Console.WriteLine est console.log parce que le symbole dit System.Console. Les heuristiques de
nom (un tiret bas initial, une propriété capitalisée) ne sont légales que là où le modèle ne peut pas
être interrogé : un extrait sans modèle, un nœud qu'une stratégie a réécrit. Sous un modèle qui fait
autorité, un nom de l'arbre qui ne se lie pas est une erreur de compilation (EQ2006), jamais une
traduction devinée.
Depuis 0.2.0-preview.36
CodeGen/Ir/ est un petit arbre avec un écrivain par niveau, JsExpr → JsStatement →
JsClassMember → JsClass → JsModule, et les écrivains possèdent tout ce qu'une stratégie ne doit
jamais écrire à la main :
-
Les parenthèses viennent de la précédence et de l'associativité, jamais d'un gabarit. Avant
cela,
f ?? g && gétait livré tel quel : C# n'a besoin d'aucune parenthèse là, JavaScript refuse le mélange nu, et le bundle entier ne s'analysait pas. -
Une seule évaluation : un
JsTemplatenomme ce qu'il calcule ({0} === {0}.normalize()) et l'écrivain lie exactement une fois une partie utilisée plus d'une fois : un nom simple ou un littéral est inliné, une lecture de membre non (un getter peut compter). Une partie à l'intérieur d'une fonction que le gabarit définit est liée aussi, un nom simple compris, puisque cette fonction tourne une fois par élément : seuls un littéral,thiset une lambda écrite sur place restent dedans, et de même une partie qui lit un nom que la fonction déclare. -
La disposition : une instruction par ligne, les blocs indentés (
JsLayout.Pretty) ;Compactreproduit l'ancien monde de chaînes octet pour octet, et c'est ainsi que chaque étape de la migration est prouvée. Une classe a une seule règle de disposition, une ligne vide avant un membre avec un corps, les champs contigus, et un module est ses imports comme des recordsJsImport, une ligne vide, son corps. -
L'émetteur (
TypeScriptEmitter) décide ce que contient un module et remet des nœuds au constructeur ; il n'assemble aucun texte.
Deux filets tiennent tout cela : les épinglages de composant (le module généré de chaque composant
partagé, octet pour octet : un changement de disposition doit être uniquement d'espaces contre eux) et
la suite de conformité, qui exécute chaque forme traduite des deux côtés et compare les réponses. Les
jumeaux générés sous src/eQuantic.UI.Runtime/src/shared/components sont épinglés, vérifiés en types
et testés, pas passés au linter : le code généré répond à son écrivain, pas à un guide de style.
Actuellement, le compilateur prend en charge un large éventail de constructions C# :
-
Expressions : arithmétiques, logiques, ternaires, interpolation de chaînes, fusion de nuls (
??), accès conditionnel (?.,?[]). Un argument qui attend derrière?.est attendu là où il est écrit, seulement quand le receveur n'est pas nul (Depuis 0.2.0-preview.60), quel que soit le receveur : un appel, un élément ou une propriété devant?.est lu une fois dans une variable de la méthode où il est écrit (Depuis 0.2.0-preview.61 ; jusque-là un tel receveur cassait la compilation avec EQ1004). Une lecture conditionnelle de nul répondnulllà où sa valeur est utilisée, comme C# le fait, jamais l'undefinedde JavaScript (Depuis 0.2.0-preview.61). Un groupe de méthodes est le délégué que C# fait : son receveur est lu une fois, quand le délégué est fait,base.Mappelle la méthode de la base sur cet objet, et le groupe d'une méthode d'extension est lié sur la classe qui la déclare, là où va son appel. Un groupe sur une extension que rien dans le bundle ne déclare, commelist.Any, casse la compilation avec EQ2004 (Depuis 0.2.0-preview.61) -
Flux de contrôle :
if,switch,for,foreach,while,do-while,break,continue,throw - Motifs modernes : prise en charge complète des motifs récursifs, de propriété, positionnels, relationnels et logiques (C# 9.0 à 12.0)
-
Gestion des ressources : prise en charge des instructions
usinget des déclarationsusing var -
Exceptions : prise en charge complète de
try-catch-finallyet des instructionsthrow(Exception → Error) -
Index et plages : prise en charge de l'opérateur d'index depuis la fin (
array[^1]→array[array.length - 1]) -
Méthodes de chaîne : méthodes d'instance (
Split,Replace,StartsWith,EndsWith,Contains,Substring,IndexOf,LastIndexOf,PadLeft,PadRight,Trim,TrimStart,TrimEnd,ToUpper,ToLower,ToUpperInvariant,ToLowerInvariant,Insert,Remove,ToCharArray) et méthodes statiques (IsNullOrEmpty,IsNullOrWhiteSpace,Join,Concat,Compare,Equals,Format). Une méthode à laquelle on donne unStringComparisonrépond par lui, par le runtime, et une recherche par une comparaison de culture casse la compilation (EQ1004) : le navigateur n'a pas de recherche consciente de la culture (Depuis 0.2.0-preview.60) -
Méthodes numériques :
ParseetTryParsesur chaque type numérique (int,uint,long,ulong,short,ushort,byte,sbyte,float,double,decimal), lus par le runtime avec la grammaire de .NET -
Méthodes de List :
Add,AddRange,Insert,InsertRange,Remove,RemoveAt,RemoveRange,RemoveAll,Clear,IndexOf,LastIndexOf,Find,FindIndex,FindLast,FindLastIndex,FindAll,Exists,TrueForAll,Sort,ForEach,GetRange,CopyTo,BinarySearch -
Faces de List : un élément lu ou écrit par
IList<T>,IReadOnlyList<T>ouIList→$eq.collections.item(list, i)et$eq.collections.setItem(list, i, value), sonCount→$eq.collections.count(list), et l'Addet leClearpropres d'ICollection<T>→$eq.collections.add(collection, item)et$eq.collections.clear(collection). Chacun répond pour la collection que la face tient quand il tourne : un tableau par son indice, l'ensemble, la liste chaînée ou le dictionnaire du runtime par ses propres membres, et un type à vous par son indexeur, sonCount, sonAddet sonClear(Depuis 0.2.0-preview.61) -
Méthodes statiques d'Array : prise en charge complète des méthodes statiques d'Array
-
Array.Sort(array)→array.sort()- Trie le tableau sur place -
Array.Sort(array, comparison)→array.sort(comparison)- Trie avec un comparateur à soi -
Array.Reverse(array)→array.reverse()- Inverse le tableau sur place -
Array.Find(array, predicate)→array.find(predicate)- Trouve le premier élément qui correspond -
Array.FindIndex(array, predicate)→array.findIndex(predicate)- Trouve l'index de la première correspondance -
Array.FindAll(array, predicate)→array.filter(predicate)- Trouve tous les éléments qui correspondent -
Array.IndexOf(array, value)→array.indexOf(value)- Trouve l'index d'une valeur -
Array.LastIndexOf(array, value)→array.lastIndexOf(value)- Trouve le dernier index d'une valeur -
Array.Exists(array, predicate)→array.some(predicate)- Vérifie si un élément correspond -
Array.TrueForAll(array, predicate)→array.every(predicate)- Vérifie si tous les éléments correspondent -
Array.Clear(array)→array.splice(0)- Retire tous les éléments -
Array.Resize(ref array, size)→array.length = size- Redimensionne le tableau
-
-
Méthodes d'Enum : un enum n'a pas d'objet à lui dans le navigateur, donc les statiques d'
Enumappellent les fonctions d'enum du runtime,$eq.enums, avec la forme de l'enum écrite à l'appel : ses noms, les noms camelCase que le navigateur tient, ses valeurs et s'il est un enum[Flags](Depuis 0.2.0-preview.60 ; elles nommaient un objet qu'aucun module ne déclare, et levaient)-
Enum.Parse<T>(text)etEnum.Parse(typeof(T), text)→$eq.enums.parse(text, shape): un nom, des noms joints par des virgules ou un nombre, la casse gardée sauf siignoreCaseest vrai, et une erreur là où .NET en lève une -
Enum.TryParse<T>(text, out var result)→ vrai avec la valeur, ou faux avec le défaut de l'enum dansresult, et null pour la surcharge qui prend unType -
Enum.GetName(value)→ le nom du membre qui a cette valeur, ou null là où aucun ne l'a -
Enum.GetNames<T>()etEnum.GetValues<T>()→ les noms et les valeurs, dans l'ordre des valeurs -
Enum.IsDefined(typeof(T), value)→ si un membre a cette valeur, donnée comme l'enum, un nombre, un nom déclaré, ou unobjectqui en tient un -
value.ToString(), une interpolation et une concaténation écrivent le nom du membre, les drapeaux posés d'une combinaison de flags (Read, Write), les chiffres d'une valeur qu'aucun membre ne nomme, et rien pour un enum nullable nul.ToString("D"),"X","F"et"G", et le format d'une interpolation, écrivent ce que .NET écrit, et un alignement complète ce texte - Un cast depuis un
objectqui tient l'enum le garde ((Status)Enum.Parse(typeof(Status), text)), un cast entre deux enums passe par leurs valeurs, et une valeur qu'aucun membre ne nomme est tenue comme son nombre
-
-
Méthodes de Dictionary : chaque Dictionary, IDictionary et IReadOnlyDictionary, et les triés, est
une classe du runtime sous
$eq.collections.*, interrogée avechas/get(Depuis 0.2.0-preview.60)-
new Dictionary<K, V>()→$eq.collections.dictionary(), ou$eq.collections.dictionary(null, true)là où le type de la clé compare par valeur (un record, un struct, un tuple, un decimal, une date, une classe qui surchargeEquals) ; une copie l'amorce avec sa source, et un initialiseur avec des paires[key, value] -
dict[key]→$eq.mapGet(dict, key)- lève pour une clé manquante comme .NET le fait ;dict[key] = value→$eq.mapSet(dict, key, value), qui répond la valeur écrite -
ContainsKey(key)→dict.has(key)- ne répond jamais pour un membre du prototype -
TryGetValue(key, out var value)→(dict.has(key) ? ((value = dict.get(key)), true) : ((value = 0), false))- un échec écritdefault(TValue)(0ici, pour unint) -
GetValueOrDefault(key[, defaultValue])→(dict.has(key) ? dict.get(key) : 0)- un défaut explicite est évalué une fois, qu'il soit nécessaire ou non - Un receveur ou une clé lus par un membre ou un appel sont liés une fois :
this.cache.TryGetValue(Next(), out var v)→(($0, $1) => …)(this.cache, next()) -
Add(key, value)→dict.set(key, value)- ne lève PAS pour une clé déjà présente, là où celui de .NET lève (#440) -
Remove(key)→dict.delete(key), etRemove(key, out var value)déplace la valeur dans l'out -
TryAdd(key, value)→dict.tryAdd(key, value),ContainsValue(value)→dict.containsValue(value) -
Clear()→dict.clear() -
Keys/Values(propriétés) →dict.keys()/dict.values(), des tableaux dans l'ordre du dictionnaire ;Count,Keys.CountetValues.Count→dict.size, etKeys.Contains(key)→dict.has(key)
-
-
LINQ : conversion directe des méthodes LINQ vers leurs équivalents JS :
-
Projection :
Select→map,SelectMany→flatMap -
Filtrage :
Where→filter,Distinct→[...new Set()] -
Ordonnancement :
OrderBy/OrderByDescending→sort,Reverse→[...arr].reverse() -
Partitionnement :
Skip→slice(n),Take→slice(0, n) -
Élément :
First/FirstOrDefault→find/[0],Last/LastOrDefault→arr[arr.length-1],Single/SingleOrDefault→find/[0]; avec une valeur par défaut,FirstOrDefault,LastOrDefaultetSingleOrDefault→$eq.linq.firstOrDefault,$eq.linq.lastOrDefaultet$eq.linq.singleOrDefault, qui la répondent là où rien ne correspond (Depuis 0.2.0-preview.62) -
Quantificateurs :
Any→some/length > 0,All→every,Contains→includes -
Agrégation :
Count→length/filter().length,Sum→reduce(($a, $b) => $a + $b, 0),Average→$eq.linq.average, qui calcule la moyenne dans le type de l'élément, un long, un float, un decimal ou un nullable (Depuis 0.2.0-preview.62),Min→Math.min(...),Max→Math.max(...) -
Opérations d'ensemble :
-
Concat(other)→[...source, ...other]- Concatène deux séquences -
Union(other)→[...new Set([...source, ...other])]- Éléments uniques des deux séquences -
Intersect(other)→[...new Set(source)].filter(($x) => other.includes($x))- Éléments communs -
Except(other)→[...new Set(source)].filter(($x) => !other.includes($x))- Éléments de la source mais pas de l'autre -
other, comme chaque argument d'un appel LINQ qui n'est pas une lambda écrite sur place, est évalué une fois, dans l'ordre de C#, avant que l'abaissement ne tourne : il tournait une fois par élément (Depuis 0.2.0-preview.61)
-
-
Paires :
Zip(second)etZip(second, third)→$eq.linq.zipTuples, qui répond les paires et les triplets que .NET répond (Depuis 0.2.0-preview.62) -
Égalité :
SequenceEqual→$eq.linq.sequenceEqual, qui compare par l'égalité par défaut du type de l'élément (Depuis 0.2.0-preview.62) -
Comparateurs : un comparateur remis à
ToDictionary,ToLookup,GroupBy,Distinct,ToHashSetouSequenceEqualqui demande celui par défaut (EqualityComparer<T>.Default,null,StringComparer.Ordinal) est écarté, puisque l'abaissement trouve déjà ses clés ainsi, et tout autre casse la compilation avec EQ2007 (Depuis 0.2.0-preview.61) -
Filtrage par type :
-
Cast<T>()→ passage direct (JavaScript est à typage dynamique) -
OfType<T>()→filter(($x) => typeof $x === 'type')pour les primitifs,filter(($x) => $x instanceof Type)pour les objets
-
-
Projection :
-
Async/Await : mise en correspondance de
TaskavecPromiseet prise en charge native d'await. -
Opérateurs C# modernes : prise en charge des opérateurs et des mots-clés modernes de C#
-
Affectation avec fusion de nuls :
x ??= value→x ?? (x = value)- Affecte seulement si nul ou undefined -
Opérateur nameof :
nameof(variable)→'variable'- Obtient le nom comme chaîne à la compilation. C'est le nom, pas son orthographe :nameof(@class)→'class'(Depuis 0.2.0-preview.60) -
Mot-clé default :
default(T)et le littéraldefaultsont le défaut du type, le type du littéral étant celui vers lequel C# le convertit :default(int)→0,default(long)→0n,default(int?)→null,int x = default→0(Depuis 0.2.0-preview.58)
-
Affectation avec fusion de nuls :
Deux valeurs par défaut sont implicites en C# et absentes en JavaScript. Les deux ont été émises comme rien pendant un temps, et les deux échouent TARD, pas à la compilation, et pas là où est la cause.
Depuis 0.2.0-preview.22
Un champ d'un type valeur vaut zéro que quelqu'un ait écrit = 0 ou non. Sur le client c'était
undefined, et les deux ne sont pas la même valeur. Les lectures survivent par chance aussi longtemps
qu'elles sont des TESTS (undefined > 0 est faux, ce que 0 aurait dit), et puis la première
ARITHMÉTIQUE le transforme en NaN. Math.max(width, undefined) atteint la feuille de styles comme
width:NaNpx, une règle que l'analyseur CSS jette entière : la classe est calculée, hachée, émise,
posée sur l'élément, et ne fait rien.
Cela n'apparaît que sur les pages RENDUES côté client, jamais sur du SSR ni sur un chargement direct, parce que le serveur calcule la même propriété en C# où elle valait 0 depuis le début. Donc les deux cibles sont en désaccord sur un champ et la page qui le prouve est celle que personne ne recharge.
Chaque type valeur non nullable porte désormais son défaut (0, false, le membre zéro d'un enum).
Les nullables non, parce que là null EST la réponse de C#, et inventer un zéro serait la même
divergence pointant dans l'autre sens.
Depuis 0.2.0-preview.21
public sealed class TocEntry(Action<string, bool> onSeen, string id) : StatelessComponent
{
public override VisualNode Build(ComponentContext context) =>
new InView(Heading(id), visible => onSeen(id, visible)); // this.onSeen, this.id
}Roslyn modélise la capture comme un IParameterSymbol, donc chaque endroit qui demande « est-ce un
paramètre ? » répond oui à propos de quelque chose qui se comporte comme un champ. Émis nu cela
compile, la page se rend, et le ReferenceError arrive quand le callback finit par se déclencher, en
remontant depuis l'intérieur du réconciliateur comme un TypeError à propos de tout autre chose.
Un composant n'est pas le seul C# dont une page a besoin. Le modèle de document derrière un éditeur, une petite machine à états, un analyseur : aucun d'eux n'est un composant, et tous doivent tourner sur les deux cibles. Ils transpilent de la même façon, comme leurs propres modules, et les règles ci-dessous sont ce qui rend l'émission VÉRIFIÉE plutôt que simplement présente.
line[start..end] → line.slice(start, end)
line[2..] → line.slice(2)
line[..^1] → line.slice(0, -1)
line[..^n] → $eq.slice(line, 0, false, n, true)La dernière forme est celle que JavaScript ne peut pas dire directement : ^0 signifie la FIN, tandis
que slice(0, -0) est slice(0, 0), qui est vide. Tout ce qui n'est pas un littéral positif après
^ se résout donc contre la longueur comme Index.GetOffset le fait. Une Range gardée comme VALEUR
est signalée (EQ2004) : rien de l'autre côté n'en reçoit une, et indexer au point d'usage est à quoi
elle sert.
Une plage sur un type à vous avec un Length (ou un Count) et un Slice(int start, int length)
n'est pas la tranche d'un tableau : C# l'abaisse vers ce Slice, qui prend un début et une LONGUEUR
là où le slice de JavaScript prend une fin, et le jumeau fait de même (Depuis 0.2.0-preview.61) :
strip[1..3] → strip.slice(1, 3 - 1)Le receveur est lu une fois, puis les extrémités dans l'ordre où elles sont écrites, puis le Length
ou le Count, lu seulement là où une extrémité compte depuis la fin ou la fin est laissée ouverte,
comme .NET les lit. Une plage remise à un indexeur qui prend la Range elle-même lui remettrait une
valeur Range, et casse la compilation (EQ2004) pour la même raison qu'une plage gardée.
JavaScript n'a ni l'un ni l'autre. Une méthode qui les déclare renvoie un OBJET (sa propre valeur sous
$, chaque out et ref sous son nom) et son corps passe dans une fermeture pour que chaque return
dedans garde le sens qu'il avait. Le site d'appel déballe avec une flèche, ce qui marche dans
n'importe quelle position d'expression, y compris dans un if :
var next = document.Replace(range, text, out var caret);let caret: any;
let next = ($o => (caret = $o.caret, $o.$))(document.replace(range, text));out quitte la liste de paramètres JS (il n'est pas passé VERS l'intérieur) ; ref reste, parce
qu'il est lu avant d'être écrit. out _ n'affecte rien.
La liaison d'un motif (x is T t), un out var et l'élément d'une déconstruction
((var c, var d) = …) sont affectés dans l'expression qui les déclare, donc l'instruction qui tient
cette expression les déclare, avec la portée que Roslyn leur donne (Depuis 0.2.0-preview.59) :
- une instruction d'expression, un
if, unreturn, unthrow, unyield return, une déclaration, l'expression gouvernante d'unswitchet unlockles déclarent devant eux-mêmes, là où le code après eux peut les lire (if (!int.TryParse(s, out var n)) return;puisn). Posés directement dans une section de switch, ils les laissent au switch, qui les déclare une fois pour tout son bloc, comme C# les porte, donc une autre section peut les affecter ; - un
while, undoet unforles déclarent dans la tête propre de la boucle,for (let n; …), que JavaScript copie pour chaque itération, comme .NET donne à la condition d'une boucle une variable fraîche : une fermeture faite dans une itération garde la valeur de cette itération ; - un
foreachet unusingles déclarent dans un bloc autour de l'instruction, qui est leur portée en C# ; - une lambda, une fonction locale, la clause d'une requête et le bras d'une expression switch déclarent les leurs, une fois par appel ;
- l'initialiseur d'un champ ou d'une propriété les déclare dans une flèche à lui.
Un nom que JavaScript refuse, et que l'échappement verbatim de C# permet (@class, @new,
@default), devient un nom légal à sa déclaration et à chaque référence, de quelque façon qu'il ait
été lié : class$, avec un $ qu'aucun nom C# ne peut tenir. Une étiquette et un paramètre de type
d'une méthode écrits ainsi prennent le même nom, et un membre ou une clé de with est le nom
lui-même : r.class, { class: 5 } (Depuis 0.2.0-preview.61).
Une locale nommée comme un global que le code émis lit (crypto, undefined, Math, Number,
console) prend le même $, donc le global reste visible pour le code à côté d'elle, et chaque nom
que le code émis déclare pour lui-même commence par un $, donc il ne rencontre jamais une locale que
le code autour de lui lit (Depuis 0.2.0-preview.61).
new List<T>(x) signifie deux choses opposées selon ce qu'est x, et seul le constructeur résolu
peut dire laquelle : new List<string>(other.Count) est une liste vide dimensionnée à l'avance,
new List<string>(other) est une copie. La première émet un tableau vide, la seconde [...other].
Avec un initialiseur de collection, la liste est ce que son constructeur y a mis, puis chaque élément
dans l'ordre : new List<string>(other) { "x" } est [...other, "x"] et
new List<string>(8) { "x" } est ["x"], écrit ou typé par la cible. Une capacité qui n'est pas une
constante que la liste prend est évaluée quand même, avant les éléments, et une négative est refusée
comme le constructeur de List<T> la refuse :
new List<string>(Size()) { "x" } est ($eq.collections.listCapacity(this.size()), ["x"])
(Depuis 0.2.0-preview.61).
Un char C# dans + - * / % promeut en int et calcule sur l'unité de code, tandis qu'un char
transpilé est une chaîne de longueur 1. Quand le type du RÉSULTAT est numérique, les opérandes char
s'abaissent en unités de code (les littéraux constants se replient en nombre ; les expressions lisent
charCodeAt(0)), donc text[i] - '0' est le chiffre et 'A' + col est un nombre, exactement comme
dans .NET. (char)numeric s'abaisse en String.fromCharCode. char + string reste une
concaténation : son type de résultat est string, donc la branche numérique ne le voit jamais.
JavaScript n'a pas de decimal et pas d'entier 64 bits, donc le câble envoie ceux-là comme des
chaînes, un long comme "9007199254740993", un decimal comme "0.1", la famille des dates comme du
texte ISO. Le compilateur connaît le type C# de chaque champ d'état et de chaque retour de Server
Action, donc il écrit ce savoir dans le jumeau comme une carte statique $hydration, et le runtime
convertit UNE FOIS, là où la valeur arrive, au lieu que chaque site d'usage convertisse par
précaution. Un record nomme sa propre carte, une liste est [spec], les valeurs d'un dictionnaire
sont { dict: spec }, et un tuple est positionnel ({ tuple: [...] }) parce qu'il traverse comme un
tableau. Rien n'est émis là où l'hydratation serait l'identité, ce qui est la plupart des champs.
new int[0].SingleOrDefault() vaut 0 dans .NET, pas null, et il en va de même pour FirstOrDefault,
LastOrDefault, ElementAtOrDefault et DefaultIfEmpty. Un champ déclaré sans initialiseur prend le
même défaut : un int vaut 0, un bool faux, un long 0n, et un ENUM son membre de valeur zéro, qui
est une chaîne portant le NOM du membre de ce côté, donc un champ laissé non posé ne rendrait sinon
rien du tout. Une seule table répond pour les deux, par symbole, donc un type atteint par un alias
(using Amount = decimal;) n'est pas une réponse différente du type lui-même.
La même table remplit un tableau dimensionné, new T[n], et répond default(T) et le littéral
default, dont le type est celui vers lequel C# le convertit : celui d'une locale, d'un paramètre, de
l'autre bras d'une conditionnelle. Un élément struct est une instance zéro à lui, puisque fill
partagerait un objet sur chaque case, et les jumeaux de temps et d'identité (DateTime, TimeSpan,
DateOnly, TimeOnly, DateTimeOffset, Guid) partent de leur MinValue, Zero ou Empty. Un
struct dont le jumeau écrit à la main ne peut pas bâtir son zéro garde le défaut propre du jumeau dans
un littéral default : undefined, que le constructeur du jumeau transforme en son défaut, là où
null le contournerait. Depuis 0.2.0-preview.58.
xs.OrderBy(a).OrderBy(b) trie par b. L'ordonnancement précédent ne reste pas la clé primaire : il
ne survit que comme le départage qu'un tri stable lui donne, donc c'est un résultat différent de
xs.OrderBy(a).ThenBy(b) dès que b a des égalités que a départagerait. Les deux traversent
fidèlement : un OrderBy enchaîné trie sa source d'abord et trie cela, et ThenBy compose sa clé
dans la même comparaison.
Math.round(32.5) vaut 33 en JavaScript et 32 dans .NET, qui arrondit une moitié vers le pair.
Partout où le runtime reflète un MathF.Round, il utilise la règle de .NET, donc une valeur calculée
dans le navigateur est la valeur que le serveur a calculée : c'est ce qui garde l'interligne d'une
échelle typographique, et toute mise en page dérivée d'une, identique à travers l'hydratation plutôt
qu'à une fraction près.
C'est l'arithmétique de .NET, pas une approximation d'elle. Le point milieu est EXACT :
Math.Round(0.5015, 3) vaut 0,501, parce que 0.5015 * 1000 vaut 501,49999999999994 : une valeur
près d'une moitié n'est pas une moitié. Chaque mode de MidpointRounding est honoré, un compte de
chiffres au-delà de 15 (6 pour un float) lève comme celui de .NET, et MathF.Round(x, digits) met à
l'échelle et divise en simple précision.
La plupart des opérateurs LINQ font une forme chacun (Where est filter, Aggregate est reduce
avec ses arguments échangés), donc ils vivent comme des entrées dans une table indexée par opérateur
et nombre d'arguments, gardée une fois, avec l'écrivain d'IR qui ponctue et lie tout receveur utilisé
deux fois. Un opérateur qui doit RAISONNER garde une stratégie à lui : la famille OrDefault lit le
type de l'élément, Sum choisit une graine d'après lui, Cast et OfType testent des types,
Contains choisit entre égalité d'identité et structurelle. La même forme vaut pour la BCL numérique,
où la surface moderne (Double.AcosPi et sa famille) est une table de gabarits plutôt qu'une méthode
chacune.
Là où ces tables n'ont pas d'entrée, l'appel est une ERREUR de compilation qui nomme le membre, jamais
une supposition : un nom émis sans rien derrière est un ReferenceError au chargement, et une page
qui ne se rend jamais.
Un byte C# au-delà de 255 boucle ; un nombre JavaScript continue de compter. Le compilateur lit le
type du RÉSULTAT de chaque + - * << (et de ++, -- et des formes composées) et règle la valeur là
où C# le ferait : byte, sbyte, short, ushort et uint bouclent TOUJOURS (& 0xFF,
<< 16 >> 16, >>> 0 : les valeurs empaquetées et les hachages en dépendent, donc h *= 16777619
sur un uint est le pas FNV qu'il est dans .NET, via Math.imul). int et long ne bouclent que là
où vous avez écrit unchecked (unchecked(a * a) vaut 0 pour 65536 ; un long boucle via
BigInt.asIntN), parce que chaque i + 1 ordinaire dans une UI porterait sinon un | 0 pour un
débordement qui est un bug partout ailleurs, donc un int.MaxValue + 1 ordinaire garde le compte du
double, une limite documentée. Un contexte checked (le bloc, l'expression, ou le réglage de tout le
projet, lu depuis l'IsChecked de l'arbre lié plutôt que depuis la syntaxe) lève un débordement
exactement là où C# le lève. Un float est un simple partout où il est PRODUIT, chaque + - * /,
chaque incrément et affectation composée sur une cible float (une float? n'est pas encore
arrondie), un int au-delà de 2^24 sur son chemin vers l'intérieur, une constante float, une valeur
hydratée du serveur, parce que RyuJIT arrondit chaque opération float, et ainsi a*x - b*x répond le
dernier bit du serveur dans le navigateur aussi. Il s'imprime comme le plus court décimal qui se relit
comme le même simple, donc 0.1f + 0.2f vaut "0.3" des deux côtés. char++ avance le caractère ; un
enum dans une arithmétique (day + 1, a.CompareTo(b)) calcule sur la valeur derrière le nom du
membre.
La syntaxe ne montre jamais une conversion implicite (int i = c avec un char, Twice(c), a[c],
long l = n) et une règle écrite contre la syntaxe ne couvre que les formes dont son auteur s'est
souvenu. Le compilateur les lit depuis l'arbre lié de Roslyn à la place : après que chaque expression
est traduite, la conversion dans laquelle l'arbre lié l'enveloppe est appliquée (ValueFlow), à
chaque site où C# l'applique : initialiseurs, arguments, index, retours, comparaisons. Un char promu
en nombre devient son unité de code (une constante se replie : 'A' + col est 65 + col) ; un int
qui coule vers un long devient un BigInt (1 → 1n, ce qui est aussi pourquoi
TimeSpan.FromSeconds(90) émet 90n : la surcharge de .NET 9 prend un long). Deux chars comparés
restent des caractères : JavaScript ordonne les chaînes de longueur 1 par les mêmes unités de code.
Une conversion définie par l'utilisateur traverse comme un appel au statique que son jumeau porte, un
type propre à une app comme Money.fromInt(5), un type du vocabulaire comme
IconGlyph.fromIcons('search') pour Icon(Icons.Search), sauf si son jumeau prend l'opérande tel
quel : SizeValue depuis un nombre déclare exactement cela, donc Width = 120 reste un 120
ordinaire, et un type venu de l'extérieur du SDK (Index) passe son primitif directement.
Une valeur sur son chemin vers du TEXTE est réglée de la même façon : encadrée dans une concaténation,
un opérande chaîne de l'une, ou un trou d'interpolation ordinaire, elle s'imprime comme C# l'imprime,
un nul comme rien, un bool comme "True", un enum par le nom de son membre, s += flag compris, qu'aucune
règle de syntaxe n'avait vu.
JavaScript ne peut pas surcharger +, donc un record ou un struct que vous déclarez avec des
opérateurs porte chacun dans son jumeau comme une méthode statique, et chaque site où l'arbre lié
montre un opérateur l'appelle : a + b est Money.opAdd(a, b), -m est Money.opNegate(m) (le -
unaire et le binaire sont nommés par arité, donc ils n'entrent jamais en collision), m += other est
m = Money.opAdd(m, other). Les conversions aussi : implicit operator Money(int v) devient
Money.fromInt, appelé partout où C# convertit, une déclaration, un argument, l'opérande d'un
composé, et explicit operator int(Money m) devient Money.toInt, appelé par le cast. Seulement pour
les types de votre source : une enveloppe du framework comme SizeValue ou Index est son primitif
de ce côté, et ses opérateurs passent la valeur directement.
L'ÉLÉMENT d'un foreach se convertit de la même façon, un élément à la fois :
foreach (long l in ints) fait de chaque int un BigInt, foreach (int code in chars) de chaque char
son unité de code, foreach (Money m in ints) appelle la conversion du type : la boucle lie un $l
brut et déclare l depuis lui.
Une DÉCLARATION using (using var r = …;) possède le reste de son bloc : ce qui suit tourne dans un
try dont le finally libère r, après qu'une valeur de retour est prise, quand le corps lève, dans
l'ordre inverse quand plusieurs partagent un bloc, et await using attend disposeAsync.
Un dictionnaire est la classe de dictionnaire du runtime, qui tient ses entrées par case comme le
Dictionary de .NET : il s'énumère dans l'ordre d'insertion tant que rien n'est retiré, et une entrée
ajoutée après un retrait prend la case de celle retirée, la dernière libérée en premier. foreach sur
un (et new List<KeyValuePair<,>>(dict), qui l'étale) produit des paires qui se déstructurent comme
[key, value] ET répondent .key/.value (les deux formes de consommation de C#), chaque clé dans
son propre type, donc le key + 1 suivant additionne. Un dictionnaire trié produit les mêmes paires,
dans l'ordre des clés. Depuis 0.2.0-preview.60.
new List<KeyValuePair<int,float>>(…) ne peut pas annoter son let avec le nom C# nu
(KeyValuePair[] ne nomme rien en TS) ; les éléments génériques laissent l'annotation à l'inférence.
C# initialise les statiques d'un type ensemble : chacun part de son zéro, puis les initialiseurs
tournent dans l'ordre de déclaration, puis le constructeur statique, une fois, au premier usage. Un
type dont les statiques peuvent s'observer l'un l'autre (un initialiseur qui n'est pas une constante,
ou un constructeur statique) les tient tous dans un seul objet $slots qu'$init() bâtit la première
fois que l'un d'eux est lu ou écrit, et chaque statique est une paire d'accesseurs sur sa case, donc
static int A = B + 1; au-dessus de static int B = 2; lit le zéro de B et répond 1, comme .NET le
fait, et deux types dont les statiques se lisent l'un l'autre voient le zéro que .NET voit. Le même
mécanisme sert un record, un struct, une classe, une classe statique et un composant. Étant paresseux,
il survit aussi aux cycles d'import de la bibliothèque partagée (ses modules s'importent l'un l'autre
par un seul tonneau, et celui qui charge en premier voit la classe de l'autre comme undefined), et
c'est à cela que servait le getter paresseux par statique qu'il remplace ; ce getter initialisait
chaque statique à sa propre première lecture, dans aucun ordre. Un type dont les initialiseurs sont
des constantes garde des champs ordinaires, chacun écrit comme la VALEUR vers laquelle C# le replie
(static readonly int Max = Default * 2; au-dessus de const int Default = 50; est 100, là où
l'expression lisait Default avant que sa déclaration ne le définisse), et un statique dont le zéro
bâtit un jumeau est bâti au premier usage aussi, jamais pendant que son module est évalué. Un type qui
déclare un constructeur statique démarre aussi l'initialisation d'abord dans son constructeur
d'instance et dans chaque méthode, accesseur et opérateur statiques, puisque C# fait tourner ce
constructeur avant la première instance et le premier usage de tout membre statique. Les statiques
s'initialisent dans l'ordre de déclaration à travers les champs, les propriétés et les événements de
type champ ; $slots existe avant que rien ne tourne, donc une lecture réentrante voit le zéro que C#
voit ; le corps du constructeur statique tourne dans une fonction à lui après les initialiseurs, donc
un return dedans le termine seul ; et un échec est gardé, chaque usage du type levant alors une
TypeInitializationException dont l'exception interne est l'originale, comme .NET le fait. Depuis
0.2.0-preview.61.
Le .ts émis est vérifié en types (c'est la seconde des deux couches), donc une signature ne doit
jamais introduire un nom que le module ne peut pas résoudre :
| C# | TypeScript |
|---|---|
| un enum |
string, puisque sa représentation à l'exécution est le nom du membre |
| une interface sans jumeau émis | any |
IReadOnlyList<(char, char)> |
[string, string][] |
Action<T>? |
((t: T) => void) | null, entre parenthèses, sinon l'union se lie au retour |
char |
string |
| un nom que rien ne peut vérifier |
any, parce qu'un mauvais type est pire qu'un type ouvert |
Les records portent les mêmes règles, plus leurs champs statiques et leurs propriétés calculées (un record est une valeur avec un COMPORTEMENT, pas seulement ses membres positionnels).
L'ensemble transpilé et le tonneau d'exportation du runtime sont tous deux générés depuis le répertoire de sources, jamais depuis un registre tenu à la main, donc la bibliothèque embarquée ne peut jamais s'écarter du code dont elle est bâtie.
Source C# :
private void Increment() {
Count++;
if (Count > 10) Console.WriteLine("Max reached");
}Sortie TypeScript :
increment() {
this.count++;
if (this.count > 10) console.log("Max reached");
}Source C# :
public enum OrderStatus { Pending, Processing, Shipped, Delivered }
private void HandleStatusChange(string input)
{
// Parse enum from string (case-insensitive)
if (Enum.TryParse<OrderStatus>(input, ignoreCase: true, out var status))
{
Console.WriteLine($"Status changed to: {status}");
}
// Get all enum values for dropdown
var allStatuses = Enum.GetValues<OrderStatus>();
foreach (var s in allStatuses)
{
Console.WriteLine($"Available status: {s}");
}
// Validate enum value
if (Enum.IsDefined(typeof(OrderStatus), "Shipped"))
{
Console.WriteLine("Valid status");
}
}Sortie TypeScript (shape représente la forme de l'enum, qu'eqc écrit à chaque appel :
{ names: ['Pending', 'Processing', 'Shipped', 'Delivered'], keys: ['pending', 'processing', 'shipped', 'delivered'], values: [0, 1, 2, 3], flags: false, digits: 8 }):
handleStatusChange(input: string) {
let status: any;
// Parse with TryParse: the enum's default when it fails, as .NET leaves it
if (((status = $eq.enums.tryParse(input, shape, true)) !== undefined || ((status = $eq.enums.zero(shape)), false))) {
console.log(`Status changed to: ${$eq.enums.text(status, shape)}`);
}
// Get all values, in the order of the values
let allStatuses = $eq.enums.values(shape);
for (const s of allStatuses) {
console.log(`Available status: ${$eq.enums.text(s, shape)}`);
}
// Validate a declared name
if ($eq.enums.isDefined('Shipped', shape, 'name')) {
console.log('Valid status');
}
}Source C# :
private Dictionary<string, int> _settings = new();
private void ManageSettings()
{
// Add entries
_settings.Add("timeout", 5000);
_settings.Add("retries", 3);
// Check existence
if (_settings.ContainsKey("timeout"))
{
var timeout = _settings["timeout"];
Console.WriteLine($"Timeout: {timeout}");
}
// Safe retrieval
if (_settings.TryGetValue("maxItems", out var max))
{
Console.WriteLine($"Max: {max}");
}
// Iterate keys
foreach (var key in _settings.Keys)
{
Console.WriteLine($"{key} = {_settings[key]}");
}
// Clear all
_settings.Clear();
}Sortie TypeScript :
constructor() {
this._settings = $eq.collections.dictionary();
}
declare _settings: any;
manageSettings() {
let max: any;
this._settings.set('timeout', 5000);
this._settings.set('retries', 3);
if (this._settings.has('timeout')) {
let timeout = $eq.mapGet(this._settings, 'timeout');
console.log(`Timeout: ${timeout}`);
}
if ((($0: any) => ($0.has('maxItems') ? ((max = $0.get('maxItems')), true) : ((max = 0), false)))(this._settings)) {
console.log(`Max: ${max}`);
}
for (const key of this._settings.keys()) {
console.log(`${key ?? ''} = ${$eq.mapGet(this._settings, key)}`);
}
this._settings.clear();
}Source C# :
private void ProcessCollections()
{
var list1 = new[] { 1, 2, 3, 4 };
var list2 = new[] { 3, 4, 5, 6 };
// Concatenate two lists
var combined = list1.Concat(list2);
// Result: [1, 2, 3, 4, 3, 4, 5, 6]
// Union - unique elements from both
var union = list1.Union(list2);
// Result: [1, 2, 3, 4, 5, 6]
// Intersect - common elements
var common = list1.Intersect(list2);
// Result: [3, 4]
// Except - elements in list1 but not in list2
var difference = list1.Except(list2);
// Result: [1, 2]
// Complex filtering with set operations
var activeUsers = GetActiveUsers();
var premiumUsers = GetPremiumUsers();
// Users that are both active AND premium
var activePremium = activeUsers.Intersect(premiumUsers);
// Users that are active but NOT premium
var activeFree = activeUsers.Except(premiumUsers);
}Sortie TypeScript :
processCollections() {
const list1 = [1, 2, 3, 4];
const list2 = [3, 4, 5, 6];
// Concatenate
const combined = [...list1, ...list2];
// Union (with Set to remove duplicates)
const union = [...new Set([...list1, ...list2])];
// Intersect (common elements)
const common = [...new Set(list1)].filter(x => list2.includes(x));
// Except (difference)
const difference = [...new Set(list1)].filter(x => !list2.includes(x));
// Complex filtering
const activeUsers = this.getActiveUsers();
const premiumUsers = this.getPremiumUsers();
const activePremium = [...new Set(activeUsers)].filter(x => premiumUsers.includes(x));
const activeFree = [...new Set(activeUsers)].filter(x => !premiumUsers.includes(x));
}Source C# :
private void ProcessArrayOperations()
{
var numbers = new[] { 5, 2, 8, 1, 9 };
var items = new[] { "apple", "banana", "cherry" };
// Sort array in place
Array.Sort(numbers);
// Result: [1, 2, 5, 8, 9]
// Sort with custom comparison
Array.Sort(items, (a, b) => b.Length - a.Length);
// Result: ["banana", "cherry", "apple"]
// Reverse array
Array.Reverse(numbers);
// Result: [9, 8, 5, 2, 1]
// Find operations
var firstEven = Array.Find(numbers, n => n % 2 == 0);
var firstEvenIndex = Array.FindIndex(numbers, n => n % 2 == 0);
var allEvens = Array.FindAll(numbers, n => n % 2 == 0);
// Search operations
var index = Array.IndexOf(numbers, 5);
var lastIndex = Array.LastIndexOf(numbers, 5);
// Check operations
var hasEven = Array.Exists(numbers, n => n % 2 == 0);
var allPositive = Array.TrueForAll(numbers, n => n > 0);
// Clear and resize
Array.Clear(numbers);
Array.Resize(ref items, 5); // Expand to 5 elements
}Sortie TypeScript :
processArrayOperations() {
const numbers = [5, 2, 8, 1, 9];
const items = ["apple", "banana", "cherry"];
// Sort
numbers.sort();
// Sort with comparison
items.sort((a, b) => b.length - a.length);
// Reverse
numbers.reverse();
// Find operations
const firstEven = numbers.find(n => n % 2 == 0);
const firstEvenIndex = numbers.findIndex(n => n % 2 == 0);
const allEvens = numbers.filter(n => n % 2 == 0);
// Search operations
const index = numbers.indexOf(5);
const lastIndex = numbers.lastIndexOf(5);
// Check operations
const hasEven = numbers.some(n => n % 2 == 0);
const allPositive = numbers.every(n => n > 0);
// Clear and resize
numbers.splice(0);
items.length = 5;
}Source C# :
private void FilterByType()
{
// Mixed type collection
object[] mixed = new object[] { 1, "hello", 2, "world", 3.14, true };
// Cast<T>() - assumes all elements are of type T (passthrough in JS)
var assumedStrings = mixed.Cast<string>();
// OfType<T>() - filters to only elements of type T
var onlyStrings = mixed.OfType<string>();
// Result: ["hello", "world"]
var onlyNumbers = mixed.OfType<int>();
// Result: [1, 2]
// Works with custom classes too
var shapes = new object[] { new Circle(), new Square(), new Circle() };
var circles = shapes.OfType<Circle>();
// Result: [Circle, Circle]
// Primitive type filtering
var primitives = new object[] { 1, "text", 2.5, true, null };
var strings = primitives.OfType<string>(); // ["text"]
var numbers = primitives.OfType<double>(); // [1, 2.5]
var booleans = primitives.OfType<bool>(); // [true]
}Sortie TypeScript :
filterByType() {
// Mixed type collection
const mixed = [1, "hello", 2, "world", 3.14, true];
// Cast - passthrough (JS is dynamically typed)
const assumedStrings = mixed;
// OfType - filter by typeof for primitives
const onlyStrings = mixed.filter(x => typeof x === 'string');
// Result: ["hello", "world"]
const onlyNumbers = mixed.filter(x => typeof x === 'number');
// Result: [1, 2, 3.14]
// OfType - filter by instanceof for objects
const shapes = [new Circle(), new Square(), new Circle()];
const circles = shapes.filter(x => x instanceof Circle);
// Result: [Circle, Circle]
// Primitive filtering
const primitives = [1, "text", 2.5, true, null];
const strings = primitives.filter(x => typeof x === 'string'); // ["text"]
const numbers = primitives.filter(x => typeof x === 'number'); // [1, 2.5]
const booleans = primitives.filter(x => typeof x === 'boolean'); // [true]
}Source C# :
private void DemonstrateModernOperators()
{
// Null-coalescing assignment (??=)
string? cachedData = null;
cachedData ??= LoadDataFromDatabase(); // Only loads if null
cachedData ??= "Default"; // Won't execute, already assigned
// Property null-coalescing assignment
if (user.Settings ??= new Settings())
{
Console.WriteLine("Created new settings");
}
// nameof operator (useful for property binding, validation)
var propertyName = nameof(user.Email);
Console.WriteLine($"Validating {propertyName}"); // "Validating Email"
var methodName = nameof(ProcessOrder);
LogAction(methodName); // "ProcessOrder"
// default keyword - type-safe default values
int count = default(int); // 0
string? text = default(string); // null
bool flag = default(bool); // false
DateTime date = default(DateTime); // 1/1/0001 12:00:00 AM
// default literal (contextual)
int number = default; // 0 (inferred from type)
ProcessData(default); // passes default value for parameter type
}
private void ProcessData(int value = default)
{
// value defaults to 0 if not provided
}Sortie TypeScript :
demonstrateModernOperators() {
// Null-coalescing assignment
let cachedData = null;
cachedData ?? (cachedData = this.loadDataFromDatabase());
cachedData ?? (cachedData = 'Default');
// Property assignment
if (this.user.settings ?? (this.user.settings = new Settings())) {
console.log('Created new settings');
}
// nameof operator
const propertyName = 'Email';
console.log(`Validating ${propertyName}`);
const methodName = 'ProcessOrder';
this.logAction(methodName);
// default keyword
let count = 0;
let text = null;
let flag = false;
let date = $eq.time.dateTime.minValue();
// default literal
let number = 0;
this.processData(0);
}
processData(value = 0) {
// value defaults to 0
}Source C# :
private void StringManipulation()
{
var text = " Hello World ";
// Trimming
var trimmed = text.Trim(); // "Hello World"
var leftTrim = text.TrimStart(); // "Hello World "
var rightTrim = text.TrimEnd(); // " Hello World"
// Case conversion
var upper = text.ToUpper(); // " HELLO WORLD "
var lower = text.ToLower(); // " hello world "
var upperInv = text.ToUpperInvariant(); // " HELLO WORLD "
var lowerInv = text.ToLowerInvariant(); // " hello world "
// Chaining methods
var clean = text.Trim().ToLower().Replace("world", "everyone");
// Result: "hello everyone"
}Sortie TypeScript :
stringManipulation() {
const text = " Hello World ";
// Trimming
const trimmed = $eq.text.trim(text);
const leftTrim = $eq.text.trimStart(text);
const rightTrim = $eq.text.trimEnd(text);
// Case conversion
const upper = text.toUpperCase();
const lower = text.toLowerCase();
const upperInv = text.toUpperCase();
const lowerInv = text.toLowerCase();
// Chaining
const clean = $eq.text.trim(text).toLowerCase().replaceAll("world", "everyone");
}Le rognage passe par le runtime parce que le trim de JavaScript est un ensemble différent de celui de .NET : il laisse U+0085 NEXT LINE, que .NET rogne, et prend U+FEFF, que .NET garde. char.IsWhiteSpace, string.IsNullOrWhiteSpace et un Split() nu lisent la même liste, donc un texte se rogne, se découpe et se tokenise pareil des deux côtés (Depuis 0.2.0-preview.58).
🏁 Commencer ici
🏗️ Architecture
- Vue d'ensemble de l'architecture
- Composants write-once
- Surface déclarative
- Architecture des paquets
- Composants
- Styles
- Localisation
- Analytics et GTM
📱 Write-once
- Moteur Photon
- Design System
- Capacités
- Stockage
- Formulaires
- Éditeur de code
- Markdown
- Mermaid
- Rendu des e-mails
⚙️ Compilation
- Compilateur
- Fonctionnalités C# prises en charge
- Résolution des types externes
- Flux de build
- Diagnostics
⚡ Runtime
🔌 Serveur
🎨 Écosystème
- Image · Graphiques · Icônes · Lottie
🚀 Développement