Skip to content

SupportedFeatures es

Edgar Mesquita edited this page Oct 14, 2026 · 7 revisions

Matriz de funciones de C# compatibles

🌐 Esta página en: English · Português · Español · Français · Deutsch

Este documento ofrece una lista completa de las funciones de C#, las APIs de .NET y los patrones que admite el compilador de eQuantic.UI.

Leyenda

✅ Soporte completo: se transpila a un comportamiento equivalente en JavaScript.

⚠️ Soporte parcial: funciona con salvedades o pequeñas diferencias.

❌ Sin soporte: lógica fundamentalmente incompatible (p. ej., E/S bloqueante, punteros unsafe).


🏗️ Funciones centrales del lenguaje

Función Estado Notas
Clases y structs ✅ Se transpilan a clases ES6. Una clase simple tiene un módulo propio declare lo que declare, incluida una vacía sobre los miembros por defecto de una interfaz o sobre una base, y todo módulo que la nombra la importa; una clase cuya cadena de bases llega a un atributo o a un tipo [ServerOnly] no tiene ninguno, tanto tres niveles más abajo como uno (class Underline : Mark sobre class Mark : Attribute), incluida una base de una biblioteca referenciada. Una clase de excepción de la aplicación es una clase, con un módulo propio (vea try / catch / finally). La cadena se lee por aquello de lo que deriva cada base y nunca por su nombre, así que una clase sobre una interfaz con nombre de atributo conserva su módulo, y un nombre es un módulo dondequiera que cualquier declaración suya lo sea. Una clase, una clase estática, un record o un struct declarados dentro de otro tipo también son un módulo propio, nombrado por los tipos que lo contienen y su propio nombre unidos por $ (Cart$Item, A$B$C), que ningún tipo de nivel superior puede tomar: toda referencia a él, dentro de su dueño o fuera, nombra ese gemelo, así que una clase anidada junto a una clase de nivel superior de su mismo nombre nunca se toma por ella, y una dentro de una clase solo de servidor o de un atributo no tiene ninguno, como no lo tiene su dueño: el código de cliente que la nombra se rechaza (EQ2010), y una página declarada en ella no tiene ruta, mientras que una dentro de una clase de excepción cruza como cruza esa clase (Desde 0.2.0-preview.61). Un record y un struct tienen un gemelo declaren lo que declaren, salvo que estén marcados con [ServerOnly], incluido un record sobre un record solo de servidor (Desde 0.2.0-preview.61).
Interfaces ✅ Se usan para la comprobación de tipos de TypeScript (se borran en runtime). Un miembro por defecto (una propiedad, un indexador o un método con cuerpo; un indexador desde 0.2.0-preview.61) llega al gemelo de toda clase que lo toma sin declararlo: convertido desde la fuente de la interfaz o, para las interfaces propias del SDK que implementa una aplicación (un tema, un lenguaje de código), delegado a la copia del runtime. Una interfaz de cualquier otro ensamblado compilado no tiene ni lo uno ni lo otro, y el build rechaza la clase (EQ1008) (Desde 0.2.0-preview.59).
Enums ✅ Cruzan como su nombre en camelCase (MainAlign.Start → 'start'), y un enum [Flags] como su número. .ToString(), la interpolación y la concatenación dan el nombre del miembro de C# ("Start"), el mismo texto que imprime el servidor, los flags activos de una combinación ("Read, Write"), un valor que ningún miembro nombra como sus dígitos, y nada para un enum anulable nulo. ToString("D"), "X", "F" y "G", el formato y la alineación de una interpolación escriben lo que escribe .NET, Enum.Parse, TryParse, GetName, GetNames, GetValues e IsDefined responden como los de .NET, y un diccionario con clave de enum cruza en los dos sentidos, rechazándose un nombre que ningún miembro tiene. Desde 0.2.0-preview.15; los formatos, el texto de los flags, los estáticos de Enum y las claves de enum desde 0.2.0-preview.60.
Genéricos ✅ Totalmente soportados (se borran en runtime).
Métodos de extensión ✅ Se resuelven mediante el Semantic Model y se transpilan a llamadas directas.
Async / Await ✅ Se mapean a async / await y Promise.
Expresiones lambda ✅ Se mapean a funciones flecha () => {}.
Coincidencia de patrones ✅ Patrones is, patrones de propiedad, patrones recursivos. Un tipo sin nada enlazado (o is int or long, int => …, case int:) comprueba el tipo, un long como BigInt, un entero como número entero y un char como una cadena de una unidad de código. Una constante en un patrón, incluida x is Limits.Max, se comprueba por su valor: un null casa con cualquier ausencia, un decimal y un NaN se comparan por valor, y una constante long es un BigInt, como lo es todo long (Desde 0.2.0-preview.60). Un patrón posicional sobre un tipo cuyo Deconstruct escribió la aplicación, incluido el de una extensión, lo llama una vez por cada valor que una prueba deconstruye, también entre los brazos de un switch y las alternativas de un or, como hace .NET, y lee las partes que devuelve, como hace una deconstrucción: antes leía los miembros que dan nombre a los outs, y una parte que calcula el Deconstruct no es uno de ellos (Desde 0.2.0-preview.61).
Interpolación de cadenas ✅ Se mapea a template literals `${var}`. Un formato con texto entre comillas ({date:dd 'de' MMMM}) conserva sus comillas, y una cadena interpolada raw conserva sus llaves ($$$"""{{x}}""" es {{x}}) (Desde 0.2.0-preview.60). Una alineación es el número que es, incluidas una constante por su nombre o una expresión de constantes ({name,Widths.Name}, {name,-(Widths.Name + 1)}), también en un StringBuilder (Desde 0.2.0-preview.62).
Literales de cadena y de char ✅ Una cadena o un char conserva el valor que lee C#, se escriba como se escriba: un surrogate suelto ("x\uD83D"), un carácter de control y los escapes que solo tiene C# ('\a', '\e', '\x041', '\U00000041'). Un literal UTF-8 ("ab"u8) no tiene gemelo y hace fallar el build (EQ1004) (Desde 0.2.0-preview.60).
Null-coalescing ✅ ?? y ??= mapeados a sus equivalentes de JS.
Inicializadores de objeto ✅ new Obj { Prop = 1 } sobre un record, un struct, una clase simple, una excepción o un tipo del vocabulario cuyo gemelo transpila el runtime (los modelos de la hoja de cálculo y de los formularios, CellRef, FieldError, SheetController…), aplicado una vez que el constructor ha retornado, como lo aplica C# (una clase simple lo recibía como un objeto de configuración final, una excepción lo descartaba, y un tipo del vocabulario transpilado lo entregaba a una ranura que su gemelo no tiene, antes de la 0.2.0-preview.61; un componente lo recibe como sus props): todos los inicializadores del tipo ya se ejecutaron, incluido el del propio miembro, y los valores se leen después de ellos. Un inicializador de colección anidado (Items = { 1, 2 }) añade a la colección que guarda el miembro, a través del Add que vincula el árbol enlazado (el de un método de extensión a través de la clase que lo aloja, y el de una lista, un conjunto o un miembro ICollection<T> como lo es una llamada a él); un inicializador de objeto anidado (Inner = { N = 3 }) asigna dentro del objeto que guarda el miembro; y una entrada ([key] = value) se escribe a través del indexador del tipo. El par de un diccionario (Map = { { "a", 1 } }) es su Add, que rechaza una clave que ya está. Cada parte se evalúa en el método en el que se escribió, así que un await en un elemento funciona, y cada elemento se aplica antes de que se evalúen las partes del siguiente, leyéndose el miembro al que añade o en el que asigna antes que sus propias partes, como lo aplica C#. Un elemento sin lowering hace fallar el build (EQ1004) (Desde 0.2.0-preview.61).
field (C# 14) ✅ Propiedades con guarda: el gemelo recibe una ranura $name (un nombre que ningún campo de C# puede tomar), un getter y el propio cuerpo del accesor. Una estática conserva su ranura en el tipo, incluida la de un record, y la ranura empieza con el inicializador de la propiedad o con el valor por defecto de su tipo (Desde 0.2.0-preview.60). La de instancia de un record y de un struct ejecuta los cuerpos de sus accesores sobre la ranura, como la de una clase, y una cuyo getter tiene cuerpo es estado que su record compara y copia; antes de la 0.2.0-preview.61 se guardaba bajo el nombre de la propiedad, y se descartaba el cuerpo de su setter (Desde 0.2.0-preview.61).
Propiedades virtuales y override ✅ Una propiedad que un tipo derivado puede sobrescribir o que sobrescribe otra (una autopropiedad declarada virtual u override) guarda su valor en un almacén propio ($name) bajo accesores en el prototipo del gemelo, igual en una clase, un record y un struct, así que toda lectura llega a la sobrescritura y ningún inicializador llega al accesor de otro nivel: una autopropiedad sobre una calculada, o una calculada sobre una autopropiedad, lanzaba en el new o respondía el valor de la base antes de la 0.2.0-preview.61. Una sobrescritura que declara solo el getter de una propiedad cuya base tiene los dos conserva el setter de la base, como lo hereda C#. Un record compara, hashea y copia el almacén, como .NET compara un campo de respaldo. Un gemelo con almacén lo escribe en JSON bajo el nombre de su propiedad, así que una server action recibe la propiedad por su nombre de C#. Queda una diferencia: en un tipo que sobrescribe una autopropiedad con otra, base.Name lee el valor de la sobrescritura, donde .NET lee el campo propio de la base, ya que las dos comparten un almacén (Desde 0.2.0-preview.61).
Miembros que ocultan (new) ✅ Un método que oculta un miembro heredado, con new o con la misma firma y sin override, se alcanza como lo alcanza C#: a través del tipo base responde el método de la base, y a través del tipo que oculta, el que oculta, igual en una llamada, un grupo de métodos, una llamada base y ?., incluido un método genérico sobre su propio parámetro de tipo, y lo mismo pasa con un miembro que el runtime lee por su nombre de .NET, así que ((object)c).GetHashCode() es el de object sobre un public new int GetHashCode(). Su gemelo lo guarda bajo un nombre propio (name$1), y una sobrescritura de un método new virtual ocupa el lugar de ese método. Lo mismo pasa con un método junto a una implementación explícita de interfaz de su nombre, así que una llamada a través de la clase llega a él y una a través de la interfaz, a la explícita. Uno que oculta un miembro, o que está junto a una implementación así, y responde a una interfaz hace fallar el build (EQ1007), y una propiedad o un campo que oculta otro sigue compartiendo su nombre con él (#563, desde 0.2.0-preview.61).
Accesores init ✅ Se emiten como un setter de JS, porque un cuerpo init es donde un tipo declara su invariante, y descartarlo perdería la comprobación en el cliente.
Inicializadores de colección ✅ new List<int> { 1, 2 } se mapea a [1, 2]. Una lista construida con un argumento y un inicializador contiene los dos, como la construye C#: una copia de la fuente, nunca la propia fuente, o nada para una capacidad, y después cada elemento en orden, leyéndose primero la fuente o la capacidad. Una capacidad se sigue evaluando, y una negativa se rechaza como la rechaza el constructor. Así, new List<int>(source) { 3 } es [...source, 3] y new List<int>(10) { 1, 2 } es [1, 2], escritos completos o con tipo de destino, y List<int> c = new(source) es una copia. Una capacidad descartaba los elementos, y una fuente se unía a ellos con una coma, antes de la 0.2.0-preview.61 (Desde 0.2.0-preview.61).
IEnumerable<T> propio ✅ Una clase, un record o un struct que implementa IEnumerable<T>, o solo IEnumerable, se enumera como dice su propio GetEnumerator(), con foreach, un spread, string.Join y todos los operadores de LINQ, cada uno a través del que C# vincula para él: un foreach sobre la clase recorre su propio GetEnumerator() público (incluido el de un enumerador struct), y LINQ, un spread, string.Join y un foreach sobre la interfaz recorren el de IEnumerable<T>, explícito o no, así que los dos pueden recorrer secuencias distintas, como en .NET. Es la secuencia que produce un método iterador, o un enumerador tuyo, incluida una clase que es su propio enumerador, recorrido por su MoveNext y su Current y liberado cuando termina el recorrido, incluido un break. La sobrescritura de una clase derivada responde, el IEnumerable.GetEnumerator() explícito junto al genérico es la misma secuencia, y también lo es un Current explícito junto a uno público. Una clase con solo un GetEnumerator() público también se recorre con foreach. El gemelo tiene una sola iteración y un solo Current, así que un tipo que implementa IEnumerable<T>, o IEnumerator<T>, sobre dos tipos hace fallar el build (EQ1007), y lo mismo un IEnumerable.GetEnumerator() o un Current explícitos que responden de otra manera que el miembro que conserva el gemelo, mientras que uno que devuelve su respuesta (=> GetEnumerator();, => Current;) se trata como uno solo. El propio GetEnumerator() de una colección de .NET (=> items.GetEnumerator()) no tiene traducción y hace fallar el build (EQ2004): produce los elementos con yield en su lugar (#612, desde 0.2.0-preview.61).
Deconstrucción ✅ var (a, b) = tuple se mapea a [a, b] = tuple, y lo mismo (var c, var d) = … y las designaciones anidadas. Un record o un struct se deconstruye a través del Deconstruct que llama, en todas sus formas: una declaración, una asignación ((a, b) = point, (var a, b) = point), un foreach (var (a, b) in points) y una deconstrucción anidada. El de un record lee sus miembros, y se ejecuta un Deconstruct que escribió la aplicación. Los receptores y los índices de los destinos se evalúan antes que el valor, como los evalúa C#, así que (xs[i++], xs[i++]) = point escribe los elementos que escribe C# (Desde 0.2.0-preview.60). Cada parte llega a su destino como la pone ahí C#: convertida al tipo del destino (una parte int en un long), y escrita según lo que es el destino, la entrada de un diccionario a través de su clase, en una asignación, una declaración y un bucle (Desde 0.2.0-preview.60).
Variables de expresión ✅ Lo que declara un patrón, un out var o una deconstrucción se declara donde lo declara C#, en todo tipo de miembro: en el bloque que lo contiene después de un if o de una sentencia de expresión, dentro de un bucle o de un using, y nuevo en cada iteración del bucle y en cada llamada, así que una clausura conserva su propio valor. Un nombre que JavaScript reserva (@class) se renombra una vez, en su declaración y en cada referencia (Desde 0.2.0-preview.59).
Funciones locales ✅ Se transpilan a funciones internas. Un parámetro out o ref devuelve su valor a quien llama, como el de un método (Desde 0.2.0-preview.60).
Bucles for ✅ 1:1. La variable de un bucle es una por bucle, como en C#: una clausura creada en el cuerpo lee su valor actual, nunca una copia propia de su iteración, y lo mismo ocurre con lo que declara su inicializador (out var n, var (i, j) = (0, 3)), incluida una cabecera de expresiones (for (Seed(out var n); …)) (Desde 0.2.0-preview.60).
Delegados ✅ Se llaman como valores, por nombre, a través de un miembro o desde cualquier expresión (handlers[0](x), Make()(x), desde 0.2.0-preview.60).
Records (with) ✅ Una copia del record sobre su propia clase, y después los miembros que nombra la expresión: como la copia de .NET, no ejecuta ningún inicializador ni ningún constructor. El with de un struct, y el de los records del motor de código, pasan por la copia propia del gemelo. Un record cuya cadena declara un constructor de copia se copia a través de él, como el with de C#: uno declarado empieza sus miembros en su cero, ejecuta la copia de su base con lo que pasa : base(…) y después su cuerpo, y un nivel sintetizado copia sus propios miembros (Desde 0.2.0-preview.61).
Constructores ✅ El constructor gemelo de un record, de un struct o de una clase simple es el constructor de C# que vincula la llamada, alcanzándose cada uno por las cantidades de argumentos que toma, contando sus parámetros opcionales (una clase simple conservaba solo el más ancho antes de la 0.2.0-preview.61). Uno que hace su propio trabajo (el primario, o uno explícito que no encadena) vincula sus parámetros, ejecuta todos los inicializadores en orden de declaración, igual el de un campo que el de una propiedad, los de un tipo derivado antes del constructor de su base, llama al constructor de su base con sus propios argumentos y ejecuta su cuerpo; uno que encadena con : this(…) ejecuta aquel al que encadena y después su propio cuerpo, incluso cuando el cuerpo de aquel retorna antes de tiempo. Cada argumento cae en el lugar de su parámetro y se evalúa en el orden en que está escrito, igual en new, : this(…), : base(…) y una cláusula de base: uno con nombre, uno opcional omitido, y un array params pasado entero (con nombre o no) o formado a partir de sus elementos; una variable que declara un argumento (out var n) se declara donde este se evalúa, y el parámetro de un constructor es una variable que su cuerpo puede asignar. El constructor de copia de un record no es ninguna rama. Dos constructores que toman la misma cantidad de argumentos hacen fallar el build (EQ1009), y lo mismo un encadenamiento a un constructor que a su vez encadena y : this() al constructor implícito de un struct junto a constructores propios. El cero de un struct (default, la posición de un array, new S() sin constructor propio, incluidos el de un struct genérico, que guarda el cero de cada argumento de tipo, y el de uno de otro ensamblado) no ejecuta nada de eso, ni siquiera su constructor estático. El constructor primario de una clase simple vincula sus parámetros como el de un record, y uno que un miembro lee se guarda en cada instancia bajo su nombre, así que uno que cae en el nombre de otro miembro hace fallar el build (EQ1007). Queda una diferencia: un constructor de base que lee un miembro de una clase derivada a través de un miembro virtual lo lee sin asignar, donde .NET lo lee inicializado, ya que JavaScript no deja que nada toque la instancia antes de que super() retorne (Desde 0.2.0-preview.61).
Indexadores ✅ Un indexador de instancia es el item(…) y el setItem(…, value) de su gemelo, y todo acceso a él los llama: una lectura, una escritura, una asignación compuesta, ++ sobre un valor de cualquier tipo, ??=, ?[…], el destino de una deconstrucción, una clave desde el final (^1, en la cuenta que nombra su tipo) y la entrada de un inicializador de objeto, evaluándose el receptor y las claves una vez cada uno, cada clave en el lugar de su parámetro (una clave con nombre, una clave opcional omitida, una clave params). Una asignación responde el valor que asignó, haga lo que haga el setter con su copia, y una implementación explícita del indexador de una interfaz responde a los accesos de la interfaz. Un segundo indexador en un tipo, o cualquier miembro en Item o SetItem junto a uno, hace fallar el build (EQ1007) (Desde 0.2.0-preview.61). Un acceso a través de la cara de lista (IList<T>, IReadOnlyList<T>, IList) llega a lo que contenga esa cara cuando se ejecuta, igual al elemento de un array o de una lista que al item y al setItem de un gemelo, en cada una de esas formas, y su Count cuenta un tipo tuyo por su propio Count, nunca por un Length junto a él: la cara leía un subíndice y un length, que un gemelo no responde, antes de la 0.2.0-preview.61 (Desde 0.2.0-preview.61).
Rangos ✅ x[a..b] sobre una cadena, un array o una lista es el slice de JavaScript, y un extremo que pueda ser ^0 se resuelve contra la longitud como lo hace Index.GetOffset. Sobre un tipo tuyo con un Length (o un Count) y un Slice(int start, int length), llama a ese Slice como C# baja el rango: el receptor una vez, después los extremos en el orden en que están escritos, y después la cuenta, leída solo donde un extremo cuenta desde el final o el final se deja abierto. Así, strip[1..3] es Slice(1, 2), donde antes de la 0.2.0-preview.61 llamaba al slice(1, 3) del gemelo. Un rango entregado a un indexador que toma el propio Range hace fallar el build (EQ2004), como lo hace un Range guardado como valor (Desde 0.2.0-preview.61).
Inicialización estática ✅ Los estáticos de un tipo empiezan en su cero, después se ejecutan los inicializadores en orden de declaración, y después el constructor estático, una vez, la primera vez que se lee o se escribe uno de ellos: static int A = B + 1; static int B = 2; deja A en 1, como en .NET, en un record, un struct, una clase, una clase estática y un componente. Un tipo con constructor estático lo ejecuta antes de su primera instancia y del primer uso de cualquiera de sus miembros estáticos, incluidos un método, una propiedad calculada y un operador. Los inicializadores se ejecutan en orden de declaración entre campos, propiedades y eventos de tipo campo, y el cuerpo del constructor estático después de ellos, terminándolo solo a él un return que haya dentro; uno que lanza hace que todo uso del tipo lance una TypeInitializationException, como hace .NET; un estático cuyo inicializador C# pliega a una constante es su valor; y el constructor estático de un componente se ejecuta una vez (Desde 0.2.0-preview.61).
typeof ✅ Se mapea a un literal de cadena con el nombre del tipo.
Tipos base ✅ Una clase extiende el gemelo de su base se escriba como se escriba la base: sin más, con su espacio de nombres, a través de un alias o con global:: (Desde 0.2.0-preview.60).
Nombres de tipo calificados ✅ Un tipo alcanzado a través de su espacio de nombres, de una parte de él, de todo él o de global:: se importa como un tipo alcanzado a través de un using: dentro de App.Chat, Portal.Fold.Text(n) responde lo que responde Fold.Text(n) bajo using App.Portal; (Desde 0.2.0-preview.61).
Llamadas base ✅ Se mapean a la palabra clave super.
using static ✅ Un miembro alcanzado por su nombre simple responde lo que responde su forma calificada: PI es Math.PI, Join(…) es string.Join(…), el High de un enum es Level.High, y Now bajo using static System.DateTime es DateTime.Now (Desde 0.2.0-preview.61; hasta entonces un miembro de .NET fuera de Math, MathF, string, una constante y un enum hacía fallar el build incluso donde su forma calificada se traducía). Un miembro de .NET cuya forma calificada tampoco tiene traducción hace fallar el build (EQ2004) en lugar de llegar al navegador (Desde 0.2.0-preview.60).
El propio Count de un tipo ✅ Un tipo que declara la aplicación o una biblioteca a la que hace referencia lee su propio Count: el modelo de dominio de una biblioteca lo leía como el length de un array, undefined (Desde 0.2.0-preview.60).
cast y as ✅ Se mapean a paso directo/truncamiento en JS.
sizeof ✅ Se mapea a los tamaños de los primitivos de C#.
Métodos anónimos ✅ delegate(...) { ... } se mapea a funciones flecha.
params ✅ Un parámetro REST (...xs). Funcionan las dos formas de llamada de C#: argumentos expandidos y un array pasado entero, que se esparce. Desde 0.2.0-preview.15.
stackalloc ✅ Se mapea a Typed Arrays (p. ej., Int32Array).
yield return ✅ Se mapea a funciones generadoras de JS (function*).
try / catch / finally ✅ Las cláusulas catch son UN solo catch de JavaScript que las prueba en el orden de C#: cada una por su tipo de excepción, incluido un tipo derivado de él, y por su filtro when, que declara sus propias variables (out var n, un patrón) y responde false donde lanza, como el de .NET. Una excepción que ninguna cláusula toma sigue su camino, y throw; relanza la excepción capturada. Una excepción es un Error de JavaScript que lleva sus tipos de .NET, incluidas una clase de excepción propia de la aplicación y Exception e = new("…"), así que e is ArgumentException, un switch sobre excepciones y as también comprueban el tipo, y una excepción que lanza el runtime es del tipo que lanza .NET ahí (un desbordamiento, una clave que falta, un formato incorrecto, una conversión rechazada); un TypeError de JavaScript es una NullReferenceException. Queda una diferencia, la de la plataforma: .NET ejecuta un filtro ANTES de los bloques finally que hay entre el throw y la cláusula, y JavaScript después de ellos. Desde 0.2.0-preview.60. El mensaje de una excepción del framework se compone como lo compone .NET: donde no se dio ningún mensaje, o se dio uno null, el texto que .NET escribe para ese constructor, leído del propio .NET, después (Parameter 'x'), y el valor real y el nombre de un objeto liberado en líneas propias, los mensajes internos de una agregada después del suyo y la frase de un inicializador de tipo, y ParamName, ActualValue, InnerException y TypeName leen lo que tomó el constructor, siendo el InnerException de una agregada su primera interna, y las excepciones de argumento propias del runtime nombran también su parámetro (Desde 0.2.0-preview.61). Un argumento null se rechaza como lo rechaza .NET, con una ArgumentNullException que nombra el parámetro, en Max, Min y ToDictionary de LINQ, en un operador de LINQ al que se entrega una secuencia que no es una lista (el second de Concat, el first de Zip), en new Guid(text), char.GetUnicodeCategory(text, i), CancellationToken.Register y CreateLinkedTokenSource, y CompareTo(object) responde 1 para un null, como el de .NET. Un miembro bajado al método propio de JavaScript, entre ellos LINQ sobre una lista, sigue leyendo un null a través del null, una NullReferenceException: la suite de conformidad llama a cada miembro de la superficie traducida con un null para cada parámetro de referencia, y enumera esos en null-argument-gaps.baseline.txt con sus razones (Desde 0.2.0-preview.61). Una clase de excepción de la aplicación es una clase sobre la base de excepciones del runtime, el Error que lleva sus tipos: sus campos, sus propiedades, sus constructores y sus métodos son suyos, y su llamada a la base entrega lo que entrega un new de su base de .NET, así que : base(message, inner) llega a Message y a InnerException, : base("bad", name) sobre ArgumentException lee bad (Parameter 'x') y su ParamName, una clase sobre InvalidOperationException que no da mensaje lee el texto propio de ese tipo, un mensaje que falta sobre Exception es el predeterminado de .NET (Exception of type 'App.Failure' was thrown.), un miembro propio bajo un nombre que guarda la base (un Name, una sobrescritura de Message o de ParamName) responde por él, y una genérica se captura por su construcción (catch (Failed<int>)) desde el momento en que se ejecuta su constructor, incluido un throw this ahí. Un mensaje que falta nombra el tipo como lo nombra .NET en tiempo de ejecución (App.Failed`1[System.Int32], App.Outer+Inner). Se transpila como cualquier clase: una que el navegador nunca ve, cuyos miembros llegan a lo que no puede ejecutar, se marca con [ServerOnly], y dos de sus constructores que toman la misma cantidad de argumentos se rechazan (EQ1009), salvo el que solo llama la serialización de .NET, (SerializationInfo, StreamingContext), que escribe la plantilla de Visual Studio y que el gemelo deja fuera, y una llamada a ese que haría el navegador, un new o la llamada a la base de otro constructor, hace fallar el build (EQ1009). Una excepción construida o comprobada donde un argumento de tipo que lleva es un parámetro de tipo también hace fallar el build (EQ2014), ya que JavaScript lo borra: new Failed<T>() en un método genérico, catch (Failed<T>), new Missing() dentro de la clase genérica que la declara. Una declarada dentro de otro tipo también es una clase, con su gemelo nombrado por su dueño (Holder$Nested), y también lo es lo que declara una clase de excepción, un record del motivo que lleva (Rejected$Reason). En un fragmento compilado por sí solo, sin proyecto del que leer sus bases, una clase de excepción de la aplicación sigue siendo el Error del runtime con sus tipos y ninguno de sus miembros (#611, desde 0.2.0-preview.61).
Expresiones throw ✅ x ?? throw …, c ? a : throw … y => throw …, evaluándose la excepción solo donde se lanza, incluido un await dentro de ella. Desde 0.2.0-preview.60.
lock ⚠️ Se transpila solo a su cuerpo (JS es de un solo hilo). Su expresión se ejecuta una vez, delante de él, salvo que solo lea una variable, un campo o this (Desde 0.2.0-preview.60; una que declara una variable, desde 0.2.0-preview.59). new object(), la puerta habitual, es un objeto nuevo, con o sin un inicializador vacío.

🔄 Soporte de LINQ

El compilador incluye estrategias especializadas para casi todos los métodos de LINQ.

Lógica Métodos Estado
Filtrado Where, OfType ✅
Proyección Select, SelectMany, Cast ✅
Particionado Skip, Take, SkipWhile, TakeWhile ✅
Ordenación OrderBy, OrderByDescending, ThenBy, Reverse ✅
Agregación Count, Sum, Min, Max, Average, Aggregate ✅
Cuantificadores Any, All, Contains ✅
Conjuntos Distinct, DistinctBy, Union, Intersect, Except, Concat ✅
Elementos First, FirstOrDefault, Single, Last, ElementAt ✅
Utilidades SequenceEqual, DefaultIfEmpty ✅
Generación Enumerable.Range, Enumerable.Repeat, Enumerable.Empty ✅
Conversión ToList, ToArray, ToDictionary, ToHashSet ✅
Agrupación/Join GroupBy, Join, Zip ✅

Todo operador lee su fuente como la secuencia que enumera .NET, la guarde como la guarde el navegador: un array o una lista tal como son, un HashSet, los pares de un diccionario y el conjunto ordenado, el diccionario ordenado, la lista ordenada, la cola, la pila y la lista enlazada del runtime por su propia enumeración (una pila desde su cima), y una cadena por sus chars, las unidades de código UTF-16. ToList y ToArray siempre crean un array nuevo, así que una copia y su fuente son dos listas. El foreach y el ToCharArray de una cadena también dan unidades de código, siendo un par de surrogates dos chars, new string(chars) y new string(chars, start, length) construyen su texto, y un estático de char entregado como delegado funciona, igual "a1b2".Count(char.IsDigit) que Count(IsDigit) bajo using static System.Char (Desde 0.2.0-preview.60).

Enumerable.Range y Repeat evalúan sus argumentos una vez, donde lo hace C#, incluido un await entre ellos: Range(Start(), 3) llama a Start una vez, y Repeat(new List<int>(), 3) repite una sola lista. Una cuenta negativa, o un Range que pase de int.MaxValue, lanza la ArgumentOutOfRangeException de .NET (Desde 0.2.0-preview.60).

Un comparador entregado a ToDictionary, ToLookup, GroupBy, Distinct, ToHashSet o SequenceEqual pasa la valla que pasa el constructor de una colección. Uno que pide la igualdad por defecto del tipo de la clave (EqualityComparer<T>.Default, un comparador null, o StringComparer.Ordinal para una cadena) responde como responde el operador sin él, cada selector en su parámetro, tenga nombre o no, y cualquier otro, como StringComparer.OrdinalIgnoreCase, hace fallar el build con EQ2007. ToDictionary y GroupBy rechazaban todo comparador, ToLookup llamaba a uno como selector de elementos y Distinct descartaba lo que se le entregara, antes de la 0.2.0-preview.61 (Desde 0.2.0-preview.61). SequenceEqual respondía su primera secuencia cuando se le entregaba un comparador, antes de la 0.2.0-preview.62 (Desde 0.2.0-preview.62).

Cada sobrecarga responde como su propia sobrecarga de .NET, cada argumento en el parámetro al que se vincula, con nombre o sin él. FirstOrDefault(defaultValue), LastOrDefault(defaultValue) y SingleOrDefault(defaultValue), con predicado o sin él, responden el elemento cuando lo hay, incluido un elemento null, y el valor entregado en otro caso, y SingleOrDefault rechaza un segundo elemento o una segunda coincidencia con la InvalidOperationException de .NET. Zip(second) responde pares y Zip(second, third) tríos, leídos como First, Second y Third o deconstruidos, tantos como contenga la secuencia más corta. Average calcula la media en el tipo del elemento: un int o un long sumados exactamente en un long, con una OverflowException más allá de long.MaxValue, y divididos como double, una media de float redondeada a single, una de decimal como decimal, una anulable solo sobre sus valores y null cuando no tiene ninguno, y una secuencia vacía rechazada con una InvalidOperationException donde la respuesta no es anulable. SequenceEqual compara por la igualdad por defecto del tipo del elemento: un long y un decimal por valor, un record por sus miembros, una clase por su propio Equals y por referencia donde no lo tiene, un array por referencia, y NaN igual a sí mismo. Antes de la 0.2.0-preview.62 un valor por defecto se llamaba como predicado, la tercera secuencia de un Zip como selector de resultado, y una media de longs lanzaba un TypeError (Desde 0.2.0-preview.62).


📦 Tipos de .NET (BCL)

Mapeamos los tipos comunes de .NET a sus equivalentes en JavaScript.

Primitivos

Tipo de .NET Equivalente en JavaScript
string String
int, double, float Number
bool Boolean
object Object
dynamic any

System.String

  • ✅ Join, Format. string.Join lee cualquier secuencia (un array, una lista, un HashSet, una LinkedList, las claves de un diccionario, una secuencia detrás de una interfaz) y escribe cada valor como lo escribe el ToString de .NET: un bool como True, un enum por el nombre de su miembro, un float por sus propios dígitos, un double en la notación de .NET (1E+21), un null como nada; los valores pasados uno a uno se escriben cada uno según su propio tipo, y un separador null no escribe nada (Desde 0.2.0-preview.61)
  • ✅ IsNullOrEmpty, IsNullOrWhiteSpace
  • ✅ Split, Replace, Substring, Trim. Split responde como la sobrecarga de .NET: un conjunto de separadores (un array de chars o de cadenas, o chars escritos uno a uno) divide en cualquiera de ellos, ganando en una posición el primero en el orden del conjunto; un separador único null o vacío no divide nada y un conjunto null o vacío divide en el espacio en blanco de .NET; la cuenta limita las entradas, quedándose la última con el resto; TrimEntries y RemoveEmptyEntries se aplican, escritos tal cual o guardados en una variable; y una cuenta negativa o una opción no definida lanza la excepción de .NET (Desde 0.2.0-preview.62)
  • ✅ ToLower, ToUpper y su par invariante pasan a mayúsculas o minúsculas como lo hace .NET 10: cada unidad de código, o par de surrogates, se convierte exactamente en otra (ß y fi se quedan como están, ᾳ pasa a mayúscula como ᾼ, y una sigma mayúscula pasa a minúscula como σ esté donde esté), y las dos íes son las de la cultura: tr y az pasan i a mayúscula como İ e I a minúscula como ı, cualquier otra cultura pasa ı a mayúscula como I e İ a minúscula como i, y la cultura invariante deja İ e ı como están. Una letra cuya mayúscula llegó con Unicode 16, como ƛ, se queda igual, como la deja .NET 10 en un servidor Linux o Windows, cuyo ICU es más antiguo; .NET en macOS 26 sí la cambia, por el ICU de Apple. Un argumento de cultura cruza como CultureInfo.CurrentCulture o un null, que son la cultura de la página, y CultureInfo.InvariantCulture; cualquier otro hace fallar el build (EQ2108) (Desde 0.2.0-preview.62, cuando el navegador alargaba ß a SS, leía una sigma final según su contexto, no conocía ninguna cultura y descartaba la que nombraba la llamada)
  • ✅ StartsWith, EndsWith
  • ✅ Equals, StartsWith, EndsWith, Contains, IndexOf, LastIndexOf y Replace con un StringComparison, según esa comparación, y CompareTo según la cultura actual; una búsqueda por una comparación de cultura hace fallar el build (EQ1004) (Desde 0.2.0-preview.60)

System.DateTime, TimeSpan, DateOnly, TimeOnly y DateTimeOffset

Los tipos temporales se respaldan con tipos compat con precisión de tick (ticks de 100 ns, calendario gregoriano proléptico, no el mapeo con pérdidas a new Date() / milisegundos numéricos). Los constructores, los componentes, Add*, la aritmética (-→TimeSpan), las comparaciones y .ToString() coinciden todos con .NET, escribiéndose el texto de una fecha a través de un solo formateador en la cultura vigente (Desde 0.2.0-preview.61). Un TimeSpan escribe el formato que se le da, igual a través de ToString que de un hueco y de un marcador: c, t y T su forma constante, g y G sus formas generales con el separador decimal de la cultura, un formato personalizado como hh\:mm token a token, y un formato que .NET rechaza, su FormatException con las palabras de .NET (Desde 0.2.0-preview.62, cuando todos los formatos escribían la forma constante); los valores cruzan el cable del SSR como cadenas ISO-8601 / "c" y se hidratan de vuelta en el tipo compat. TryParse(text, out var value) de cada uno de los cinco responde true con el valor que lee su Parse, o false con el valor por defecto del tipo en value para un texto que Parse rechaza y para uno null, como el de .NET (Desde 0.2.0-preview.62, cuando cuatro de ellos hacían fallar la página y el de DateOnly respondía null). Parse y TryParse leen en la cultura que nombra la llamada, como hace .NET: sin proveedor, con CultureInfo.CurrentCulture o con uno null, la cultura de la página, y con CultureInfo.InvariantCulture, la invariante. Leen las formas numéricas que escribe la cultura: una fecha de tres números en el orden de su patrón de fecha corta (01/02/2024 es el 1 de febrero en pt-BR y el 2 de enero en en-US), sus separadores junto a /, - y ., un reloj con su propio separador (el 21.30 de fi-FI, donde tres números cortos unidos por puntos son un reloj y 10.11.12 es 10:11:12) y sus propios designadores de AM y PM junto a AM y PM, antes o después del reloj, el reloj antes o después de la fecha, una fracción después de los segundos con punto o coma, una zona, y la forma ISO (2024-01-15T09:30:00Z). Un TimeSpan se lee como .NET lee uno sin formato: por cuántos números tiene y qué hay entre ellos, contra el patrón invariante d.hh:mm:ss.fffffff y el propio de la cultura, d:hh:mm:ss y su separador decimal, así que 10 son diez días, 1:2 una hora y dos minutos, 1:02:03:04 un día y dos horas, y 24:00:00, que ningún reloj lee, veinticuatro días; una parte que pase de su límite o un intervalo que pase del rango del tipo lanza la OverflowException de .NET con sus palabras, y una fracción de más de siete dígitos se redondea como la redondea .NET (Desde 0.2.0-preview.62, cuando solo se leía [-][d.]hh:mm:ss[.f] y no se comprobaba ninguna parte, así que 00:60:00 era una hora). Así, un valor escrito con ToString() se vuelve a leer, y lo que se rechaza se rechaza con las palabras de .NET. Cualquier otra cultura hace fallar el build (EQ2108), y también los estilos que no sean de espacio en blanco y un span (EQ1004); ParseExact, TryParseExact y GetDateTimeFormats no se traducen (EQ2004). Convert.ToDateTime lee un texto como lo hace DateTime.Parse, uno null como MinValue, un objeto por lo que contiene y una fecha como sí misma, y un valor de otro tipo lanza la InvalidCastException de .NET; un char guardado en un objeto se lee como un texto de un carácter, ya que un valor encapsulado no lleva su tipo (#552) (Desde 0.2.0-preview.62, cuando todos los lectores leían las formas invariantes, un proveedor hacía fallar la página y Convert.ToDateTime respondía el texto). El nombre de un mes, una fecha de dos números, un reloj sin fecha y los calendarios distintos del gregoriano todavía no se leen (#807). Un constructor que toma un Calendar hace fallar el build (EQ1004), ya que el navegador no tiene ninguno de los calendarios de .NET, incluido el de DateOnly (Desde 0.2.0-preview.62, cuando descartaba el calendario). Vea la tabla Cobertura y conformidad de la BCL de .NET más abajo para el detalle por tipo.

System.Collections.Generic

  • ✅ List<T> → Array [] de JavaScript; Remove responde el bool que responde .NET y compara como lo hace EqualityComparer<T>.Default, un record por valor, una tupla elemento a elemento y NaN con NaN (Desde 0.2.0-preview.59). IndexOf, LastIndexOf, Contains y Remove, Array.IndexOf y Array.LastIndexOf, y el Contains de LINQ comparan con esa única comparación, elegida a partir del tipo del elemento: un NaN, un record, una tupla y un decimal se encuentran por valor, una clase por su propio Equals, el miembro array de una tupla por referencia, como lo compara ValueTuple.Equals. Sort y Array.Sort son la ordenación introspectiva de .NET, inestable como la de .NET, así que los elementos iguales acaban donde acaban los de .NET, por el comparador por defecto del tipo (primero los NaN de un double), un Comparison<T>, los seis de StringComparer, Comparer<T>.Create o un comparador tuyo, con las excepciones de .NET; cualquier otro comparador hace fallar el build (EQ2007). BinarySearch responde el complemento del punto de inserción cuando no encuentra, Find y FindLast el valor por defecto del tipo del elemento, RemoveAll cuántos eliminó, CopyTo escribe en el array que se le entrega, y los rangos de FindIndex, FindLastIndex, IndexOf y LastIndexOf se comprueban con las palabras de .NET (Desde 0.2.0-preview.61)
  • ✅ El Add y el Clear de ICollection<T> añaden y vacían como lo hace la colección que hay detrás de la interfaz cuando se ejecuta la llamada: una lista anexa, un conjunto añade un valor que no contiene, una lista enlazada añade en último lugar, un diccionario añade el par y rechaza una clave que ya está con las palabras de .NET, y un tipo tuyo ejecuta su propio Add y su propio Clear, incluido el inicializador de colección de un miembro (Items = { 1, 2 }). Eran el push y el splice de un array, que no tienen un conjunto, una lista enlazada ni un diccionario, antes de la 0.2.0-preview.61 (Desde 0.2.0-preview.61)
  • ✅ Dictionary<TKey, TValue>, IDictionary e IReadOnlyDictionary → la clase de diccionario del runtime ($eq.collections.dictionary), que guarda sus entradas por posición como el de .NET: enumera en orden de inserción, y una entrada añadida después de una eliminación ocupa el lugar de la eliminada, la última liberada primero, así que d[3] = 30; d[1] = 10; enumera 3,1. Cada clave conserva su tipo (una clave int vuelve como número, una long como BigInt, una char como char) y se encuentra como la encuentra el comparador por defecto del tipo de la clave: por valor para un record, un struct, una tupla, un decimal o una fecha, por identidad para un número, una cadena, un char, un bool, un enum o un Guid, y por la igualdad propia del valor donde el tipo no decide (object, una interfaz, un parámetro de tipo, una clase), así que dos records iguales bajo object son una sola clave; el elemento array de una clave tupla, por referencia (Desde 0.2.0-preview.61). new Dictionary<K, V>(other) copia, un foreach (var (k, v) in d) con deconstrucción funciona en todos los diccionarios, incluidos los ordenados, TryAdd, ContainsValue, Remove(key, out value), Keys.Contains y Keys.Count responden como los de .NET, y ToDictionary construye la misma clase. Add, un inicializador de colección y el constructor que copia pares rechazan una clave que ya está con la ArgumentException de .NET, con las palabras de cada colección, mientras que el indexador, el [key] = value de un inicializador de objeto y TryAdd reemplazan o declinan, y new KeyValuePair<K, V>(key, value) y KeyValuePair.Create construyen el par que produce un diccionario (Desde 0.2.0-preview.60). Una clave añadida mientras un foreach recorre el diccionario termina el recorrido con una InvalidOperationException, como hace el enumerador de .NET, mientras que una sobrescritura, una eliminación y Clear siguen adelante, y un diccionario ordenado y una lista ordenada rechazan cualquier cambio, también en los recorridos de sus claves y sus valores, con las palabras de System.Collections, donde viven los dos, contando el Remove de una clave que un diccionario ordenado no contiene y su Add de una que contiene como los cuenta su árbol, y su Remove(key, out value) de una clave que no contiene como nada, ya que CollectionExtensions pregunta antes (Desde 0.2.0-preview.64, #864). Un diccionario en el estado renderizado en el servidor, en el resultado o los argumentos de una Server Action, o en el payload de un topic cruza como sus pares [key, value] y llega como la clase, con sus claves en su tipo, en el orden en que enumeraba, incluidas las claves enteras: un objeto JSON las habría listado primero y en orden ascendente (Desde 0.2.0-preview.61, #437). Un comparador que pide lo que el navegador ya hace (null, EqualityComparer<T>.Default, StringComparer.Ordinal) construye el diccionario, y uno ordenado conserva el orden que pide (Desde 0.2.0-preview.61), mientras que uno que cambia la igualdad, como StringComparer.OrdinalIgnoreCase, hace fallar el build con EQ2007. Desde 0.2.0-preview.60. Sus Keys y Values son vistas vivas, como las de .NET: una leída después de que el diccionario cambiara muestra el cambio, una clave añadida mientras se recorre una termina el recorrido, el propio Contains de las claves encuentra una clave como lo hace el diccionario, y cada elemento vuelve tal como es, incluido un delegado. Las de un diccionario ordenado y de una lista ordenada también lo son, donde eran copias, y un cambio pedido a una vista, un Add o un Remove a través de ICollection<T>, contenga la vista el valor o no, o una clave escrita mediante el Keys[i] de una lista ordenada, lanza la NotSupportedException que lanza .NET, con las palabras de cada colección; el TryAdd de un diccionario ordenado, que C# encuentra en CollectionExtensions, era un ReferenceError, y un método de extensión tuyo llamado TryAdd o GetValueOrDefault se llama como tuyo, donde se tomaba por el del diccionario (Desde 0.2.0-preview.64, #864). Una consulta LINQ sobre una vista, como sobre cualquier fuente, la lee entera antes de su primer callback (#685). EnsureCapacity responde el primo en el que se fija .NET, un constructor de capacidad y una copia dimensionan el diccionario como los de .NET, y TrimExcess compacta las entradas para que la siguiente clave vaya después de las demás (Desde 0.2.0-preview.61).
  • ✅ HashSet<T>, ISet<T> e IReadOnlySet<T> → el conjunto del runtime ($eq.collections.hashSet), construido dondequiera que se cree uno (un constructor, un inicializador, una expresión de colección, ToHashSet, un valor que cruzó desde el servidor). Guarda sus elementos por posición en la tabla en la que el diccionario del runtime guarda sus claves: un elemento añadido después de una eliminación ocupa la posición del eliminado, la última liberada primero, así que { 3, 1 } menos 3, más 5, enumera 5,1, y cada operación de conjunto (UnionWith, IntersectWith, ExceptWith, SymmetricExceptWith, RemoveWhere), una copia y TrimExcess dejan las posiciones donde las deja .NET. Un elemento se encuentra como lo encuentra el comparador por defecto de su tipo, la regla que siguen las claves de un diccionario: una fecha, un decimal, una tupla y un record por valor, NaN igual a NaN. Add responde si el elemento era nuevo, los predicados de conjunto, CopyTo y EnsureCapacity responden como los de .NET, y un elemento añadido durante un foreach termina el recorrido con una InvalidOperationException. TryGetValue todavía no se trata y hace fallar el build (EQ1004); un comparador que cambia la igualdad también lo hace fallar (EQ2007) (Desde 0.2.0-preview.61).
  • ✅ ContainsKey / TryGetValue nunca responden por "constructor", "toString" ni por nada más de Object.prototype. Desde 0.2.0-preview.15; también GetValueOrDefault, desde 0.2.0-preview.58.
  • ✅ TryGetValue y GetValueOrDefault responden a una clave que falta con default(TValue) en todos los diccionarios, incluidos los ordenados: 0 para un int, false para un bool, null para una referencia, así que d.TryGetValue(k, out var n); d[k] = n + 1; cuenta. El receptor, la clave y un valor por defecto explícito se evalúan una vez cada uno, en el orden en que están escritos; un out _ no recibe nada, y un campo o un elemento out se escribe donde lo escribe C#. La forma estática, CollectionExtensions.GetValueOrDefault(d, key), lee el diccionario que pasa su primer argumento. Desde 0.2.0-preview.58.
  • ✅ Queue<T>, Stack<T>, LinkedList<T>, SortedSet<T>, SortedDictionary<K,V>, SortedList<K,V> → colecciones compat del runtime bajo $eq.collections.*. Una ordenada ordena como Comparer<T>.Default ordena su tipo de elemento: una cadena en la cultura actual, un double con su NaN primero, un enum por su valor, un decimal, una fecha y un tipo tuyo que implementa IComparable por su CompareTo, y StringComparer.Ordinal por unidad de código. El Contains de una cola y de una pila encuentra un decimal, una fecha o un record por su valor (Desde 0.2.0-preview.60). Cada una escribe el JSON que escribe .NET, un array en el orden en que enumera, una pila desde su cima, así que un argumento de una Server Action llega al servidor como la colección (Desde 0.2.0-preview.61). El CopyTo de una cola y de una pila, y el CopyTo, UnionWith, IntersectWith, ExceptWith, SymmetricExceptWith, IsSubsetOf, IsProperSubsetOf, IsSupersetOf, IsProperSupersetOf, Overlaps, SetEquals y RemoveWhere de un conjunto ordenado responden como los de .NET, con sus rechazos con las palabras de .NET, y de dos elementos que un conjunto ordenado considera iguales (1.0m y 1.00m) una unión y una intersección conservan el que conserva .NET. Una fuente null entregada al constructor de una lista, una cola, una pila, una lista enlazada, un conjunto ordenado o cualquier diccionario lanza la ArgumentNullException que lanza .NET, nombrando collection o dictionary, donde el navegador construía uno vacío (una lista lanzaba un TypeError), una capacidad negativa de una cola o de una pila se rechaza, y una cadena copiada en una de ellas se lee por sus chars UTF-16 (Desde 0.2.0-preview.62, #723, #726). Un foreach sobre una cola, una pila, una lista enlazada o un conjunto ordenado termina en el paso que sigue al cambio de la colección, incluido el que sigue al último elemento, con la InvalidOperationException que lanza .NET, con las palabras de .NET: un cambio es lo que .NET cuenta como tal, incluidos el Add de un conjunto ordenado de un elemento que ya contiene y su Remove de uno que no contiene, y un valor escrito en el nodo de una lista enlazada no lo es. El Reverse() de un conjunto ordenado lo recorre desde su elemento mayor y se rechaza igual (Desde 0.2.0-preview.62, #765). El TryDequeue y el TryPeek de una cola, el TryPop y el TryPeek de una pila y el TryGetValue de un conjunto ordenado responden a través de su out como los de .NET, con el valor por defecto del tipo donde no hay nada; el Min y el Max de un conjunto ordenado vacío son el valor por defecto de su tipo de elemento, como los de .NET; el Capacity, el EnsureCapacity y el TrimExcess de una cola y de una pila conservan la capacidad de .NET, que crece según la regla propia de cada una y se dimensiona a partir de una secuencia como la dimensiona .NET, a la cuenta de una ICollection<T> y duplicando desde cuatro para cualquier otra cosa; el Find, FindLast, AddAfter, AddBefore y CopyTo de una lista enlazada, y las sobrecargas que toman un nodo, enlazan como los de .NET, sin pertenecer a ninguna lista un nodo eliminado o borrado, con los rechazos de .NET con sus palabras; y new LinkedListNode<T>(value) construye un nodo. Cualquier otro miembro de las cinco, entre ellos el Comparer de un conjunto ordenado y un GetEnumerator explícito, hace fallar el build (EQ2004), donde todos los miembros compilaban y los que le faltaban al runtime eran un TypeError en el navegador. Una cola o una pila construida a partir de una consulta LINQ toma la cuenta de la consulta como su capacidad, ya que la consulta se ha leído entera (#685) (Desde 0.2.0-preview.62, #866). El GetViewBetween de un conjunto ordenado responde una vista de ese rango del conjunto, que comparte los elementos del conjunto, viendo cada uno lo que cambia el otro; rechaza un elemento fuera de su rango y unos límites desordenados con las excepciones de .NET, con las palabras de .NET, y sigue los caminos de .NET por las operaciones de conjunto, preguntando su ExceptWith la cuenta que contó por última vez, así que no elimina nada hasta que se lee su cuenta, con Count, con LINQ, con una copia en una lista o en un conjunto, o con una operación de conjunto que le pregunta su cuenta, como el de .NET (Desde 0.2.0-preview.64, #865). El RemoveWhere de un conjunto ordenado pregunta a su predicado en el orden del conjunto, donde .NET recorre su árbol en anchura (#766). Una consulta LINQ sobre una vista la vuelve a contar, donde el First o el Where de .NET solo la recorren, así que su ExceptWith puede eliminar lo que el de .NET, con una cuenta nunca leída, deja en su sitio, y el AddRange de una lista no la cuenta, donde el de .NET sí (#865).

System.Threading.Tasks

  • ✅ Task, Task<T> → Promise.
  • ✅ Task.Delay → envoltorio de setTimeout.
  • ✅ Task.WhenAll, Task.WhenAny.
  • ⚠️ Task.Run se ejecuta en el hilo principal (microtarea), NO en un hilo en segundo plano.

Otras utilidades

  • ✅ Console.WriteLine → console.log.
  • ✅ Math.* (Min, Max, Abs, Round, etc.) → Math.*.
  • ✅ Guid (NewGuid, Empty, Parse, TryParse, new Guid(text)) → crypto.randomUUID() para uno nuevo, y su texto canónico, el formato D en minúsculas, para uno leído de un texto en cualquier formato que lea .NET, así que dos grafías de un mismo Guid son un solo valor para ==, un diccionario y un conjunto (Desde 0.2.0-preview.60).
  • ✅ Regex → RegExp de JavaScript.

🌐 Paquetes del ecosistema

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

Propósito: conjuntos de iconos completos. Contiene:

  • Lógica de resolución de SVG
  • Componentes de icono especializados
  • Un IconGlyph generado por icono, como propiedad para que el trimmer pueda descartar los que nadie nombra
Paquete Propósito Estado
eQuantic.UI.Charts Gráficos escritos una vez, dibujados por todos los realizadores. ✅
eQuantic.UI.Lottie Animaciones de alto rendimiento. ✅
eQuantic.UI.Image Optimizada (Lazy/Blur/Priority). ✅

🧩 Patrones del framework

Patrón Interfaz Descripción Estado
Gestión de metadatos IHandleMetadata Etiquetas SEO y elementos del head. ✅
Gestión de assets IRequireAssets Inyección dinámica de script/estilo. ✅
Server Actions [ServerAction] RPC seguro del cliente al servidor. ✅
Componentes compuestos N/A Patrones semánticos de subcomponentes. ✅
Biblioteca de clases compartida ProjectReference Un tipo de un proyecto referenciado que usa una página es un módulo del bundle: una clase estática, un record, un componente y sus cadenas. Vea Flujo de build (Desde 0.2.0-preview.62). ✅

⚠️ Limitaciones y salvedades

  1. Código bloqueante:

    • ❌ .Wait(), .Result en las Tasks NO están soportados. Hay que usar await. Bloquear el hilo principal congela la interfaz del navegador.
  2. Reflexión:

    • ❌ System.Reflection en gran parte no está soportado.
    • ✅ typeof(T).Name y nameof(...) son constantes soportadas.
  3. Sistema de archivos:

    • ❌ System.IO (File, Directory) en un componente de cliente es un error de build (EQ2112): el navegador no tiene sistema de archivos que darle.
    • Usa Server Actions para manejar las operaciones con archivos.
  4. Números:

    • ✅ int/double/float se mapean a números de JS. La división entera trunca (Math.trunc), y una división o un resto por cero, o MinValue entre -1 en int y long, lanza la excepción que lanza .NET (Desde 0.2.0-preview.58); un float se redondea a precisión simple dondequiera que se produce, exactamente como lo redondea RyuJIT, incluidos la asignación compuesta y el incremento de un destino float? (Desde 0.2.0-preview.58), y Math.Round es el de .NET: un punto medio exacto, todos los modos de MidpointRounding.
    • ✅ decimal es exacto de punta a punta: se compila al tipo compat Decimal del runtime (mantisa BigInt + escala), así que 0.1m + 0.2m == 0.3m es true. Los decimales también cruzan el cable como cadenas JSON (EqJson) y se hidratan de vuelta en Decimal en el cliente (el Decimal por defecto del campo dirige una coerción que preserva el tipo, vea hydrate-value.ts), así que los decimales que da el servidor conservan sus 28 dígitos en lugar de redondearse a través de un double. Un decimal leído de un texto (decimal.Parse/TryParse, Convert.ToDecimal("…")) sigue la gramática de .NET bajo los NumberStyles que nombra la llamada y redondea como redondea el parser de .NET. Se lee en la cultura invariante, y la llamada lo dice con CultureInfo.InvariantCulture: sin cultura el build avisa (EQ2110) y con otra falla (EQ2108), ya que el navegador no lee el texto de ninguna otra cultura. Un double o un float convertido a decimal conserva los 15 o 7 dígitos significativos de .NET: (decimal)(0.1 + 0.2) es 0.3. Desde 0.2.0-preview.58.
    • ✅ Una constante es su valor en su tipo de C#: decimal.MaxValue, MinValue, Zero, One y MinusOne son decimales exactos, se escriban decimal o Decimal, y una constante long como TimeSpan.TicksPerSecond es un BigInt, así que se encuentra con cualquier otro long. Un literal decimal es su valor, así que 1_000.5m funciona, y el valor por defecto de un parámetro, rellenado para un argumento que uno con nombre se salta, conserva su tipo: un decimal, un long, un char, una cadena con una comilla dentro. Una constante alcanzada por su nombre simple bajo un using static se escribe igual. Una constante de un tipo enum es un valor de ese enum, no su número, y una constante decimal casa por valor en un patrón, un brazo de switch o un case (d is 1m, case decimal.One:). Desde 0.2.0-preview.60.
    • ✅ Todos los demás tipos numéricos también leen texto como lo hace .NET: int, uint, long, ulong, short, ushort, byte, sbyte, double y float con Parse/TryParse, y Convert.ToInt32("…") y sus hermanos, leen el texto entero bajo los propios NumberStyles del tipo (Integer, o Float | AllowThousands para un real) o los que nombra la llamada. Un texto que no es un número lanza una FormatException ("12abc", "1e3" como int, "0x1F" sin HexNumber), un valor que el tipo no puede contener, una OverflowException, un TryParse fallido deja 0, un estilo hexadecimal o binario lee los bits del tipo ("FFFFFFFF" es -1 para un int), el texto de un long es un BigInt, y un float se redondea una sola vez, a partir de los dígitos. La cultura es la del decimal: EQ2110 sin ninguna, EQ2108 con cualquiera salvo CultureInfo.InvariantCulture. Desde 0.2.0-preview.58.
    • ✅ El texto de un double y de un float es el de .NET: los dígitos más cortos que se vuelven a leer como el valor, notación fija desde 1e-4 hasta 1e16 para un double y 1e8 para un float, científica fuera de eso (1E+17, 1E-05), -0 para un cero negativo e Infinity para un infinito, lo mismo a través de ToString(), ToString(CultureInfo.InvariantCulture), una concatenación y una interpolación; un float? o double? null no escribe nada. Desde 0.2.0-preview.58.
    • ✅ El texto de un número sin especificador es el de la cultura, dondequiera que lo escriba C#: una concatenación, un hueco de interpolación simple o alineado, ToString(), Convert.ToString, string.Format("{0}"), string.Concat, string.Join, el StringBuilder con su Append y su Insert, y el texto de un record, como los escribe .NET en la cultura vigente: -1,5 en pt-BR, un signo menos que la cultura escribe a su manera (el −5 de sv-SE), un float con sus propios dígitos, un decimal con su escala, y nunca -0 para un entero que JavaScript guarda como cero negativo. Un valor escrito para una máquina lo dice con ToString(CultureInfo.InvariantCulture), que conserva su forma en todas las culturas. Desde 0.2.0-preview.61.
    • ✅ También a través del formateador: un float formateado con alineación o especificador ($"{f,8}", $"{f:G}"), ToString("G"), ToString("R") y un float pasado a string.Format escriben sus propios dígitos, G y R en la notación de .NET (1E+21). Un marcador de string.Format alinea ({0,5}, {0,-5}), un null en un hueco alineado se rellena, y un array params pasado entero, incluidos un string[] o una expresión de colección, son sus valores. El proveedor de string.Format sigue la política que tiene ToString: CultureInfo.InvariantCulture formatea de forma invariante, incluidos los patrones de una fecha y el ¤ genérico de una moneda, CultureInfo.CurrentCulture o un null como lo hace una llamada sin proveedor, y cualquier otro es EQ2108. El ToString() de un bool es True o False. Un límite: un float encapsulado en un object a través del cual el compilador no puede ver (object o = 0.1f; $"{o}", o un object[] construido en tiempo de ejecución) es el número simple que hay debajo. Desde 0.2.0-preview.58.
    • ✅ Un número se imprime a través de su especificador como lo imprime .NET: E y e escriben los dígitos de la mantisa, el signo del exponente y al menos tres dígitos de exponente ((12345.0).ToString("E2") es 1.23E+004). F, N, P, C, G y una máscara personalizada redondean el valor EXACTO del número, incluido el valor binario de un double ((0.1).ToString("F20") es 0.10000000000000000555), y todos los dígitos de un long y de un decimal (9007199254740993L.ToString("N0") es 9,007,199,254,740,993), con una mitad exacta redondeada al par para un double y un float y lejos del cero para un decimal y un entero ((2.5).ToString("F0") es 2, (2.5m).ToString("F0") es 3, 125.ToString("E1") es 1.3E+002). N, F, C y P se disponen a partir del propio NumberFormatInfo de la cultura, que viaja con cada página: sus patrones, símbolos, separadores, tamaños de grupo y dígitos por defecto ((1234.5).ToString("N") es 1,234.500 en en-US en un servidor cuyo .NET lee ICU), en dígitos ASCII en todas las culturas, incluida la de ar-EG, como los escribe .NET (Desde 0.2.0-preview.61). Una cultura a la que se cambia sin un servidor al que preguntar se dispone con Intl.NumberFormat v3 (ES2023): lo tienen Chrome y Edge 106, Firefox 116 y Safari 15.4. X y B (binario) escriben un entero negativo con el ancho de su tipo (((short)-1).ToString("X") es FFFF, el de un int es FFFFFFFF). Una máscara personalizada se dibuja como la dibuja .NET: secciones (0.00;(0.00);'zero'), texto entre comillas y escapado, % y ‰, exponentes (0.00E+00), y las comas antes del punto que dividen por mil (#,##0,). Una precisión puede pasar de 100 dígitos, como permite .NET. Los números se agrupan como los agrupa .NET (es-ES escribe 1.234) y llevan el signo menos de la cultura (el − de sv-SE), y un especificador que el tipo no admite lanza, como en .NET ((2.5).ToString("D")). Desde 0.2.0-preview.59. Un double bajo D, X o B lanza tenga lo que tenga, nint y nuint son enteros de 64 bits, y el signo de por mil (el ؉ de ar-SA), los signos de un exponente y el signo de porcentaje son los propios de la cultura. Desde 0.2.0-preview.61.
    • ✅ long/ulong ahora son exactos: se compilan a BigInt de JS mediante el helper compat long. 9007199254740993L + 1L es 9007199254740994 (un número JS normal redondearía a …992). Los literales pasan a BigInt (5L → 5n), y lo mismo un literal sin sufijo que solo cabe en un long (637000000000000000 → 637000000000000000n, Desde 0.2.0-preview.58); los operandos de aritmética/comparación se envuelven en long() (que convierte number/string → bigint) para que las expresiones mixtas nunca lancen. En el cable, los enteros de 64 bits cruzan como cadenas JSON (Server Actions + estado del SSR, mediante EqJson) para que los valores más allá de 2^53 sobrevivan al viaje de ida y vuelta. Los demás sufijos de tipo numérico (1.5f, 100u) se eliminan.
  5. Seguridad de hilos:

    • Como JS es de un solo hilo, las sentencias lock se eliminan al compilar (se ignoran).
    • Thread.Sleep no está soportado (usa Task.Delay).

🧪 Cobertura y conformidad de la BCL de .NET

La fidelidad de la transpilación la garantiza un arnés de conformidad (tests/eQuantic.UI.Conformance.Tests): cada caso ejecuta la misma expresión de C# de dos maneras, transpilada a JS (ejecutada mediante el Bun embebido) y evaluada directamente en .NET (scripting de Roslyn), y comprueba que los resultados son idénticos. Un caso cuyo C# rechaza el build (un error EQ, como EQ2008 para una cláusula de consulta que eqc no traduce) falla antes de que se ejecute ninguno de los dos lados, nombrando cada rechazo, salvo que esté escrito para fijar ese rechazo: entonces comprueba el id del diagnóstico y no ejecuta nada. La suite cuenta sus casos (más de mil en el momento de escribir esto, y creciendo con cada construcción); todos están en verde.

(Lo más reciente: todos los diccionarios a través de la clase de diccionario del runtime, $eq.collections.dictionary.) Esto cubre tanto expresiones como bloques de sentencias (flujo de control: if/for/foreach/while/switch/ try-catch-finally/funciones locales; el bloque se ejecuta en una IIFE y se compara el valor que devuelve).

Cada construcción se resuelve mediante uno de tres mecanismos (vea docs/DOTNET-COVERAGE-PROGRAM.md):

  1. Estrategia nativa: JS idiomático cuando el runtime tiene un equivalente.

  2. Helper de compatibilidad con .NET en el runtime: semántica fiel de .NET donde JS no tiene ninguna. El transpilador emite estos helpers bajo un único espacio de nombres $eq (organizado por dominio), traído con un solo import por módulo, import { $eq } from "@equantic/runtime" (resuelto por el import map de la página), en lugar de N imports sueltos de helpers, y $eq.* nunca puede chocar con un identificador del usuario en el ámbito generado:

    • $eq.num: dec (Decimal exacto), decParse/decTryParse/decConvert (un decimal a partir de un texto o de Convert), intParse/intTryParse/intConvert y realParse/realTryParse/realConvert (un entero, un double o un float a partir de un texto o de Convert), decFromDouble/decFromSingle (un número binario a un decimal), long (Int64 mediante BigInt), double/single (el texto de un número)
    • $eq.math: round (redondeo bancario)
    • $eq.convert: toBase64String, toBase64CharArray, toHexString, toHexStringLower (la codificación de un array, como la escribe el Convert de .NET)
    • $eq.text: format (formato de números/cadenas), stringBuilder, record y tuple (el texto de un record, de un objeto anónimo y de una tupla de valor, como lo escribe .NET)
    • $eq.time: dateTime, timeSpan, dateOnly, timeOnly, dateTimeOffset
    • $eq.enums: text, parse, tryParse, zero, names, values, isDefined (un enum como lo lee .NET, a partir de la forma que escribe eqc en cada llamada)
    • $eq.collections: queue (FIFO), stack (LIFO), dictionary (guardado por posición como el de .NET, con las claves encontradas por identidad o por valor), linkedList, sortedSet/sortedDictionary/sortedList (ordenados por clave)
    • $eq.nullable: arith, cmp, unary (operadores elevados de Nullable<T>: aritmética y operadores unarios que propagan el null, relacionales que dan false ante un null)
    • $eq.equals: igualdad estructural (por valor) para records/structs/tuplas (respalda ==, .Equals, Contains, Distinct)
    • $eq.hash: of (GetHashCode(), que concuerda con $eq.equals, así que los valores que Equals considera iguales tienen el mismo hash, incluido un tipo de valor del vocabulario como Point o TypeStyle, y un tipo que lo sobrescribe responde el suyo), instance (la misma llamada sobre una referencia, rechazada cuando es null igual que .NET lanza NullReferenceException, mientras que un Nullable<T> vacío responde 0), group (el grupo de métodos value.GetHashCode, que toma su receptor una vez y rechaza uno null donde se crea el delegado), combine (HashCode.Combine, por sus parámetros incluso cuando tienen nombre) e identity (el object.GetHashCode de una clase que no lo sobrescribe, y el de un array, cambien como cambien sus elementos). Los propios números de .NET no son estables entre procesos, así que el navegador conserva el contrato, nunca el número del servidor (Desde 0.2.0-preview.60)
    • $eq.css: styleBuilder, classBuilder, joinClasses, whenClass (el subsistema de estilos)
  3. Fallar ante lo no soportado: una construcción sin representación posible en JS ahora produce un error de build (con un código EQ estable) en lugar de emitirse tal cual en silencio. Dos capas:

    • UnsupportedConstructStrategy (EQ2001): intrínsecos de referencias tipadas (__makeref, __refvalue, __reftype), tipos puntero, punteros a función.
    • goto/goto case/goto default (EQ2002): sin equivalente en JS; reestructura con bucles/condicionales. (Los bloques unsafe/fixed/lock se desenvuelven a su cuerpo, siendo lock un no-op de un solo hilo, y una etiqueta suelta se reduce a su sentencia interior.)
    • Frontera cliente/servidor de SemanticValidator (EQ21xx): llamadas a System.IO, System.Net.Http, System.Net.Sockets, EF Core / System.Data, threading del SO (Thread/Monitor/Mutex), Process, InteropServices (P/Invoke), Reflection.Emit desde un componente de cliente. (System.Threading.Tasks no está prohibido: async se mapea a Promise.) La solución es mover la llamada a un [ServerAction].

    Cualquier otra construcción que no encuentre ninguna estrategia es un error (EQ1001 para una expresión, EQ1002 para una sentencia, EQ1003 para cualquier otra cosa) y no se emite nada para ella. Antes pasaba tal cual con un aviso, lo que significaba un build que tenía éxito y una página que lanzaba — rechazar lo que no puede representar es justamente la cuestión. Los diagnósticos se imprimen en la forma canónica de MSBuild, así que hacen fallar dotnet build y el IDE los enlaza. La lista completa está en Diagnósticos.

Área Estado Notas
Aritmética / bit a bit / comparación ✅ la división entera trunca; %, los desplazamientos y `&
Math.* / MathF.* ✅ la misma tabla que double.* / float.*, con las composiciones propias de .NET; Round es el de .NET (punto medio exacto, todos los modos)
decimal ✅ exacto mediante Decimal (literales + + - * / % == != < > <= >=, el resto con el signo del dividendo, Desde 0.2.0-preview.58 para %); las constantes MaxValue, MinValue, Zero, One y MinusOne (Desde 0.2.0-preview.60); decimal.Parse/TryParse según la gramática y el redondeo de .NET, incluidos los NumberStyles que nombra una llamada; Convert.ToDecimal y las conversiones desde double/float con los 15 y 7 dígitos de .NET
Constantes numéricas ✅ int.MaxValue, double.Epsilon, … → literales
Análisis / Convert.* ✅ Parse/TryParse en todos los tipos numéricos, y Convert.ToInt32/ToInt64/ToDouble/… de un texto, según la gramática de .NET (Desde 0.2.0-preview.58); bool.Parse/TryParse y Convert.ToBoolean de un texto como lo lee el Boolean.TryParse de .NET: "True" o "False" en mayúsculas o minúsculas, sin los espacios en blanco ni los NUL de los extremos, y con las excepciones de .NET, y Convert.ToBoolean de un número según si no es cero, de un bool como sí mismo, y de un char o una fecha como la InvalidCastException de .NET (Desde 0.2.0-preview.59); Convert.ToString/…; Convert.ToDateTime de un texto como lo lee DateTime.Parse en la cultura que nombra la llamada, uno null como MinValue (Desde 0.2.0-preview.62, cuando respondía el texto); Convert.ToBase64String, ToBase64CharArray, ToHexString y ToHexStringLower escriben la codificación de .NET de un byte[], incluidos su desplazamiento, su longitud y sus saltos de línea cada 76 caracteres, y rechazan lo que rechaza .NET con sus palabras; un span que no es una expresión de colección hace fallar el build (EQ1004) (Desde 0.2.0-preview.62, cuando cada uno escribía el 1,2,255 del array)
Cadenas ✅ Substring/IndexOf/Replace/Split/Pad/Trim/Trim(char)/Concat/Format/Join/IsNullOrEmpty/IsNullOrWhiteSpace; Trim(chars) y sus dos mitades leen la cadena y después los caracteres una vez, incluido un await entre ellos, y ningún carácter (un array vacío o null) recorta el espacio en blanco, como hace .NET (Desde 0.2.0-preview.60); Trim(), sus dos mitades, IsNullOrWhiteSpace y un Split() sin argumentos leen el espacio en blanco de .NET, donde el de JavaScript deja U+0085 y toma U+FEFF (Desde 0.2.0-preview.58); los propios Equals, StartsWith, EndsWith, Contains, IndexOf y LastIndexOf (con un inicio y una cuenta) de una cadena y Replace con un StringComparison responden según esa comparación, escrita tal cual o guardada en una variable: OrdinalIgnoreCase compara mayúsculas y minúsculas como .NET (el signo Kelvin, la s larga y la i sin punto no son k, s e I, y un par de surrogates se lee como su code point), un inicio o una cuenta fuera de la cadena lanza la excepción de .NET, Replace escribe su reemplazo como texto, y CompareTo ordena según la cultura actual, como lo hace string.Compare (Desde 0.2.0-preview.60). IndexOf y LastIndexOf de una cadena desde un inicio, y para una cuenta, sin comparación se detienen en su cuenta, leen el inicio de LastIndexOf como el último char que puede cubrir una coincidencia, y lanzan la excepción de .NET ante un rango fuera de la cadena o un valor null (Desde 0.2.0-preview.62, cuando se descartaba la cuenta y LastIndexOf("bc", 4) respondía la coincidencia que empieza en 4). IndexOf(char, startIndex[, count]) y LastIndexOf(char, startIndex[, count]) buscan en ese rango y lanzan la excepción de .NET ante un inicio o una cuenta fuera de la cadena: el inicio de LastIndexOf está sobre un char de la cadena, y una cadena vacía responde -1 (Desde 0.2.0-preview.61, cuando el inicio se acotaba y se descartaba la cuenta). Una BÚSQUEDA por una comparación de cultura, o con un CultureInfo, hace fallar el build (EQ1004), porque el navegador no tiene búsqueda sensible a la cultura: busca por Ordinal u OrdinalIgnoreCase, o compara cadenas enteras con Equals o string.Compare. Las sobrecargas sin comparación buscan de forma ordinal en el navegador y según la cultura actual en .NET (#532).
char.* ✅ ToUpper/ToLower/IsDigit/IsLetter/IsWhiteSpace/… (conscientes de Unicode; IsWhiteSpace es el conjunto de .NET, con U+0085 dentro y U+FEFF fuera, Desde 0.2.0-preview.58; ToUpper, ToLower y su par invariante pasan un char a mayúsculas o minúsculas como a una cadena de uno, en la cultura que nombra la llamada, y una cultura null lanza la ArgumentNullException de .NET, Desde 0.2.0-preview.62)
StringBuilder ✅ tipo compat: Append(incl. bool→"True"/"False")/AppendLine("\n")/Insert/Remove/Replace/Clear/Length/ToString. Las sobrecargas que cuentan o toman un rango escriben lo que escribe .NET, Append('x', 3), Append(text, start, count), Insert(i, text, count), Replace sobre un rango y ToString(start, length), un char[] como sus caracteres y un null como nada, y cada rechazo es el de .NET (Desde 0.2.0-preview.61). AppendFormat y AppendJoin añaden lo que escriben string.Format y string.Join, Capacity sigue los fragmentos que .NET reserva a lo largo de cada edición, MaxCapacity y EnsureCapacity responden, el indexador Chars lee y escribe un char, Length corta el texto o lo rellena con \0, Equals(StringBuilder) compara el texto y CopyTo lo copia, rechazando cada uno lo que rechaza .NET; un Append($"…") o AppendLine($"…") interpolado añade cada parte por turno, como hace el handler de .NET, así que un hueco que lee el builder ve las partes anteriores, un argumento con nombre se vincula a su parámetro, y un texto más largo de lo que puede contener una cadena del navegador es la OutOfMemoryException de .NET; un proveedor distinto de la cultura actual en Append, GetChunks y las sobrecargas de span hacen fallar el build (Desde 0.2.0-preview.61)
CancellationTokenSource / CancellationToken ✅ los del runtime, como los de .NET: una fuente (con un retardo o sin él), su Token, IsCancellationRequested, Cancel, CancelAfter, Dispose y CreateLinkedTokenSource; el None de un token (y default, y new CancellationToken(bool)), IsCancellationRequested, CanBeCanceled, ThrowIfCancellationRequested (una OperationCanceledException, "The operation was canceled.") y el Register de un callback, cuyo registro hace Dispose y Unregister. Los callbacks se ejecutan una vez, el último registrado primero, uno registrado tarde se ejecuta de inmediato y recibe a cambio el registro por defecto (su token es None), y los que lanzan se reúnen en una AggregateException, como en .NET. Un método de los tres pasado como callback (token.Register(source.Cancel)) conserva su receptor, y una fuente enlazada, al liberarse, suelta los tokens que sigue. Una fuente liberada suelta también sus callbacks, y un registro todavía se anula una vez después de eso, y new CancellationTokenSource(delay, timeProvider) hace fallar el build con EQ2004: la fuente del runtime se cronometra con el reloj real. Un retardo se acepta hasta 4294967294 ms, como lo acepta .NET, y se espera un mes entero aunque el temporizador de un navegador aguanta unos 24,8 días. default(CancellationTokenRegistration) es el registro de nada, cuyo token es None. Cualquier otro miembro de los tres hace fallar el build con EQ2004 (Desde 0.2.0-preview.61)
LINQ ✅ Where/Select/SelectMany/Where-Select(indexado)/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 = hash join que preserva el orden sobre claves primitivas; OrderBy+ThenBy = una única ordenación compuesta estable, con la fuente copiada). El IGrouping de GroupBy/ToLookup se puede usar como secuencia (iterar, g.Select/g.Sum/g.Count()) y expone g.Key; el indexador [key] de ILookup no está modelado. Un comparador que pide el valor por defecto pasa en ToDictionary, ToLookup, GroupBy, Distinct y ToHashSet, y cualquier otro hace fallar el build (EQ2007) (Desde 0.2.0-preview.61).
Colecciones ✅ List, Dictionary, HashSet (incl. inicializadores y .Count; un conjunto es el conjunto del runtime, guardado por posición y que encuentra sus elementos por valor donde lo hace su tipo de elemento, Desde 0.2.0-preview.61); compat de Queue/Stack/LinkedList; familia ordenada SortedSet/SortedDictionary/SortedList (enumeración ordenada por clave). Todo diccionario es la clase de diccionario del runtime, $eq.collections.dictionary, que enumera por posición como el de .NET, encontrándose por valor una clave record/struct/tupla (construcción y copia, get/set de d[k], ContainsKey/Add/Remove/Clear/TryGetValue/GetValueOrDefault/TryAdd/ContainsValue, Keys/Values/Count, foreach) (Desde 0.2.0-preview.60). Leer una clave que falta lanza KeyNotFoundException, y lo mismo una asignación compuesta y un incremento sobre una (Desde 0.2.0-preview.58 para el incremento). El indexador ILookup[key] devuelve el grupo (o uno vacío para una clave ausente). El Add y el Clear de ICollection<T> llegan a la colección que hay detrás de la interfaz, e IList<T> e IReadOnlyList<T> leen y escriben un tipo tuyo a través de su indexador y lo cuentan por su Count (Desde 0.2.0-preview.61).
enum ✅ cadena con el nombre del miembro, un enum [Flags] su número (igualdad/switch/ternario, texto y formatos, los estáticos de Enum)
long/ulong ✅ exacto mediante BigInt (helper long); literales 5L→5n, y también las constantes (TimeSpan.TicksPerSecond → 10000000n, Desde 0.2.0-preview.60), en el cable como cadena JSON
DateTime ✅ tipo compat DateTime con precisión de tick: constructores, componentes, Add* hasta el tick en el que cae .NET 7+ (una fracción truncada, una cuenta o un resultado fuera de rango rechazados con las palabras de .NET, incluido AddMicroseconds, Desde 0.2.0-preview.59), -→TimeSpan, comparaciones, .ToString()/format; cable ISO-8601 + hidratación. ToString(), ToString(format) y ToString(format, provider) imprimen como los imprime .NET (sin especificador, o con uno null o vacío, es el G de la cultura actual, y ninguna zona horaria mueve las partes del valor salvo la de U): todos los especificadores estándar de una letra (de d a Y, s, u y R a partir de las partes propias del valor, o a partir de ellas con la zona que nombra su kind, U llevado a UTC salvo que ya sea UTC), una letra que no es especificador estándar rechazada como la rechaza .NET, una máscara personalizada token a token con su fracción de segundo (HH:mm:ss.fff), los separadores de la cultura para / y : y la era para g (Desde 0.2.0-preview.61), los patrones de la cultura invariante para CultureInfo.InvariantCulture o cuando no hay cultura vigente, y una cultura con nombre rechazada en el build (EQ2108); ToShortDateString() y sus hermanos son d, D, t y T (Desde 0.2.0-preview.60). Add, Subtract, AddMonths, AddYears y los operadores + y -, incluidas sus formas compuestas, rechazan un resultado fuera del calendario con las palabras de .NET, nombrando los operadores su parámetro t (Desde 0.2.0-preview.60). Todo constructor que el compilador deja pasar construye el valor que construye .NET, cada argumento en el lugar de su parámetro, incluido uno con nombre: los componentes con un milisegundo, un microsegundo y un kind, los ticks con un kind, y un DateOnly con un TimeOnly, rechazándose con las palabras de .NET un componente, un kind o unos ticks fuera de rango, y rechazándose en el build (EQ1004) una sobrecarga que toma un Calendar. Un valor lleva su Kind (Now y Today locales, UtcNow UTC, SpecifyKind), que la aritmética conserva y que la igualdad, la ordenación y el hash dejan fuera; ToLocalTime() y ToUniversalTime() convierten según él en la zona horaria del navegador, Microsecond y Nanosecond responden, y el JSON escribe un valor UTC con Z y uno local con su desfase y los vuelve a leer como lo hace System.Text.Json (Z una hora UTC, un desfase llevado a la hora local del navegador), mientras que DateTime.Parse lleva una zona escrita a la hora local. Una hora local hecha a partir de un instante conserva cuál de dos horas repetidas es, como lo conserva .NET junto al kind, a lo largo de la aritmética hasta SpecifyKind, así que su camino de vuelta a UTC es ese instante, y el texto de una fecha escribe su kind: o y K terminan una hora UTC con Z y una local con su desfase, y z, zz y zzz escriben el desfase de la zona para todo lo que no es UTC (Desde 0.2.0-preview.61)
TimeSpan ✅ tipo compat TimeSpan con precisión de tick: From*, constructores, componentes/totales, + -, comparaciones, .ToString() "c" de .NET, y sus formatos estándar y personalizados (Desde 0.2.0-preview.62); Parse y TryParse leen como lee .NET sin formato, cada parte contra su límite (Desde 0.2.0-preview.62); cable "c" + hidratación
DateOnly / TimeOnly ✅ tipos compat (.NET 6+): constructores, componentes, Add*(+ la vuelta de TimeOnly, convirtiéndose un único producto como convierte un double .NET 9+, Desde 0.2.0-preview.59, rechazándose con las palabras de .NET un DateOnly que sale del calendario, Desde 0.2.0-preview.60), comparaciones, ToString(), ToString(format) y ToString(format, provider) como los imprime .NET en la cultura vigente: d y t sin formato, los especificadores estándar que admite cada tipo y una máscara personalizada de sus propias partes, cualquier otra cosa la FormatException de .NET, y ToShortDateString(), ToLongDateString(), ToShortTimeString() y ToLongTimeString() como d, D, t y T (Desde 0.2.0-preview.61); cable ISO + hidratación
DateTimeOffset ✅ tipo compat con precisión de tick (hora de reloj + desfase, comparado por el instante): constructores, componentes, MinValue/MaxValue (Desde 0.2.0-preview.58), Offset/UtcDateTime/LocalDateTime, ToOffset, Add* hasta el tick con AddMilliseconds/AddMicroseconds, la hora de reloj y después la hora UTC rechazadas con las palabras de .NET (Desde 0.2.0-preview.59), incluidos AddMonths y AddYears (Desde 0.2.0-preview.60), From/ToUnixTime* (ToUnixTimeSeconds y ToUnixTimeMilliseconds un long, redondeado hacia abajo antes de 1970 como lo redondea .NET, Desde 0.2.0-preview.61), - →TimeSpan, comparaciones de instantes, ToString() y sus sobrecargas como los imprime .NET en la cultura vigente: la fecha corta, la hora larga y el desfase sin formato, todos los especificadores estándar salvo U, escribiendo o y K el desfase y convirtiendo u y R según él (Desde 0.2.0-preview.61); cable ISO+desfase + hidratación. Todo constructor construye el valor que construye .NET: los componentes con un milisegundo y un microsegundo, los ticks, un DateOnly con un TimeOnly, y un DateTime, que sin desfase toma el del navegador para una hora que no es UTC; un desfase que no es de minutos enteros, pasa de catorce horas o no es cero para un DateTime UTC se rechaza con las palabras de .NET. Now es el instante en el desfase del navegador, LocalDateTime y ToLocalTime() leen la zona horaria del navegador, y Microsecond y Nanosecond responden (Desde 0.2.0-preview.61)
record / struct / tupla de valor ✅ Semántica de valor. Los records/structs son instancias de sus clases gemelas (el new Point(1,2) posicional llama al constructor que llama C#, y un inicializador de objeto se aplica después de él), las tuplas son arrays con acceso a los elementos por posición (t.Item1) y por nombre declarado ((int X, int Y).X) → índice. ==/!=, .Equals y Distinct comparan estructuralmente mediante $eq.equals, y Contains, IndexOf y un conjunto por el comparador por defecto del tipo del elemento, el miembro array de una tupla por referencia (Desde 0.2.0-preview.61); with copia y reemplaza. Un struct mutable y una tupla de valor se copian donde los copia C#, así que una escritura a través de un nombre nunca se ve a través de otro al que el valor se asignó, se pasó, se devolvió o se encapsuló: el gemelo copia al escribir, y copia this donde sale del miembro de su struct (Desde 0.2.0-preview.61). La deconstrucción var (a, b) = … funciona para tuplas (desestructuración de array, huecos de descarte) y records (desestructuración de objeto por el orden de Deconstruct). Los records se emiten como clases JS con nombre que llevan sus métodos de instancia del usuario + un equals estructural, un with que preserva el prototipo, y el toString de .NET; la semántica de valor no cambia. El equals de un record compara su tipo en runtime (un Dog nunca es igual a un Animal), lo que compara su base y cada campo que declara, incluido uno privado, y su texto nombra los miembros que escribe el PrintMembers de .NET, en su orden: primero los miembros de su base, después los suyos, incluidas una propiedad calculada y una propiedad cuyo getter tiene una accesibilidad propia, dejándose a la base una sobrescritura, cada uno una vez, y Name { } cuando no hay ninguno. Dos miembros de un record o de un struct que caen en un mismo nombre del gemelo (struct S(int x) { public int X … }, un campo celsius junto a Celsius) hacen fallar el build (EQ1007); el parámetro de un constructor primario que solo lee un inicializador (struct Point(int x) { public int X { get; } = x; }) es del constructor, y compila; una propiedad abstracta no guarda nada. Cada valor se escribe como lo escribe .NET: un bool como True, un enum por el nombre de su miembro, un null como nada y un número en la cultura vigente, y una clase o un struct simples que no escriben texto propio escriben el nombre completo de su tipo (My.App.Outer+In), donde el navegador escribía [object Object] o el texto de un record; uno genérico todavía no (Desde 0.2.0-preview.61). Un parámetro posicional cuya propiedad declara el record (record Box(int X) { public int X { get; set; } = X; }) es un solo miembro, el declarado (Desde 0.2.0-preview.61). Un miembro empieza como dice su declaración, sea cual sea la expresión: un parámetro posicional, el inicializador de una propiedad o de un campo (= 1.5m, = 5 en un long, = new(), = "#" + Id), escrito una sola vez, en el constructor de la clase (Desde 0.2.0-preview.59), que ejecuta todos los inicializadores en cada construcción, establezca lo que establezca después un inicializador de objeto (desde 0.2.0-preview.61; vea Constructores más arriba). Los miembros de un record y de un struct se bajan como los de una clase: un método async, un iterador, un parámetro out o ref, y la variable que vincula el patrón del cuerpo de una expresión (o is P p && p.X == X) funcionan en sus métodos, operadores, conversiones y propiedades calculadas, los propios y los que aporta por defecto una interfaz, y un getter que hace yield devuelve su secuencia también en una clase (Desde 0.2.0-preview.60). La pipeline de build descubre los records escaneando y emite cada uno como su propio módulo; los componentes que referencian un record lo importan automáticamente (reactivo, sin lista fija). Los valores del SSR se rehidratan de vuelta en la clase del record en el cliente (recursivamente, restaurando los records anidados y los miembros compat), así que los métodos/instanceof sobreviven. Cubre records posicionales y de cuerpo y structs simples, además de la herencia de records (extends + super) y los records genéricos, cuya igualdad compara el tipo cerrado con el que se construyó un valor: un Box<int> nunca es igual a un Box<double>, y uno construido dentro de código genérico, cuyos argumentos de tipo el build no puede conocer, no se toma por otro tipo (Desde 0.2.0-preview.61). Los diccionarios con clave record (Dictionary<RecordKey, V>) encuentran una clave por valor, así que las claves iguales por valor colisionan como en .NET. Cada miembro se compara como EqualityComparer<T>.Default compara su tipo, así que un array, una lista o la interfaz de una colección (IReadOnlyList<int>) por referencia, y el Equals de una tupla compara sus elementos de la misma manera, siendo distinta una tupla de otra aridad e igual un par null solo a otro; una tupla vista solo como object, o como una interfaz que implementa una tupla, es un array en la web y se compara por sus elementos (Desde 0.2.0-preview.61). El texto de una tupla de valor es el de .NET, (1, a): cada elemento como lo escribe su tipo en la cultura en vigor, incluidos una tupla anidada y un objeto anónimo, uno null como nada, y ocho elementos o más sin anidar, por ToString() y su grupo de métodos, un hueco de interpolación (también uno con alineación o formato, que una tupla ignora), una concatenación, string.Concat, string.Join, string.Format, un StringBuilder, Convert.ToString y el miembro de un record, entre ellos los pares de Zip, donde el navegador escribía 1,2. Un elemento que es un array, una lista, un diccionario, un set o un delegate escribe el nombre de su tipo, System.Int32[], como .NET, y también el propio valor, por dondequiera que se escriba en texto, donde el navegador escribía 1,2 o el código de una función. Equals(object) encuentra distinta una tupla de otros tipos de elemento, ((byte)1, 2).Equals((object)(1, 2)) es false, donde el navegador veía el mismo array. El grupo de métodos t.Equals compara como la llamada, y el Equals de una tupla Nullable es el de su valor (Desde 0.2.0-preview.64). Una tupla guardada como object, cuyo tipo el build no ve, todavía escribe 1,2 y se compara por sus elementos, y también una pasada al delegate del grupo de métodos Equals.
Objeto anónimo (new { A = 1 }) ✅ Un objeto plano, con sus miembros bajo los nombres de su gemelo ({ a: 1 }). Equals, una llamada o un grupo de métodos, compara como el Equals que C# escribe para un tipo anónimo: cada miembro por la igualdad de su tipo (un array o una lista por referencia, un decimal y una tupla por valor), y solo otro objeto anónimo de los mismos miembros en el mismo orden puede ser igual, así que otros miembros, los mismos miembros en otro orden y un record con los mismos miembros no lo son. La llamada lanzaba antes. Miembros de otros tipos también hacen otro tipo: new { A = (byte)1 }.Equals(new { A = 1 }) es false. Por null, Equals, ToString y crear el delegate de cualquiera de los grupos de métodos lanzan NullReferenceException, como en .NET, mientras que un hueco de interpolación y una concatenación no escriben nada. == sigue siendo la igualdad por referencia, y GetHashCode da el mismo hash a objetos iguales, y un set, Distinct y GroupBy toman objetos iguales por una sola clave, Distinct conservando el primero de cada uno. Su texto es el de .NET, { R = 255, Name = a } y { }: los nombres C# de los miembros en su orden, cada miembro como lo escribe su tipo en la cultura en vigor, por todos los caminos que toma el texto de una tupla, donde el navegador escribía [object Object] (Desde 0.2.0-preview.64). Uno guardado como object, o pasado al delegate del grupo de métodos Equals, todavía escribe [object Object] y se compara por sus miembros, sean cuales sean sus tipos.
Nullable<T> (T?) ✅ HasValue/Value, GetValueOrDefault() (valor por defecto según el tipo: 0/false/$eq.num.dec(0)/miembro cero del enum/…) y GetValueOrDefault(fallback), ??; operadores elevados mediante $eq.nullable.*: la aritmética propaga null, los relacionales (< > <= >=) son false cuando cualquiera de los lados es null (no una coerción numérica); una asignación compuesta (+=, *=, …), un incremento y un -, ~ o + unario sobre un número anulable también se elevan, quedándose null un null y siguiendo un valor la regla de su tipo (un float? redondea, un byte? da la vuelta, un decimal? sigue siendo exacto; Desde 0.2.0-preview.58). GetValueOrDefault() sin argumentos sobre DateTime?/Guid?/struct produce null, así que ahí usa la forma con fallback.
Guid ✅ Guid.NewGuid()→crypto.randomUUID(), Guid.Empty, Guid.Parse/TryParse/new Guid(text) → el texto canónico D en minúsculas (Desde 0.2.0-preview.60); cable como cadena

Clone this wiki locally